Compare commits

...

9 Commits

Author SHA1 Message Date
chen08209
5dda2854be Fix list form input view issues
Fix traffic view issues
2025-03-05 15:11:19 +08:00
chen08209
5184ed6fc7 Update changelog 2025-03-05 02:36:31 +00:00
chen08209
4e679f776e Optimize performance
Update core

Optimize core stability

Fix linux tun authority check error

Fix some issues
2025-03-05 10:21:51 +08:00
chen08209
96328f66e9 Fix scroll physics error 2025-02-09 16:51:57 +08:00
chen08209
3eb14ab8a1 Update changelog 2025-02-09 08:36:14 +00:00
chen08209
c6266b7917 Add windows storage corruption detection
Fix core crash caused by windows resource manager restart

Optimize logs, requests, access to pages

Fix macos bypass domain issues
2025-02-09 16:23:40 +08:00
chen08209
6c27f2e2f1 Update changelog 2025-02-03 13:28:20 +00:00
chen08209
e04a0094b1 Fix some issues 2025-02-03 21:15:26 +08:00
chen08209
683e6a58ea Update changelog 2025-02-02 11:48:19 +00:00
159 changed files with 15727 additions and 11844 deletions

6
.gitmodules vendored
View File

@@ -6,3 +6,9 @@
path = plugins/flutter_distributor path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git url = git@github.com:chen08209/flutter_distributor.git
branch = FlClash branch = FlClash
[submodule "plugins/tray_manager"]
path = plugins/tray_manager
url = git@github.com:chen08209/tray_manager.git
branch = main

View File

@@ -1,3 +1,55 @@
## v0.8.77
- Optimize performance
- Update core
- Optimize core stability
- Fix linux tun authority check error
- Fix some issues
- Fix scroll physics error
- Update changelog
## v0.8.75
- Add windows storage corruption detection
- Fix core crash caused by windows resource manager restart
- Optimize logs, requests, access to pages
- Fix macos bypass domain issues
- Update changelog
## v0.8.74
- Fix some issues
- Update changelog
## v0.8.73
- Update popup menu
- Add file editor
- Fix android service issues
- Optimize desktop background performance
- Optimize android main process performance
- Optimize delay test
- Optimize vpn protect
- Update changelog
## v0.8.72 ## v0.8.72
- Update core - Update core

View File

@@ -1,29 +1,8 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
linter: linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at analyzer:
# https://dart.dev/guides/language/analysis-options plugins:
- custom_lint

View File

@@ -4,5 +4,5 @@ data class Package(
val packageName: String, val packageName: String,
val label: String, val label: String,
val isSystem: Boolean, val isSystem: Boolean,
val firstInstallTime: Long, val lastUpdateTime: Long,
) )

View File

@@ -7,6 +7,7 @@ enum class AccessControlMode {
} }
data class AccessControl( data class AccessControl(
val enable: Boolean,
val mode: AccessControlMode, val mode: AccessControlMode,
val acceptList: List<String>, val acceptList: List<String>,
val rejectList: List<String>, val rejectList: List<String>,
@@ -17,7 +18,7 @@ data class CIDR(val address: InetAddress, val prefixLength: Int)
data class VpnOptions( data class VpnOptions(
val enable: Boolean, val enable: Boolean,
val port: Int, val port: Int,
val accessControl: AccessControl?, val accessControl: AccessControl,
val allowBypass: Boolean, val allowBypass: Boolean,
val systemProxy: Boolean, val systemProxy: Boolean,
val bypassDomain: List<String>, val bypassDomain: List<String>,

View File

@@ -37,7 +37,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@@ -302,7 +301,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
packageName = it.packageName, packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(), label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1, isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
firstInstallTime = it.firstInstallTime lastUpdateTime = it.lastUpdateTime
) )
}?.let { packages.addAll(it) } }?.let { packages.addAll(it) }
return packages return packages

View File

@@ -10,6 +10,7 @@ import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.follow.clash.FlClashApplication import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
@@ -92,11 +93,13 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
"setProtect" -> { "setProtect" -> {
val fd = call.argument<Int>("fd") val fd = call.argument<Int>("fd")
if (fd != null) { if (fd != null && flClashService is FlClashVpnService) {
if (flClashService is FlClashVpnService) { try {
(flClashService as FlClashVpnService).protect(fd) (flClashService as FlClashVpnService).protect(fd)
result.success(true)
} catch (e: RuntimeException) {
result.success(false)
} }
result.success(true)
} else { } else {
result.success(false) result.success(false)
} }

View File

@@ -68,17 +68,19 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} }
addDnsServer(options.dnsServerAddress) addDnsServer(options.dnsServerAddress)
setMtu(9000) setMtu(9000)
options.accessControl?.let { accessControl -> options.accessControl.let { accessControl ->
when (accessControl.mode) { if (accessControl.enable) {
AccessControlMode.acceptSelected -> { when (accessControl.mode) {
(accessControl.acceptList + packageName).forEach { AccessControlMode.acceptSelected -> {
addAllowedApplication(it) (accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
} }
}
AccessControlMode.rejectSelected -> { AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach { (accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it) addDisallowedApplication(it)
}
} }
} }
} }

View File

@@ -5,6 +5,7 @@ targets:
options: options:
build_extensions: build_extensions:
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart' '^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
freezed: freezed:
options: options:
build_extensions: build_extensions:

View File

@@ -22,99 +22,99 @@ func (result ActionResult) Json() ([]byte, error) {
return data, err return data, err
} }
func (action Action) wrapMessage(data interface{}) []byte { func (action Action) getResult(data interface{}) []byte {
sendAction := ActionResult{ resultAction := ActionResult{
Id: action.Id, Id: action.Id,
Method: action.Method, Method: action.Method,
Data: data, Data: data,
} }
res, _ := sendAction.Json() res, _ := resultAction.Json()
return res return res
} }
func handleAction(action *Action, send func([]byte)) { func handleAction(action *Action, result func(data interface{})) {
switch action.Method { switch action.Method {
case initClashMethod: case initClashMethod:
data := action.Data.(string) data := action.Data.(string)
send(action.wrapMessage(handleInitClash(data))) result(handleInitClash(data))
return return
case getIsInitMethod: case getIsInitMethod:
send(action.wrapMessage(handleGetIsInit())) result(handleGetIsInit())
return return
case forceGcMethod: case forceGcMethod:
handleForceGc() handleForceGc()
send(action.wrapMessage(true)) result(true)
return return
case shutdownMethod: case shutdownMethod:
send(action.wrapMessage(handleShutdown())) result(handleShutdown())
return return
case validateConfigMethod: case validateConfigMethod:
data := []byte(action.Data.(string)) data := []byte(action.Data.(string))
send(action.wrapMessage(handleValidateConfig(data))) result(handleValidateConfig(data))
return return
case updateConfigMethod: case updateConfigMethod:
data := []byte(action.Data.(string)) data := []byte(action.Data.(string))
send(action.wrapMessage(handleUpdateConfig(data))) result(handleUpdateConfig(data))
return return
case getProxiesMethod: case getProxiesMethod:
send(action.wrapMessage(handleGetProxies())) result(handleGetProxies())
return return
case changeProxyMethod: case changeProxyMethod:
data := action.Data.(string) data := action.Data.(string)
handleChangeProxy(data, func(value string) { handleChangeProxy(data, func(value string) {
send(action.wrapMessage(value)) result(value)
}) })
return return
case getTrafficMethod: case getTrafficMethod:
send(action.wrapMessage(handleGetTraffic())) result(handleGetTraffic())
return return
case getTotalTrafficMethod: case getTotalTrafficMethod:
send(action.wrapMessage(handleGetTotalTraffic())) result(handleGetTotalTraffic())
return return
case resetTrafficMethod: case resetTrafficMethod:
handleResetTraffic() handleResetTraffic()
send(action.wrapMessage(true)) result(true)
return return
case asyncTestDelayMethod: case asyncTestDelayMethod:
data := action.Data.(string) data := action.Data.(string)
handleAsyncTestDelay(data, func(value string) { handleAsyncTestDelay(data, func(value string) {
send(action.wrapMessage(value)) result(value)
}) })
return return
case getConnectionsMethod: case getConnectionsMethod:
send(action.wrapMessage(handleGetConnections())) result(handleGetConnections())
return return
case closeConnectionsMethod: case closeConnectionsMethod:
send(action.wrapMessage(handleCloseConnections())) result(handleCloseConnections())
return return
case closeConnectionMethod: case closeConnectionMethod:
id := action.Data.(string) id := action.Data.(string)
send(action.wrapMessage(handleCloseConnection(id))) result(handleCloseConnection(id))
return return
case getExternalProvidersMethod: case getExternalProvidersMethod:
send(action.wrapMessage(handleGetExternalProviders())) result(handleGetExternalProviders())
return return
case getExternalProviderMethod: case getExternalProviderMethod:
externalProviderName := action.Data.(string) externalProviderName := action.Data.(string)
send(action.wrapMessage(handleGetExternalProvider(externalProviderName))) result(handleGetExternalProvider(externalProviderName))
case updateGeoDataMethod: case updateGeoDataMethod:
paramsString := action.Data.(string) paramsString := action.Data.(string)
var params = map[string]string{} var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params) err := json.Unmarshal([]byte(paramsString), &params)
if err != nil { if err != nil {
send(action.wrapMessage(err.Error())) result(err.Error())
return return
} }
geoType := params["geo-type"] geoType := params["geo-type"]
geoName := params["geo-name"] geoName := params["geo-name"]
handleUpdateGeoData(geoType, geoName, func(value string) { handleUpdateGeoData(geoType, geoName, func(value string) {
send(action.wrapMessage(value)) result(value)
}) })
return return
case updateExternalProviderMethod: case updateExternalProviderMethod:
providerName := action.Data.(string) providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) { handleUpdateExternalProvider(providerName, func(value string) {
send(action.wrapMessage(value)) result(value)
}) })
return return
case sideLoadExternalProviderMethod: case sideLoadExternalProviderMethod:
@@ -122,46 +122,56 @@ func handleAction(action *Action, send func([]byte)) {
var params = map[string]string{} var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params) err := json.Unmarshal([]byte(paramsString), &params)
if err != nil { if err != nil {
send(action.wrapMessage(err.Error())) result(err.Error())
return return
} }
providerName := params["providerName"] providerName := params["providerName"]
data := params["data"] data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) { handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
send(action.wrapMessage(value)) result(value)
}) })
return return
case startLogMethod: case startLogMethod:
handleStartLog() handleStartLog()
send(action.wrapMessage(true)) result(true)
return return
case stopLogMethod: case stopLogMethod:
handleStopLog() handleStopLog()
send(action.wrapMessage(true)) result(true)
return return
case startListenerMethod: case startListenerMethod:
send(action.wrapMessage(handleStartListener())) result(handleStartListener())
return return
case stopListenerMethod: case stopListenerMethod:
send(action.wrapMessage(handleStopListener())) result(handleStopListener())
return return
case getCountryCodeMethod: case getCountryCodeMethod:
ip := action.Data.(string) ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) { handleGetCountryCode(ip, func(value string) {
send(action.wrapMessage(value)) result(value)
}) })
return return
case getMemoryMethod: case getMemoryMethod:
handleGetMemory(func(value string) { handleGetMemory(func(value string) {
send(action.wrapMessage(value)) result(value)
}) })
return return
case getProfileMethod:
profileId := action.Data.(string)
handleGetMemory(func(value string) {
result(handleGetProfile(profileId))
})
return
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
result(true)
default: default:
handle := nextHandle(action, send) handle := nextHandle(action, result)
if handle { if handle {
return return
} else { } else {
send(action.wrapMessage(action.DefaultValue)) result(action.DefaultValue)
} }
} }
} }

View File

@@ -31,7 +31,7 @@ import (
var ( var (
isRunning = false isRunning = false
runLock sync.Mutex runLock sync.Mutex
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"} ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
) )
@@ -215,6 +215,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Tun.Device = patchConfig.Tun.Device targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
targetConfig.GeodataLoader = patchConfig.GeodataLoader targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl targetConfig.GeoXUrl = patchConfig.GeoXUrl

View File

@@ -8,12 +8,11 @@ import (
) )
type ConfigExtendedParams struct { type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"` IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"` IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"` SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"` TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"` OverrideDns bool `json:"override-dns"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
} }
type GenerateConfigParams struct { type GenerateConfigParams struct {
@@ -80,6 +79,7 @@ const (
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions" getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
getRunTimeMethod Method = "getRunTime" getRunTimeMethod Method = "getRunTime"
getCurrentProfileNameMethod Method = "getCurrentProfileName" getCurrentProfileNameMethod Method = "getCurrentProfileName"
getProfileMethod Method = "getProfile"
) )
type Method string type Method string

View File

@@ -6,7 +6,7 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
require ( require (
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000 github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
github.com/samber/lo v1.47.0 github.com/samber/lo v1.49.1
) )
require ( require (
@@ -19,28 +19,28 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.1 // indirect github.com/ebitengine/purego v0.8.2 // indirect
github.com/enfein/mieru/v3 v3.10.0 // indirect github.com/enfein/mieru/v3 v3.11.2 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.0 // indirect github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect github.com/gofrs/uuid/v5 v5.3.1 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect
@@ -51,21 +51,22 @@ require (
github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.0 // indirect github.com/metacubex/chacha v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da // indirect github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.4.5 // indirect github.com/metacubex/sing-tun v0.4.5 // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
github.com/metacubex/utls v1.6.6 // indirect github.com/metacubex/utls v1.6.6 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.62 // indirect github.com/miekg/dns v1.1.63 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect
@@ -73,18 +74,18 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/cors v1.2.1 // indirect github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // indirect github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing v0.5.1 // indirect github.com/sagernet/sing v0.5.2 // indirect
github.com/sagernet/sing-mux v0.2.1 // indirect github.com/sagernet/sing-mux v0.2.1 // indirect
github.com/sagernet/sing-shadowtls v0.1.5 // indirect github.com/sagernet/sing-shadowtls v0.1.5 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/shirou/gopsutil/v4 v4.24.11 // indirect github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
@@ -101,13 +102,13 @@ require (
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.20.0 // indirect golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect

View File

@@ -24,12 +24,12 @@ github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ= github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo=
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o= github.com/enfein/mieru/v3 v3.11.2/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -43,8 +43,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
@@ -59,8 +59,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -74,8 +74,8 @@ github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@@ -84,6 +84,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -98,26 +99,28 @@ github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc= github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg= github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic= github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30= github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA= github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM= github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo= github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0= github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I= github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc= github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
@@ -126,10 +129,11 @@ github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo= github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
@@ -149,8 +153,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
@@ -164,18 +168,18 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJ
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo= github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE= github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo= github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4= github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
@@ -218,8 +222,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -228,11 +232,11 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -248,12 +252,12 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -263,8 +267,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"core/state"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
@@ -26,10 +27,8 @@ import (
) )
var ( var (
isInit = false isInit = false
configParams = ConfigExtendedParams{ configParams = ConfigExtendedParams{}
OnlyStatisticsProxy: false,
}
externalProviders = map[string]cp.Provider{} externalProviders = map[string]cp.Provider{}
logSubscriber observable.Subscription[log.Event] logSubscriber observable.Subscription[log.Event]
currentConfig *config.Config currentConfig *config.Config
@@ -152,7 +151,7 @@ func handleChangeProxy(data string, fn func(string string)) {
} }
func handleGetTraffic() string { func handleGetTraffic() string {
up, down := statistic.DefaultManager.Current(configParams.OnlyStatisticsProxy) up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -166,7 +165,7 @@ func handleGetTraffic() string {
} }
func handleGetTotalTraffic() string { func handleGetTotalTraffic() string {
up, down := statistic.DefaultManager.Total(configParams.OnlyStatisticsProxy) up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -179,6 +178,15 @@ func handleGetTotalTraffic() string {
return string(data) return string(data)
} }
func handleGetProfile(profileId string) string {
prof := getRawConfigWithId(profileId)
data, err := json.Marshal(prof)
if err != nil {
return ""
}
return string(data)
}
func handleResetTraffic() { func handleResetTraffic() {
statistic.DefaultManager.ResetStatistic() statistic.DefaultManager.ResetStatistic()
} }
@@ -220,6 +228,7 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
if params.TestUrl != "" { if params.TestUrl != "" {
testUrl = params.TestUrl testUrl = params.TestUrl
} }
delayData.Url = testUrl
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus) delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
if err != nil || delay == 0 { if err != nil || delay == 0 {
@@ -423,6 +432,10 @@ func handleGetMemory(fn func(value string)) {
}() }()
} }
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func init() { func init() {
adapter.UrlTestHook = func(url string, name string, delay uint16) { adapter.UrlTestHook = func(url string, name string, delay uint16) {
delayData := &Delay{ delayData := &Delay{

View File

@@ -49,8 +49,8 @@ func invokeAction(paramsChar *C.char, port C.longlong) {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
go handleAction(action, func(bytes []byte) { go handleAction(action, func(data interface{}) {
bridge.SendToPort(i, string(bytes)) bridge.SendToPort(i, string(action.getResult(data)))
}) })
} }
@@ -64,7 +64,7 @@ func sendMessage(message Message) {
} }
bridge.SendToPort(messagePort, string(Action{ bridge.SendToPort(messagePort, string(Action{
Method: messageMethod, Method: messageMethod,
}.wrapMessage(res))) }.getResult(res)))
} }
//export startListener //export startListener

View File

@@ -52,18 +52,6 @@ func NewInvokeManager() *InvokeManager {
} }
} }
func (m *InvokeManager) load(id string) string {
res, ok := m.invokeMap.Load(id)
if ok {
return res.(string)
}
return ""
}
func (m *InvokeManager) delete(id string) {
m.invokeMap.Delete(id)
}
func (m *InvokeManager) completer(id string, value string) { func (m *InvokeManager) completer(id string, value string) {
m.invokeMap.Store(id, value) m.invokeMap.Store(id, value)
m.chanLock.Lock() m.chanLock.Lock()
@@ -74,7 +62,7 @@ func (m *InvokeManager) completer(id string, value string) {
m.chanLock.Unlock() m.chanLock.Unlock()
} }
func (m *InvokeManager) await(id string) { func (m *InvokeManager) await(id string) string {
m.chanLock.Lock() m.chanLock.Lock()
if _, ok := m.chanMap[id]; !ok { if _, ok := m.chanMap[id]; !ok {
m.chanMap[id] = make(chan struct{}) m.chanMap[id] = make(chan struct{})
@@ -85,12 +73,17 @@ func (m *InvokeManager) await(id string) {
timeout := time.After(500 * time.Millisecond) timeout := time.After(500 * time.Millisecond)
select { select {
case <-ch: case <-ch:
return res, ok := m.invokeMap.Load(id)
m.invokeMap.Delete(id)
if ok {
return res.(string)
} else {
return ""
}
case <-timeout: case <-timeout:
m.completer(id, "") m.completer(id, "")
return return ""
} }
} }
var ( var (
@@ -195,7 +188,6 @@ func initSocketHook() {
}) })
fdInvokeMap.await(id) fdInvokeMap.await(id)
fdInvokeMap.delete(id)
}) })
} }
} }
@@ -214,10 +206,7 @@ func init() {
Id: id, Id: id,
Metadata: metadata, Metadata: metadata,
}) })
processInvokeMap.await(id) return processInvokeMap.await(id), nil
res := processInvokeMap.load(id)
processInvokeMap.delete(id)
return res, nil
} }
} }
@@ -225,14 +214,14 @@ func handleGetAndroidVpnOptions() string {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
options := state.AndroidVpnOptions{ options := state.AndroidVpnOptions{
Enable: state.CurrentState.Enable, Enable: state.CurrentState.VpnProps.Enable,
Port: currentConfig.General.MixedPort, Port: currentConfig.General.MixedPort,
Ipv4Address: state.DefaultIpv4Address, Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(), Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.AccessControl, AccessControl: state.CurrentState.VpnProps.AccessControl,
SystemProxy: state.CurrentState.SystemProxy, SystemProxy: state.CurrentState.VpnProps.SystemProxy,
AllowBypass: state.CurrentState.AllowBypass, AllowBypass: state.CurrentState.VpnProps.AllowBypass,
RouteAddress: state.CurrentState.RouteAddress, RouteAddress: currentConfig.General.Tun.RouteAddress,
BypassDomain: state.CurrentState.BypassDomain, BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(), DnsServerAddress: state.GetDnsServerAddress(),
} }
@@ -244,10 +233,6 @@ func handleGetAndroidVpnOptions() string {
return string(data) return string(data)
} }
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func handleUpdateDns(value string) { func handleUpdateDns(value string) {
go func() { go func() {
log.Infoln("[DNS] updateDns %s", value) log.Infoln("[DNS] updateDns %s", value)
@@ -263,46 +248,41 @@ func handleGetCurrentProfileName() string {
return state.CurrentState.CurrentProfileName return state.CurrentState.CurrentProfileName
} }
func nextHandle(action *Action, send func([]byte)) bool { func nextHandle(action *Action, result func(data interface{})) bool {
switch action.Method { switch action.Method {
case startTunMethod: case startTunMethod:
data := action.Data.(string) data := action.Data.(string)
var fd int var fd int
_ = json.Unmarshal([]byte(data), &fd) _ = json.Unmarshal([]byte(data), &fd)
send(action.wrapMessage(handleStartTun(fd))) result(handleStartTun(fd))
return true return true
case stopTunMethod: case stopTunMethod:
handleStopTun() handleStopTun()
send(action.wrapMessage(true)) result(true)
return true
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
send(action.wrapMessage(true))
return true return true
case getAndroidVpnOptionsMethod: case getAndroidVpnOptionsMethod:
send(action.wrapMessage(handleGetAndroidVpnOptions())) result(handleGetAndroidVpnOptions())
return true return true
case updateDnsMethod: case updateDnsMethod:
data := action.Data.(string) data := action.Data.(string)
handleUpdateDns(data) handleUpdateDns(data)
send(action.wrapMessage(true)) result(true)
return true return true
case setFdMapMethod: case setFdMapMethod:
fdId := action.Data.(string) fdId := action.Data.(string)
handleSetFdMap(fdId) handleSetFdMap(fdId)
send(action.wrapMessage(true)) result(true)
return true return true
case setProcessMapMethod: case setProcessMapMethod:
data := action.Data.(string) data := action.Data.(string)
handleSetProcessMap(data) handleSetProcessMap(data)
send(action.wrapMessage(true)) result(true)
return true return true
case getRunTimeMethod: case getRunTimeMethod:
send(action.wrapMessage(handleGetRunTime())) result(handleGetRunTime())
return true return true
case getCurrentProfileNameMethod: case getCurrentProfileNameMethod:
send(action.wrapMessage(handleGetCurrentProfileName())) result(handleGetCurrentProfileName())
return true return true
} }
return false return false
@@ -315,7 +295,10 @@ func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, po
bytes := []byte(C.GoString(paramsChar)) bytes := []byte(C.GoString(paramsChar))
stateParams := C.GoString(stateParamsChar) stateParams := C.GoString(stateParamsChar)
go func() { go func() {
handleInitClash(dir) res := handleInitClash(dir)
if res == false {
bridge.SendToPort(i, "init error")
}
handleSetState(stateParams) handleSetState(stateParams)
bridge.SendToPort(i, handleUpdateConfig(bytes)) bridge.SendToPort(i, handleUpdateConfig(bytes))
}() }()

View File

@@ -2,10 +2,6 @@
package main package main
func nextHandle(action *Action) { func nextHandle(action *Action, result func(data interface{})) bool {
return action
}
func nextHandle(action *Action, send func([]byte)) bool {
return false return false
} }

View File

@@ -19,7 +19,7 @@ func sendMessage(message Message) {
} }
send(Action{ send(Action{
Method: messageMethod, Method: messageMethod,
}.wrapMessage(res)) }.getResult(res))
} }
func send(data []byte) { func send(data []byte) {
@@ -61,12 +61,12 @@ func startServer(arg string) {
return return
} }
go handleAction(action, func(bytes []byte) { go handleAction(action, func(data interface{}) {
send(bytes) send(action.getResult(data))
}) })
} }
} }
func nextHandle(action *Action, send func([]byte)) bool { func nextHandle(action *Action, result func(data interface{})) bool {
return false return false
} }

View File

@@ -1,7 +1,7 @@
//go:build android && cgo
package state package state
import "net/netip"
var DefaultIpv4Address = "172.19.0.1/30" var DefaultIpv4Address = "172.19.0.1/30"
var DefaultDnsAddress = "172.19.0.2" var DefaultDnsAddress = "172.19.0.2"
var DefaultIpv6Address = "fdfe:dcba:9876::1/126" var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
@@ -13,13 +13,14 @@ type AndroidVpnOptions struct {
AllowBypass bool `json:"allowBypass"` AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"` SystemProxy bool `json:"systemProxy"`
BypassDomain []string `json:"bypassDomain"` BypassDomain []string `json:"bypassDomain"`
RouteAddress []string `json:"routeAddress"` RouteAddress []netip.Prefix `json:"routeAddress"`
Ipv4Address string `json:"ipv4Address"` Ipv4Address string `json:"ipv4Address"`
Ipv6Address string `json:"ipv6Address"` Ipv6Address string `json:"ipv6Address"`
DnsServerAddress string `json:"dnsServerAddress"` DnsServerAddress string `json:"dnsServerAddress"`
} }
type AccessControl struct { type AccessControl struct {
Enable bool `json:"enable"`
Mode string `json:"mode"` Mode string `json:"mode"`
AcceptList []string `json:"acceptList"` AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"` RejectList []string `json:"rejectList"`
@@ -31,20 +32,23 @@ type AndroidVpnRawOptions struct {
AccessControl *AccessControl `json:"accessControl"` AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"` AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"` SystemProxy bool `json:"systemProxy"`
RouteAddress []string `json:"routeAddress"`
Ipv6 bool `json:"ipv6"` Ipv6 bool `json:"ipv6"`
BypassDomain []string `json:"bypassDomain"`
} }
type State struct { type State struct {
AndroidVpnRawOptions VpnProps AndroidVpnRawOptions `json:"vpn-props"`
CurrentProfileName string `json:"currentProfileName"` CurrentProfileName string `json:"current-profile-name"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
BypassDomain []string `json:"bypass-domain"`
} }
var CurrentState = &State{} var CurrentState = &State{
OnlyStatisticsProxy: false,
CurrentProfileName: "",
}
func GetIpv6Address() string { func GetIpv6Address() string {
if CurrentState.Ipv6 { if CurrentState.VpnProps.Ipv6 {
return DefaultIpv6Address return DefaultIpv6Address
} else { } else {
return "" return ""

View File

@@ -33,7 +33,7 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
} }
prefix4 = append(prefix4, tempPrefix4) prefix4 = append(prefix4, tempPrefix4)
var prefix6 []netip.Prefix var prefix6 []netip.Prefix
if state.CurrentState.Ipv6 { if state.CurrentState.VpnProps.Ipv6 {
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address) tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
if err != nil { if err != nil {
log.Errorln("startTUN error:", err) log.Errorln("startTUN error:", err)

View File

@@ -7,57 +7,27 @@ import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/hotkey_manager.dart'; import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart'; import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'controller.dart'; import 'controller.dart';
import 'models/models.dart'; import 'models/models.dart';
import 'pages/pages.dart'; import 'pages/pages.dart';
runAppWithPreferences( class Application extends ConsumerStatefulWidget {
Widget child, {
required AppState appState,
required Config config,
required AppFlowingState appFlowingState,
required ClashConfig clashConfig,
}) {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<ClashConfig>(
create: (_) => clashConfig,
),
ChangeNotifierProvider<Config>(
create: (_) => config,
),
ChangeNotifierProvider<AppFlowingState>(
create: (_) => appFlowingState,
),
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
create: (_) => appState,
update: (_, config, clashConfig, appState) {
appState?.mode = clashConfig.mode;
appState?.selectedMap = config.currentSelectedMap;
return appState!;
},
)
],
child: child,
));
}
class Application extends StatefulWidget {
const Application({ const Application({
super.key, super.key,
}); });
@override @override
State<Application> createState() => ApplicationState(); ConsumerState<Application> createState() => ApplicationState();
} }
class ApplicationState extends State<Application> { class ApplicationState extends ConsumerState<Application> {
late SystemColorSchemes systemColorSchemes; late ColorSchemes systemColorSchemes;
Timer? _autoUpdateGroupTaskTimer; Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer; Timer? _autoUpdateProfilesTaskTimer;
@@ -73,7 +43,7 @@ class ApplicationState extends State<Application> {
ColorScheme _getAppColorScheme({ ColorScheme _getAppColorScheme({
required Brightness brightness, required Brightness brightness,
int? primaryColor, int? primaryColor,
required SystemColorSchemes systemColorSchemes, required ColorSchemes systemColorSchemes,
}) { }) {
if (primaryColor != null) { if (primaryColor != null) {
return ColorScheme.fromSeed( return ColorScheme.fromSeed(
@@ -81,7 +51,7 @@ class ApplicationState extends State<Application> {
brightness: brightness, brightness: brightness,
); );
} else { } else {
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness); return systemColorSchemes.getColorSchemeForBrightness(brightness);
} }
} }
@@ -90,12 +60,21 @@ class ApplicationState extends State<Application> {
super.initState(); super.initState();
_autoUpdateGroupTask(); _autoUpdateGroupTask();
_autoUpdateProfilesTask(); _autoUpdateProfilesTask();
globalState.appController = AppController(context); globalState.appController = AppController(context, ref);
globalState.measure = Measure.of(context); globalState.measure = Measure.of(context);
ref.listenManual(themeSettingProvider.select((state) => state.fontFamily),
(prev, next) {
if (prev != next) {
globalState.measure = Measure.of(
context,
fontFamily: next.value,
);
}
}, fireImmediately: true);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext; final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) { if (currentContext != null) {
globalState.appController = AppController(currentContext); globalState.appController = AppController(currentContext, ref);
} }
await globalState.appController.init(); await globalState.appController.init();
globalState.appController.initLink(); globalState.appController.initLink();
@@ -167,7 +146,7 @@ class ApplicationState extends State<Application> {
ColorScheme? lightDynamic, ColorScheme? lightDynamic,
ColorScheme? darkDynamic, ColorScheme? darkDynamic,
) { ) {
systemColorSchemes = SystemColorSchemes( systemColorSchemes = ColorSchemes(
lightColorScheme: lightDynamic, lightColorScheme: lightDynamic,
darkColorScheme: darkDynamic, darkColorScheme: darkDynamic,
); );
@@ -180,15 +159,11 @@ class ApplicationState extends State<Application> {
Widget build(context) { Widget build(context) {
return _buildPlatformWrap( return _buildPlatformWrap(
_buildWrap( _buildWrap(
Selector2<AppState, Config, ApplicationSelectorState>( Consumer(
selector: (_, appState, config) => ApplicationSelectorState( builder: (_, ref, child) {
locale: config.appSetting.locale, final locale =
themeMode: config.themeProps.themeMode, ref.watch(appSettingProvider.select((state) => state.locale));
primaryColor: config.themeProps.primaryColor, final themeProps = ref.watch(themeSettingProvider);
prueBlack: config.themeProps.prueBlack,
fontFamily: config.themeProps.fontFamily,
),
builder: (_, state, child) {
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) { builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic); _updateSystemColorSchemes(lightDynamic, darkDynamic);
@@ -204,11 +179,9 @@ class ApplicationState extends State<Application> {
return MessageManager( return MessageManager(
child: LayoutBuilder( child: LayoutBuilder(
builder: (_, container) { builder: (_, container) {
final appController = globalState.appController; globalState.appController.updateViewWidth(
final maxWidth = container.maxWidth; container.maxWidth,
if (appController.appState.viewWidth != maxWidth) { );
globalState.appController.updateViewWidth(maxWidth);
}
return _buildPage(child!); return _buildPage(child!);
}, },
), ),
@@ -216,28 +189,28 @@ class ApplicationState extends State<Application> {
}, },
scrollBehavior: BaseScrollBehavior(), scrollBehavior: BaseScrollBehavior(),
title: appName, title: appName,
locale: other.getLocaleForString(state.locale), locale: other.getLocaleForString(locale),
supportedLocales: AppLocalizations.delegate.supportedLocales, supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode, themeMode: themeProps.themeMode,
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
fontFamily: state.fontFamily.value, fontFamily: themeProps.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme, pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme( colorScheme: _getAppColorScheme(
brightness: Brightness.light, brightness: Brightness.light,
systemColorSchemes: systemColorSchemes, systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor, primaryColor: themeProps.primaryColor,
), ),
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
useMaterial3: true, useMaterial3: true,
fontFamily: state.fontFamily.value, fontFamily: themeProps.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme, pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme( colorScheme: _getAppColorScheme(
brightness: Brightness.dark, brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes, systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor, primaryColor: themeProps.primaryColor,
).toPrueBlack(state.prueBlack), ).toPrueBlack(themeProps.prueBlack),
), ),
home: child, home: child,
); );

View File

@@ -33,7 +33,7 @@ class ClashCore {
} }
static Future<void> initGeo() async { static Future<void> initGeo() async {
final homePath = await appPath.getHomeDirPath(); final homePath = await appPath.homeDirPath;
final homeDir = Directory(homePath); final homeDir = Directory(homePath);
final isExists = await homeDir.exists(); final isExists = await homeDir.exists();
if (!isExists) { if (!isExists) {
@@ -63,15 +63,16 @@ class ClashCore {
} }
} }
Future<bool> init({ Future<bool> init() async {
required ClashConfig clashConfig,
required Config config,
}) async {
await initGeo(); await initGeo();
final homeDirPath = await appPath.getHomeDirPath(); final homeDirPath = await appPath.homeDirPath;
return await clashInterface.init(homeDirPath); return await clashInterface.init(homeDirPath);
} }
Future<bool> setState(CoreState state) async {
return await clashInterface.setState(state);
}
shutdown() async { shutdown() async {
await clashInterface.shutdown(); await clashInterface.shutdown();
} }
@@ -234,6 +235,14 @@ class ClashCore {
return int.parse(value); return int.parse(value);
} }
Future<ClashConfig?> getProfile(String id) async {
final res = await clashInterface.getProfile(id);
if (res.isEmpty) {
return null;
}
return ClashConfig.fromJson(json.decode(res));
}
resetTraffic() { resetTraffic() {
clashInterface.resetTraffic(); clashInterface.resetTraffic();
} }

View File

@@ -2,12 +2,9 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:fl_clash/clash/message.dart'; import 'package:fl_clash/clash/message.dart';
import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/future.dart';
import 'package:fl_clash/common/other.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart' hide Action;
mixin ClashInterface { mixin ClashInterface {
Future<bool> init(String homeDir); Future<bool> init(String homeDir);
@@ -66,6 +63,10 @@ mixin ClashInterface {
FutureOr<bool> closeConnection(String id); FutureOr<bool> closeConnection(String id);
FutureOr<bool> closeConnections(); FutureOr<bool> closeConnections();
FutureOr<String> getProfile(String id);
Future<bool> setState(CoreState state);
} }
mixin AndroidClashInterface { mixin AndroidClashInterface {
@@ -73,8 +74,6 @@ mixin AndroidClashInterface {
Future<bool> setProcessMap(ProcessMapItem item); Future<bool> setProcessMap(ProcessMapItem item);
Future<bool> setState(CoreState state);
Future<bool> stopTun(); Future<bool> stopTun();
Future<bool> updateDns(String value); Future<bool> updateDns(String value);
@@ -106,6 +105,7 @@ abstract class ClashHandlerInterface with ClashInterface {
case ActionMethod.closeConnections: case ActionMethod.closeConnections:
case ActionMethod.closeConnection: case ActionMethod.closeConnection:
case ActionMethod.stopListener: case ActionMethod.stopListener:
case ActionMethod.setState:
completer?.complete(result.data as bool); completer?.complete(result.data as bool);
return; return;
case ActionMethod.changeProxy: case ActionMethod.changeProxy:
@@ -137,7 +137,7 @@ abstract class ClashHandlerInterface with ClashInterface {
completer?.complete(result.data); completer?.complete(result.data);
} }
} catch (_) { } catch (_) {
debugPrint(result.id); commonPrint.log(result.id);
} }
} }
@@ -198,6 +198,14 @@ abstract class ClashHandlerInterface with ClashInterface {
); );
} }
@override
Future<bool> setState(CoreState state) {
return invoke<bool>(
method: ActionMethod.setState,
data: json.encode(state),
);
}
@override @override
shutdown() async { shutdown() async {
return await invoke<bool>( return await invoke<bool>(
@@ -232,6 +240,7 @@ abstract class ClashHandlerInterface with ClashInterface {
return await invoke<String>( return await invoke<String>(
method: ActionMethod.updateConfig, method: ActionMethod.updateConfig,
data: json.encode(updateConfigParams), data: json.encode(updateConfigParams),
timeout: Duration(minutes: 2),
); );
} }
@@ -239,6 +248,7 @@ abstract class ClashHandlerInterface with ClashInterface {
Future<String> getProxies() { Future<String> getProxies() {
return invoke<String>( return invoke<String>(
method: ActionMethod.getProxies, method: ActionMethod.getProxies,
timeout: Duration(seconds: 5),
); );
} }
@@ -268,9 +278,9 @@ abstract class ClashHandlerInterface with ClashInterface {
@override @override
Future<String> updateGeoData(UpdateGeoDataParams params) { Future<String> updateGeoData(UpdateGeoDataParams params) {
return invoke<String>( return invoke<String>(
method: ActionMethod.updateGeoData, method: ActionMethod.updateGeoData,
data: json.encode(params), data: json.encode(params),
); timeout: Duration(minutes: 1));
} }
@override @override
@@ -292,6 +302,7 @@ abstract class ClashHandlerInterface with ClashInterface {
return invoke<String>( return invoke<String>(
method: ActionMethod.updateExternalProvider, method: ActionMethod.updateExternalProvider,
data: providerName, data: providerName,
timeout: Duration(minutes: 1),
); );
} }
@@ -317,6 +328,14 @@ abstract class ClashHandlerInterface with ClashInterface {
); );
} }
@override
Future<String> getProfile(String id) {
return invoke<String>(
method: ActionMethod.getProfile,
data: id,
);
}
@override @override
FutureOr<String> getTotalTraffic() { FutureOr<String> getTotalTraffic() {
return invoke<String>( return invoke<String>(

View File

@@ -67,7 +67,6 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
switch (result.method) { switch (result.method) {
case ActionMethod.setFdMap: case ActionMethod.setFdMap:
case ActionMethod.setProcessMap: case ActionMethod.setProcessMap:
case ActionMethod.setState:
case ActionMethod.stopTun: case ActionMethod.stopTun:
case ActionMethod.updateDns: case ActionMethod.updateDns:
completer?.complete(result.data as bool); completer?.complete(result.data as bool);
@@ -123,14 +122,6 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
); );
} }
@override
Future<bool> setState(CoreState state) {
return invoke<bool>(
method: ActionMethod.setState,
data: json.encode(state),
);
}
@override @override
Future<DateTime?> startTun(int fd) async { Future<DateTime?> startTun(int fd) async {
final res = await invoke<String>( final res = await invoke<String>(
@@ -259,7 +250,7 @@ class ClashLibHandler {
setProcessMap(ProcessMapItem processMapItem) { setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar = final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>(); json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar); clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar); malloc.free(processMapItemChar);
} }
@@ -321,10 +312,10 @@ class ClashLibHandler {
} }
Future<String> quickStart( Future<String> quickStart(
String homeDir, String homeDir,
UpdateConfigParams updateConfigParams, UpdateConfigParams updateConfigParams,
CoreState state, CoreState state,
) { ) {
final completer = Completer<String>(); final completer = Completer<String>();
final receiver = ReceivePort(); final receiver = ReceivePort();
receiver.listen((message) { receiver.listen((message) {

View File

@@ -6,6 +6,7 @@ import 'dart:typed_data';
import 'package:fl_clash/clash/interface.dart'; import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/core.dart'; import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/state.dart';
class ClashService extends ClashHandlerInterface { class ClashService extends ClashHandlerInterface {
static ClashService? _instance; static ClashService? _instance;
@@ -14,6 +15,8 @@ class ClashService extends ClashHandlerInterface {
Completer<Socket> socketCompleter = Completer(); Completer<Socket> socketCompleter = Completer();
bool isStarting = false;
Process? process; Process? process;
factory ClashService() { factory ClashService() {
@@ -27,48 +30,61 @@ class ClashService extends ClashHandlerInterface {
} }
_initServer() async { _initServer() async {
final address = !Platform.isWindows runZonedGuarded(() async {
? InternetAddress( final address = !Platform.isWindows
unixSocketPath, ? InternetAddress(
type: InternetAddressType.unix, unixSocketPath,
) type: InternetAddressType.unix,
: InternetAddress( )
localhost, : InternetAddress(
type: InternetAddressType.IPv4, localhost,
); type: InternetAddressType.IPv4,
await _deleteSocketFile(); );
final server = await ServerSocket.bind( await _deleteSocketFile();
address, final server = await ServerSocket.bind(
0, address,
shared: true, 0,
); shared: true,
serverCompleter.complete(server); );
await for (final socket in server) { serverCompleter.complete(server);
await _destroySocket(); await for (final socket in server) {
socketCompleter.complete(socket); await _destroySocket();
socket socketCompleter.complete(socket);
.transform( socket
StreamTransformer<Uint8List, String>.fromHandlers( .transform(
handleData: (Uint8List data, EventSink<String> sink) { StreamTransformer<Uint8List, String>.fromHandlers(
sink.add(utf8.decode(data, allowMalformed: true)); handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
),
)
.transform(LineSplitter())
.listen(
(data) {
handleResult(
ActionResult.fromJson(
json.decode(data.trim()),
),
);
}, },
), );
) }
.transform(LineSplitter()) }, (error, stack) {
.listen( commonPrint.log(error.toString());
(data) { if(error is SocketException){
handleResult( globalState.showNotifier(error.toString());
ActionResult.fromJson( globalState.appController.restartCore();
json.decode(data.trim()), }
), });
);
},
);
}
} }
@override @override
reStart() async { reStart() async {
if (isStarting == true) {
return;
}
isStarting = true;
socketCompleter = Completer();
if (process != null) { if (process != null) {
await shutdown(); await shutdown();
} }
@@ -90,6 +106,7 @@ class ClashService extends ClashHandlerInterface {
], ],
); );
process!.stdout.listen((_) {}); process!.stdout.listen((_) {});
isStarting = false;
} }
@override @override
@@ -125,7 +142,6 @@ class ClashService extends ClashHandlerInterface {
@override @override
shutdown() async { shutdown() async {
await super.shutdown();
if (Platform.isWindows) { if (Platform.isWindows) {
await request.stopCoreByHelper(); await request.stopCoreByHelper();
} }

View File

@@ -11,7 +11,7 @@ extension ColorExtension on Color {
} }
Color get toSoft { Color get toSoft {
return withOpacity(0.12); return withOpacity(0.15);
} }
Color get toLittle { Color get toLittle {

View File

@@ -34,4 +34,6 @@ export 'text.dart';
export 'tray.dart'; export 'tray.dart';
export 'window.dart'; export 'window.dart';
export 'windows.dart'; export 'windows.dart';
export 'render.dart'; export 'render.dart';
export 'mixin.dart';
export 'print.dart';

View File

@@ -11,6 +11,8 @@ import 'package:flutter/material.dart';
const appName = "FlClash"; const appName = "FlClash";
const appHelperService = "FlClashHelperService"; const appHelperService = "FlClashHelperService";
const coreName = "clash.meta"; const coreName = "clash.meta";
const browserUa =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
const packageName = "com.follow.clash"; const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock"; final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890; const helperPort = 47890;
@@ -33,16 +35,6 @@ final double kHeaderHeight = system.isDesktop
? 40 ? 40
: 28 : 28
: 0; : 0;
const GeoXMap defaultGeoXMap = {
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
"geosite":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
};
const profilesDirectoryName = "profiles"; const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1"; const localhost = "127.0.0.1";
const clashConfigKey = "clash_config"; const clashConfigKey = "clash_config";
@@ -53,8 +45,6 @@ const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090"; const defaultExternalController = "127.0.0.1:9090";
const maxMobileWidth = 600; const maxMobileWidth = 600;
const maxLaptopWidth = 840; const maxLaptopWidth = 840;
const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard";
const defaultTestUrl = "https://www.gstatic.com/generate_204"; const defaultTestUrl = "https://www.gstatic.com/generate_204";
final filter = ImageFilter.blur( final filter = ImageFilter.blur(
sigmaX: 5, sigmaX: 5,

View File

@@ -22,4 +22,23 @@ extension BuildContextExtension on BuildContext {
ColorScheme get colorScheme => Theme.of(this).colorScheme; ColorScheme get colorScheme => Theme.of(this).colorScheme;
TextTheme get textTheme => Theme.of(this).textTheme; TextTheme get textTheme => Theme.of(this).textTheme;
T? findLastStateOfType<T extends State>() {
T? state;
visitor(Element element) {
if(!element.mounted){
return;
}
if(element is StatefulElement){
if (element.state is T) {
state = element.state as T;
}
}
element.visitChildren(visitor);
}
visitor(this as Element);
return state;
}
} }

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
class Debouncer { class Debouncer {
Map<dynamic, Timer> operators = {}; final Map<dynamic, Timer> _operations = {};
call( call(
dynamic tag, dynamic tag,
@@ -9,14 +9,15 @@ class Debouncer {
List<dynamic>? args, List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600), Duration duration = const Duration(milliseconds: 600),
}) { }) {
final timer = operators[tag]; final timer = _operations[tag];
if (timer != null) { if (timer != null) {
timer.cancel(); timer.cancel();
} }
operators[tag] = Timer( _operations[tag] = Timer(
duration, duration,
() { () {
operators.remove(tag); _operations[tag]?.cancel();
_operations.remove(tag);
Function.apply( Function.apply(
func, func,
args, args,
@@ -26,8 +27,59 @@ class Debouncer {
} }
cancel(dynamic tag) { cancel(dynamic tag) {
operators[tag]?.cancel(); _operations[tag]?.cancel();
} }
} }
class Throttler {
final Map<dynamic, Timer> _operations = {};
call(
String tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
}) {
final timer = _operations[tag];
if (timer != null) {
return true;
}
_operations[tag] = Timer(
duration,
() {
_operations[tag]?.cancel();
_operations.remove(tag);
Function.apply(
func,
args,
);
},
);
return false;
}
cancel(dynamic tag) {
_operations[tag]?.cancel();
}
}
Future<T> retry<T>({
required Future<T> Function() task,
int maxAttempts = 3,
required bool Function(T res) retryIf,
Duration delay = Duration.zero,
}) async {
int attempts = 0;
while (attempts < maxAttempts) {
final res = await task();
if (!retryIf(res) || attempts >= maxAttempts) {
return res;
}
attempts++;
}
throw "unknown error";
}
final debouncer = Debouncer(); final debouncer = Debouncer();
final throttler = Throttler();

View File

@@ -10,7 +10,7 @@ extension CompleterExt<T> on Completer<T> {
FutureOr<T> Function()? onTimeout, FutureOr<T> Function()? onTimeout,
required String functionName, required String functionName,
}) { }) {
final realTimeout = timeout ?? const Duration(seconds: 1); final realTimeout = timeout ?? const Duration(seconds: 30);
Timer(realTimeout + commonDuration, () { Timer(realTimeout + commonDuration, () {
if (onLast != null) { if (onLast != null) {
onLast(); onLast();

View File

@@ -1,9 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:fl_clash/common/common.dart';
import '../state.dart'; import '../state.dart';
import 'constant.dart';
class FlClashHttpOverrides extends HttpOverrides { class FlClashHttpOverrides extends HttpOverrides {
@override @override
@@ -14,10 +13,9 @@ class FlClashHttpOverrides extends HttpOverrides {
if ([localhost].contains(url.host)) { if ([localhost].contains(url.host)) {
return "DIRECT"; return "DIRECT";
} }
final appController = globalState.appController; final port = globalState.config.patchClashConfig.mixedPort;
final port = appController.clashConfig.mixedPort; final isStart = globalState.appState.runTime != null;
final isStart = appController.appFlowingState.isStart; commonPrint.log("find $url proxy:$isStart");
debugPrint("find $url proxy:$isStart");
if (!isStart) return "DIRECT"; if (!isStart) return "DIRECT";
return "PROXY localhost:$port"; return "PROXY localhost:$port";
}; };

View File

@@ -1,7 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'print.dart';
typedef InstallConfigCallBack = void Function(String url); typedef InstallConfigCallBack = void Function(String url);
@@ -15,11 +16,11 @@ class LinkManager {
} }
initAppLinksListen(installConfigCallBack) async { initAppLinksListen(installConfigCallBack) async {
debugPrint("initAppLinksListen"); commonPrint.log("initAppLinksListen");
destroy(); destroy();
subscription = _appLinks.uriLinkStream.listen( subscription = _appLinks.uriLinkStream.listen(
(uri) { (uri) {
debugPrint('onAppLink: $uri'); commonPrint.log('onAppLink: $uri');
if (uri.host == 'install-config') { if (uri.host == 'install-config') {
final parameters = uri.queryParameters; final parameters = uri.queryParameters;
final url = parameters['url']; final url = parameters['url'];

View File

@@ -1,3 +1,66 @@
import 'dart:collection';
class FixedList<T> {
final int maxLength;
final List<T> _list;
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
add(T item) {
if (_list.length == maxLength) {
_list.removeAt(0);
}
_list.add(item);
}
clear() {
_list.clear();
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
FixedList<T> copyWith() {
return FixedList(
maxLength,
list: _list,
);
}
}
class FixedMap<K, V> {
final int maxSize;
final Map<K, V> _map = {};
final Queue<K> _queue = Queue<K>();
FixedMap(this.maxSize);
put(K key, V value) {
if (_map.length == maxSize) {
final oldestKey = _queue.removeFirst();
_map.remove(oldestKey);
}
_map[key] = value;
_queue.add(key);
}
clear() {
_map.clear();
_queue.clear();
}
V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length;
Map<K, V> get map => Map.unmodifiable(_map);
}
extension ListExtension<T> on List<T> { extension ListExtension<T> on List<T> {
List<T> intersection(List<T> list) { List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList(); return where((item) => list.contains(item)).toList();
@@ -17,8 +80,8 @@ extension ListExtension<T> on List<T> {
} }
List<T> safeSublist(int start) { List<T> safeSublist(int start) {
if(start <= 0) return this; if (start <= 0) return this;
if(start > length) return []; if (start > length) return [];
return sublist(start); return sublist(start);
} }
} }

View File

@@ -15,7 +15,7 @@ class SingleInstanceLock {
Future<bool> acquire() async { Future<bool> acquire() async {
try { try {
final lockFilePath = await appPath.getLockFilePath(); final lockFilePath = await appPath.lockFilePath;
final lockFile = File(lockFilePath); final lockFile = File(lockFilePath);
await lockFile.create(); await lockFile.create();
_accessFile = await lockFile.open(mode: FileMode.write); _accessFile = await lockFile.open(mode: FileMode.write);

View File

@@ -4,20 +4,32 @@ import 'package:flutter/material.dart';
class Measure { class Measure {
final TextScaler _textScale; final TextScaler _textScale;
late BuildContext context; final BuildContext context;
final String? _fontFamily;
Measure.of(this.context) Measure.of(this.context, {String? fontFamily})
: _textScale = TextScaler.linear( : _textScale = TextScaler.linear(
WidgetsBinding.instance.platformDispatcher.textScaleFactor, WidgetsBinding.instance.platformDispatcher.textScaleFactor,
); ),
_fontFamily = fontFamily ?? "";
Size computeTextSize(Text text) { Size computeTextSize(
Text text, {
double maxWidth = double.infinity,
}) {
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan(text: text.data, style: text.style), text: TextSpan(
text: text.data,
style: text.style?.copyWith(
fontFamily: _fontFamily,
),
),
maxLines: text.maxLines, maxLines: text.maxLines,
textScaler: _textScale, textScaler: _textScale,
textDirection: text.textDirection ?? TextDirection.ltr, textDirection: text.textDirection ?? TextDirection.ltr,
)..layout(); )..layout(
maxWidth: maxWidth,
);
return textPainter.size; return textPainter.size;
} }

46
lib/common/mixin.dart Normal file
View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart';
import 'context.dart';
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
set value(T value) {
state = value;
}
@override
bool updateShouldNotify(previous, next) {
final res = super.updateShouldNotify(previous, next);
if (res) {
onUpdate(next);
}
return res;
}
onUpdate(T value) {}
}
mixin PageMixin<T extends StatefulWidget> on State<T> {
void onPageShow() {
initPageState();
}
initPageState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onSearch = onSearch;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
});
}
void onPageHidden() {}
List<Widget> get actions => [];
Widget? get floatingActionButton => null;
Function(String)? get onSearch => null;
Function(List<String>)? get onKeywordsUpdate => null;
}

View File

@@ -6,55 +6,81 @@ import 'package:flutter/material.dart';
class Navigation { class Navigation {
static Navigation? _instance; static Navigation? _instance;
getItems({ List<NavigationItem> getItems({
bool openLogs = false, bool openLogs = false,
bool hasProxies = false, bool hasProxies = false,
}) { }) {
return [ return [
const NavigationItem( const NavigationItem(
icon: Icon(Icons.space_dashboard), icon: Icon(Icons.space_dashboard),
label: "dashboard", label: PageLabel.dashboard,
fragment: DashboardFragment(), fragment: DashboardFragment(
key: GlobalObjectKey(PageLabel.dashboard),
),
), ),
NavigationItem( NavigationItem(
icon: const Icon(Icons.rocket), icon: const Icon(Icons.article),
label: "proxies", label: PageLabel.proxies,
fragment: const ProxiesFragment(), fragment: const ProxiesFragment(
key: GlobalObjectKey(
PageLabel.proxies,
),
),
modes: hasProxies modes: hasProxies
? [NavigationItemMode.mobile, NavigationItemMode.desktop] ? [NavigationItemMode.mobile, NavigationItemMode.desktop]
: [], : [],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.folder), icon: Icon(Icons.folder),
label: "profiles", label: PageLabel.profiles,
fragment: ProfilesFragment(), fragment: ProfilesFragment(
key: GlobalObjectKey(
PageLabel.profiles,
),
),
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.view_timeline), icon: Icon(Icons.view_timeline),
label: "requests", label: PageLabel.requests,
fragment: RequestsFragment(), fragment: RequestsFragment(
key: GlobalObjectKey(
PageLabel.requests,
),
),
description: "requestsDesc", description: "requestsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.ballot), icon: Icon(Icons.ballot),
label: "connections", label: PageLabel.connections,
fragment: ConnectionsFragment(), fragment: ConnectionsFragment(
key: GlobalObjectKey(
PageLabel.connections,
),
),
description: "connectionsDesc", description: "connectionsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.storage), icon: Icon(Icons.storage),
label: "resources", label: PageLabel.resources,
description: "resourcesDesc", description: "resourcesDesc",
keep: false, keep: false,
fragment: Resources(), fragment: Resources(
modes: [NavigationItemMode.desktop, NavigationItemMode.more], key: GlobalObjectKey(
PageLabel.resources,
),
),
modes: [NavigationItemMode.more],
), ),
NavigationItem( NavigationItem(
icon: const Icon(Icons.adb), icon: const Icon(Icons.adb),
label: "logs", label: PageLabel.logs,
fragment: const LogsFragment(), fragment: const LogsFragment(
key: GlobalObjectKey(
PageLabel.logs,
),
),
description: "logsDesc", description: "logsDesc",
modes: openLogs modes: openLogs
? [NavigationItemMode.desktop, NavigationItemMode.more] ? [NavigationItemMode.desktop, NavigationItemMode.more]
@@ -62,8 +88,12 @@ class Navigation {
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.construction), icon: Icon(Icons.construction),
label: "tools", label: PageLabel.tools,
fragment: ToolsFragment(), fragment: ToolsFragment(
key: GlobalObjectKey(
PageLabel.tools,
),
),
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile], modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
), ),
]; ];

View File

@@ -1,10 +1,12 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class BaseNavigator { class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async { static Future<T?> push<T>(BuildContext context, Widget child) async {
if (!globalState.appController.isMobileView) { if (globalState.appState.viewMode != ViewMode.mobile) {
return await Navigator.of(context).push<T>( return await Navigator.of(context).push<T>(
CommonDesktopRoute( CommonDesktopRoute(
builder: (context) => child, builder: (context) => child,
@@ -68,7 +70,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
Duration get transitionDuration => const Duration(milliseconds: 500); Duration get transitionDuration => const Duration(milliseconds: 500);
@override @override
Duration get reverseTransitionDuration => const Duration(milliseconds: 300); Duration get reverseTransitionDuration => const Duration(milliseconds: 250);
} }
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
@@ -272,7 +274,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
return; return;
} }
final double shadowWidth = 0.05 * configuration.size!.width; final double shadowWidth = 0.03 * configuration.size!.width;
final double shadowHeight = configuration.size!.height; final double shadowHeight = configuration.size!.height;
final double bandWidth = shadowWidth / (colors.length - 1); final double bandWidth = shadowWidth / (colors.length - 1);

View File

@@ -2,8 +2,15 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension NumExt on num { extension NumExt on num {
String fixed({digit = 2}) { String fixed({decimals = 2}) {
return toStringAsFixed(truncateToDouble() == this ? 0 : digit); String formatted = toStringAsFixed(decimals);
if (formatted.contains('.')) {
formatted = formatted.replaceAll(RegExp(r'0*$'), '');
if (formatted.endsWith('.')) {
formatted = formatted.substring(0, formatted.length - 1);
}
}
return formatted;
} }
} }

View File

@@ -1,15 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:lpinyin/lpinyin.dart'; import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart';
class Other { class Other {
Color? getDelayColor(int? delay) { Color? getDelayColor(int? delay) {
@@ -34,6 +30,26 @@ class Other {
); );
} }
String generateRandomString({int minLength = 10, int maxLength = 100}) {
const latinChars =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
final random = Random();
int length = minLength + random.nextInt(maxLength - minLength + 1);
String result = '';
for (int i = 0; i < length; i++) {
if (random.nextBool()) {
result +=
String.fromCharCode(0x4E00 + random.nextInt(0x9FA5 - 0x4E00 + 1));
} else {
result += latinChars[random.nextInt(latinChars.length)];
}
}
return result;
}
String get uuidV4 { String get uuidV4 {
final Random random = Random(); final Random random = Random();
final bytes = List.generate(16, (_) => random.nextInt(256)); final bytes = List.generate(16, (_) => random.nextInt(256));
@@ -165,30 +181,6 @@ class Other {
: ""; : "";
} }
Future<String?> parseQRCode(Uint8List? bytes) {
return Isolate.run<String?>(() {
if (bytes == null) return null;
img.Image? image = img.decodeImage(bytes);
LuminanceSource source = RGBLuminanceSource(
image!.width,
image.height,
image
.convert(numChannels: 4)
.getBytes(order: img.ChannelOrder.abgr)
.buffer
.asInt32List(),
);
final bitmap = BinaryBitmap(GlobalHistogramBinarizer(source));
final reader = QRCodeReader();
try {
final result = reader.decode(bitmap);
return result.text;
} catch (_) {
return null;
}
});
}
String? getFileNameForDisposition(String? disposition) { String? getFileNameForDisposition(String? disposition) {
if (disposition == null) return null; if (disposition == null) return null;
final parseValue = HeaderValue.parse(disposition); final parseValue = HeaderValue.parse(disposition);

View File

@@ -48,35 +48,40 @@ class AppPath {
return join(executableDirPath, "$appHelperService$executableExtension"); return join(executableDirPath, "$appHelperService$executableExtension");
} }
Future<String> getDownloadDirPath() async { Future<String> get downloadDirPath async {
final directory = await downloadDir.future; final directory = await downloadDir.future;
return directory.path; return directory.path;
} }
Future<String> getHomeDirPath() async { Future<String> get homeDirPath async {
final directory = await dataDir.future; final directory = await dataDir.future;
return directory.path; return directory.path;
} }
Future<String> getLockFilePath() async { Future<String> get lockFilePath async {
final directory = await dataDir.future; final directory = await dataDir.future;
return join(directory.path, "FlClash.lock"); return join(directory.path, "FlClash.lock");
} }
Future<String> getProfilesPath() async { Future<String> get sharedPreferencesPath async {
final directory = await dataDir.future;
return join(directory.path, "shared_preferences.json");
}
Future<String> get profilesPath async {
final directory = await dataDir.future; final directory = await dataDir.future;
return join(directory.path, profilesDirectoryName); return join(directory.path, profilesDirectoryName);
} }
Future<String?> getProfilePath(String? id) async { Future<String?> getProfilePath(String? id) async {
if (id == null) return null; if (id == null) return null;
final directory = await getProfilesPath(); final directory = await profilesPath;
return join(directory, "$id.yaml"); return join(directory, "$id.yaml");
} }
Future<String?> getProvidersPath(String? id) async { Future<String?> getProvidersPath(String? id) async {
if (id == null) return null; if (id == null) return null;
final directory = await getProfilesPath(); final directory = await profilesPath;
return join(directory, "providers", id); return join(directory, "providers", id);
} }

View File

@@ -4,13 +4,14 @@ import 'dart:typed_data';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class Picker { class Picker {
Future<PlatformFile?> pickerFile() async { Future<PlatformFile?> pickerFile() async {
final filePickerResult = await FilePicker.platform.pickFiles( final filePickerResult = await FilePicker.platform.pickFiles(
withData: true, withData: true,
allowMultiple: false, allowMultiple: false,
initialDirectory: await appPath.getDownloadDirPath(), initialDirectory: await appPath.downloadDirPath,
); );
return filePickerResult?.files.first; return filePickerResult?.files.first;
} }
@@ -18,7 +19,7 @@ class Picker {
Future<String?> saveFile(String fileName, Uint8List bytes) async { Future<String?> saveFile(String fileName, Uint8List bytes) async {
final path = await FilePicker.platform.saveFile( final path = await FilePicker.platform.saveFile(
fileName: fileName, fileName: fileName,
initialDirectory: await appPath.getDownloadDirPath(), initialDirectory: await appPath.downloadDirPath,
bytes: Platform.isAndroid ? bytes : null, bytes: Platform.isAndroid ? bytes : null,
); );
if (!Platform.isAndroid && path != null) { if (!Platform.isAndroid && path != null) {
@@ -30,9 +31,14 @@ class Picker {
Future<String?> pickerConfigQRCode() async { Future<String?> pickerConfigQRCode() async {
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery); final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
final bytes = await xFile?.readAsBytes(); if (xFile == null) {
if (bytes == null) return null; return null;
final result = await other.parseQRCode(bytes); }
final controller = MobileScannerController();
final capture = await controller.analyzeImage(xFile.path, formats: [
BarcodeFormat.qrCode,
]);
final result = capture?.barcodes.first.rawValue;
if (result == null || !result.isUrl) { if (result == null || !result.isUrl) {
throw appLocalizations.pleaseUploadValidQrcode; throw appLocalizations.pleaseUploadValidQrcode;
} }

View File

@@ -1,19 +1,22 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/cupertino.dart'; import 'package:fl_clash/models/models.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../models/models.dart';
import 'constant.dart'; import 'constant.dart';
class Preferences { class Preferences {
static Preferences? _instance; static Preferences? _instance;
Completer<SharedPreferences> sharedPreferencesCompleter = Completer(); Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
Future<bool> get isInit async =>
await sharedPreferencesCompleter.future != null;
Preferences._internal() { Preferences._internal() {
SharedPreferences.getInstance() SharedPreferences.getInstance()
.then((value) => sharedPreferencesCompleter.complete(value)); .then((value) => sharedPreferencesCompleter.complete(value))
.onError((_, __) => sharedPreferencesCompleter.complete(null));
} }
factory Preferences() { factory Preferences() {
@@ -23,50 +26,38 @@ class Preferences {
Future<ClashConfig?> getClashConfig() async { Future<ClashConfig?> getClashConfig() async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
final clashConfigString = preferences.getString(clashConfigKey); final clashConfigString = preferences?.getString(clashConfigKey);
if (clashConfigString == null) return null; if (clashConfigString == null) return null;
final clashConfigMap = json.decode(clashConfigString); final clashConfigMap = json.decode(clashConfigString);
try { return ClashConfig.fromJson(clashConfigMap);
return ClashConfig.fromJson(clashConfigMap);
} catch (e) {
debugPrint(e.toString());
return null;
}
}
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
final preferences = await sharedPreferencesCompleter.future;
return preferences.setString(
clashConfigKey,
json.encode(clashConfig),
);
} }
Future<Config?> getConfig() async { Future<Config?> getConfig() async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
final configString = preferences.getString(configKey); final configString = preferences?.getString(configKey);
if (configString == null) return null; if (configString == null) return null;
final configMap = json.decode(configString); final configMap = json.decode(configString);
try { return Config.compatibleFromJson(configMap);
return Config.fromJson(configMap);
} catch (e) {
debugPrint(e.toString());
return null;
}
} }
Future<bool> saveConfig(Config config) async { Future<bool> saveConfig(Config config) async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
return preferences.setString( return await preferences?.setString(
configKey, configKey,
json.encode(config), json.encode(config),
); ) ??
false;
}
clearClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.remove(clashConfigKey);
} }
clearPreferences() async { clearPreferences() async {
final sharedPreferencesIns = await sharedPreferencesCompleter.future; final sharedPreferencesIns = await sharedPreferencesCompleter.future;
sharedPreferencesIns.clear(); sharedPreferencesIns?.clear();
} }
} }
final preferences = Preferences(); final preferences = Preferences();

31
lib/common/print.dart Normal file
View File

@@ -0,0 +1,31 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart';
class CommonPrint {
static CommonPrint? _instance;
CommonPrint._internal();
factory CommonPrint() {
_instance ??= CommonPrint._internal();
return _instance!;
}
log(String? text) {
final payload = "[FlClash] $text";
debugPrint(payload);
if (globalState.isService) {
return;
}
globalState.appController.addLog(
Log(
logLevel: LogLevel.info,
payload: payload,
),
);
}
}
final commonPrint = CommonPrint();

View File

@@ -1,5 +1,5 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:flutter/cupertino.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
class Render { class Render {
@@ -23,14 +23,14 @@ class Render {
pause() { pause() {
debouncer.call( debouncer.call(
"render_pause", DebounceTag.renderPause,
_pause, _pause,
duration: Duration(seconds: 5), duration: Duration(seconds: 15),
); );
} }
resume() { resume() {
debouncer.cancel("render_pause"); debouncer.cancel(DebounceTag.renderPause);
_resume(); _resume();
} }
@@ -41,7 +41,7 @@ class Render {
_drawFrame = _dispatcher.onDrawFrame; _drawFrame = _dispatcher.onDrawFrame;
_dispatcher.onBeginFrame = null; _dispatcher.onBeginFrame = null;
_dispatcher.onDrawFrame = null; _dispatcher.onDrawFrame = null;
debugPrint("[App] pause"); commonPrint.log("pause");
} }
void _resume() { void _resume() {
@@ -49,7 +49,8 @@ class Render {
_isPaused = false; _isPaused = false;
_dispatcher.onBeginFrame = _beginFrame; _dispatcher.onBeginFrame = _beginFrame;
_dispatcher.onDrawFrame = _drawFrame; _dispatcher.onDrawFrame = _drawFrame;
debugPrint("[App] resume"); _dispatcher.scheduleFrame();
commonPrint.log("resume");
} }
} }

View File

@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -14,11 +13,10 @@ class Request {
String? userAgent; String? userAgent;
Request() { Request() {
_dio = Dio(); _dio = Dio(
_dio.interceptors.add( BaseOptions(
InterceptorsWrapper( headers: {
onRequest: (options, handler) { "User-Agent": browserUa,
return handler.next(options); // 继续请求
}, },
), ),
); );
@@ -30,7 +28,7 @@ class Request {
url, url,
options: Options( options: Options(
headers: { headers: {
"User-Agent": globalState.appController.clashConfig.globalUa "User-Agent": globalState.ua,
}, },
responseType: ResponseType.bytes, responseType: ResponseType.bytes,
), ),
@@ -71,31 +69,32 @@ class Request {
return data; return data;
} }
final List<String> _ipInfoSources = [ final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
"https://ipwho.is/?fields=ip&output=csv", "https://ipwho.is/": IpInfo.fromIpwhoIsJson,
"https://ipinfo.io/ip", "https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
"https://ifconfig.me/ip/", "https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
]; "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async { Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources) { for (final source in _ipInfoSources.entries) {
try { try {
final response = await _dio final response = await _dio.get<Map<String, dynamic>>(
.get<String>( source.key,
source, cancelToken: cancelToken,
cancelToken: cancelToken, options: Options(
) responseType: ResponseType.json,
.timeout(httpTimeoutDuration); ),
);
if (response.statusCode != 200 || response.data == null) { if (response.statusCode != 200 || response.data == null) {
continue; continue;
} }
final ipInfo = await clashCore.getCountryCode(response.data!); if (response.data == null) {
if (ipInfo == null && source != _ipInfoSources.last) {
continue; continue;
} }
return ipInfo; return source.value(response.data!);
} catch (e) { } catch (e) {
debugPrint("checkIp error ===> $e"); commonPrint.log("checkIp error ===> $e");
if (e is DioException && e.type == DioExceptionType.cancel) { if (e is DioException && e.type == DioExceptionType.cancel) {
throw "cancelled"; throw "cancelled";
} }
@@ -110,6 +109,9 @@ class Request {
.get( .get(
"http://$localhost:$helperPort/ping", "http://$localhost:$helperPort/ping",
options: Options( options: Options(
headers: {
"User-Agent": browserUa,
},
responseType: ResponseType.plain, responseType: ResponseType.plain,
), ),
) )

View File

@@ -1,3 +1,4 @@
import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
@@ -15,6 +16,8 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
}; };
} }
class BaseScrollBehavior2 extends ScrollBehavior {}
class HiddenBarScrollBehavior extends BaseScrollBehavior { class HiddenBarScrollBehavior extends BaseScrollBehavior {
@override @override
Widget buildScrollbar( Widget buildScrollbar(
@@ -40,3 +43,95 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
); );
} }
} }
class NextClampingScrollPhysics extends ClampingScrollPhysics {
const NextClampingScrollPhysics({super.parent});
@override
NextClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return NextClampingScrollPhysics(parent: buildParent(ancestor));
}
@override
Simulation? createBallisticSimulation(
ScrollMetrics position, double velocity) {
final Tolerance tolerance = toleranceFor(position);
if (position.outOfRange) {
double? end;
if (position.pixels > position.maxScrollExtent) {
end = position.maxScrollExtent;
}
if (position.pixels < position.minScrollExtent) {
end = position.minScrollExtent;
}
assert(end != null);
return ScrollSpringSimulation(
spring,
end!,
end,
min(0.0, velocity),
tolerance: tolerance,
);
}
if (velocity.abs() < tolerance.velocity) {
return null;
}
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) {
return null;
}
if (velocity < 0.0 && position.pixels <= position.minScrollExtent) {
return null;
}
return ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
tolerance: tolerance,
);
}
}
class ReverseScrollController extends ScrollController {
ReverseScrollController({
super.initialScrollOffset,
super.keepScrollOffset,
super.debugLabel,
});
@override
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition? oldPosition,
) {
return ReverseScrollPosition(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
}
class ReverseScrollPosition extends ScrollPositionWithSingleContext {
ReverseScrollPosition({
required super.physics,
required super.context,
super.initialPixels = 0.0,
super.keepScrollOffset,
super.oldPosition,
super.debugLabel,
});
bool _isInit = false;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (!_isInit) {
correctPixels(maxScrollExtent);
_isInit = true;
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
}

0
lib/common/state.dart Normal file
View File

View File

@@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'print.dart';
extension StringExtension on String { extension StringExtension on String {
bool get isUrl { bool get isUrl {
@@ -43,8 +43,17 @@ extension StringExtension on String {
RegExp(this); RegExp(this);
return true; return true;
} catch (e) { } catch (e) {
debugPrint(e.toString()); commonPrint.log(e.toString());
return false; return false;
} }
} }
} }
extension StringExtensionSafe on String? {
String getSafeValue(String defaultValue) {
if (this == null || this!.isEmpty) {
return defaultValue;
}
return this!;
}
}

View File

@@ -46,7 +46,7 @@ class System {
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]); final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
final output = result.stdout.trim(); final output = result.stdout.trim();
if (output.startsWith('root:') && output.contains('rwx')) { if (output.startsWith('root:') && output.contains('rws')) {
return true; return true;
} }
return false; return false;

View File

@@ -39,10 +39,7 @@ class Tray {
} }
update({ update({
required AppState appState, required TrayState trayState,
required AppFlowingState appFlowingState,
required Config config,
required ClashConfig clashConfig,
bool focus = false, bool focus = false,
}) async { }) async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
@@ -50,7 +47,7 @@ class Tray {
} }
if (!Platform.isLinux) { if (!Platform.isLinux) {
await _updateSystemTray( await _updateSystemTray(
brightness: appState.brightness, brightness: trayState.brightness,
force: focus, force: focus,
); );
} }
@@ -63,9 +60,7 @@ class Tray {
); );
menuItems.add(showMenuItem); menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox( final startMenuItem = MenuItem.checkbox(
label: appFlowingState.isStart label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
? appLocalizations.stop
: appLocalizations.start,
onClick: (_) async { onClick: (_) async {
globalState.appController.updateStart(); globalState.appController.updateStart();
}, },
@@ -80,23 +75,22 @@ class Tray {
onClick: (_) { onClick: (_) {
globalState.appController.changeMode(mode); globalState.appController.changeMode(mode);
}, },
checked: mode == clashConfig.mode, checked: mode == trayState.mode,
), ),
); );
} }
menuItems.add(MenuItem.separator()); menuItems.add(MenuItem.separator());
if (!Platform.isWindows) { if (!Platform.isWindows) {
final groups = appState.currentGroups; for (final group in trayState.groups) {
for (final group in groups) {
List<MenuItem> subMenuItems = []; List<MenuItem> subMenuItems = [];
for (final proxy in group.all) { for (final proxy in group.all) {
subMenuItems.add( subMenuItems.add(
MenuItem.checkbox( MenuItem.checkbox(
label: proxy.name, label: proxy.name,
checked: appState.selectedMap[group.name] == proxy.name, checked: trayState.selectedMap[group.name] == proxy.name,
onClick: (_) { onClick: (_) {
final appController = globalState.appController; final appController = globalState.appController;
appController.config.updateCurrentSelectedMap( appController.updateCurrentSelectedMap(
group.name, group.name,
proxy.name, proxy.name,
); );
@@ -117,18 +111,18 @@ class Tray {
), ),
); );
} }
if (groups.isNotEmpty) { if (trayState.groups.isNotEmpty) {
menuItems.add(MenuItem.separator()); menuItems.add(MenuItem.separator());
} }
} }
if (appFlowingState.isStart) { if (trayState.isStart) {
menuItems.add( menuItems.add(
MenuItem.checkbox( MenuItem.checkbox(
label: appLocalizations.tun, label: appLocalizations.tun,
onClick: (_) { onClick: (_) {
globalState.appController.updateTun(); globalState.appController.updateTun();
}, },
checked: clashConfig.tun.enable, checked: trayState.tunEnable,
), ),
); );
menuItems.add( menuItems.add(
@@ -137,7 +131,7 @@ class Tray {
onClick: (_) { onClick: (_) {
globalState.appController.updateSystemProxy(); globalState.appController.updateSystemProxy();
}, },
checked: config.networkProps.systemProxy, checked: trayState.systemProxy,
), ),
); );
menuItems.add(MenuItem.separator()); menuItems.add(MenuItem.separator());
@@ -147,12 +141,12 @@ class Tray {
onClick: (_) async { onClick: (_) async {
globalState.appController.updateAutoLaunch(); globalState.appController.updateAutoLaunch();
}, },
checked: config.appSetting.autoLaunch, checked: trayState.autoLaunch,
); );
final copyEnvVarMenuItem = MenuItem( final copyEnvVarMenuItem = MenuItem(
label: appLocalizations.copyEnvVar, label: appLocalizations.copyEnvVar,
onClick: (_) async { onClick: (_) async {
await _copyEnv(clashConfig.mixedPort); await _copyEnv(trayState.port);
}, },
); );
menuItems.add(autoStartMenuItem); menuItems.add(autoStartMenuItem);
@@ -169,12 +163,25 @@ class Tray {
await trayManager.setContextMenu(menu); await trayManager.setContextMenu(menu);
if (Platform.isLinux) { if (Platform.isLinux) {
await _updateSystemTray( await _updateSystemTray(
brightness: appState.brightness, brightness: trayState.brightness,
force: focus, force: focus,
); );
} }
} }
updateTrayTitle([Traffic? traffic]) async {
// if (!Platform.isMacOS) {
// return;
// }
// if (traffic == null) {
// await trayManager.setTitle("");
// } else {
// await trayManager.setTitle(
// "${traffic.up.shortShow} ↑ \n${traffic.down.shortShow} ↓",
// );
// }
}
Future<void> _copyEnv(int port) async { Future<void> _copyEnv(int port) async {
final url = "http://127.0.0.1:$port"; final url = "http://127.0.0.1:$port";

View File

@@ -1,13 +1,14 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:screen_retriever/screen_retriever.dart'; import 'package:screen_retriever/screen_retriever.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class Window { class Window {
init(WindowProps props, int version) async { init(int version) async {
final props = globalState.config.windowProps;
final acquire = await singleInstanceLock.acquire(); final acquire = await singleInstanceLock.acquire();
if (!acquire) { if (!acquire) {
exit(0); exit(0);
@@ -24,6 +25,8 @@ class Window {
); );
if (!Platform.isMacOS || version > 10) { if (!Platform.isMacOS || version > 10) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden); await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
if(!Platform.isMacOS){
final left = props.left ?? 0; final left = props.left ?? 0;
final top = props.top ?? 0; final top = props.top ?? 0;
final right = left + props.width; final right = left + props.width;
@@ -33,7 +36,7 @@ class Window {
} else { } else {
final displays = await screenRetriever.getAllDisplays(); final displays = await screenRetriever.getAllDisplays();
final isPositionValid = displays.any( final isPositionValid = displays.any(
(display) { (display) {
final displayBounds = Rect.fromLTWH( final displayBounds = Rect.fromLTWH(
display.visiblePosition!.dx, display.visiblePosition!.dx,
display.visiblePosition!.dy, display.visiblePosition!.dy,
@@ -60,10 +63,10 @@ class Window {
} }
show() async { show() async {
render?.resume();
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
await windowManager.setSkipTaskbar(false); await windowManager.setSkipTaskbar(false);
render?.resume();
} }
Future<bool> isVisible() async { Future<bool> isVisible() async {
@@ -75,9 +78,9 @@ class Window {
} }
hide() async { hide() async {
render?.pause();
await windowManager.hide(); await windowManager.hide();
await windowManager.setSkipTaskbar(true); await windowManager.setSkipTaskbar(true);
render?.pause();
} }
} }

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/cupertino.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
class Windows { class Windows {
@@ -54,7 +53,7 @@ class Windows {
calloc.free(argumentsPtr); calloc.free(argumentsPtr);
calloc.free(operationPtr); calloc.free(operationPtr);
debugPrint("[Windows] runas: $command $arguments resultCode:$result"); commonPrint.log("windows runas: $command $arguments resultCode:$result");
if (result < 42) { if (result < 42) {
return false; return false;

View File

@@ -8,28 +8,24 @@ import 'package:archive/archive.dart';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/archive.dart'; import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'common/common.dart'; import 'common/common.dart';
import 'models/models.dart'; import 'models/models.dart';
class AppController { class AppController {
final BuildContext context; bool lastTunEnable = false;
late AppState appState; int? lastProfileModified;
late AppFlowingState appFlowingState;
late Config config;
late ClashConfig clashConfig;
AppController(this.context) { final BuildContext context;
appState = context.read<AppState>(); final WidgetRef _ref;
config = context.read<Config>();
clashConfig = context.read<ClashConfig>(); AppController(this.context, WidgetRef ref) : _ref = ref;
appFlowingState = context.read<AppFlowingState>();
}
updateClashConfigDebounce() { updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig); debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
@@ -41,13 +37,13 @@ class AppController {
addCheckIpNumDebounce() { addCheckIpNumDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () { debouncer.call(DebounceTag.addCheckIpNum, () {
appState.checkIpNum++; _ref.read(checkIpNumProvider.notifier).add();
}); });
} }
applyProfileDebounce() { applyProfileDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () { debouncer.call(DebounceTag.addCheckIpNum, () {
applyProfile(isPrue: true); applyProfile();
}); });
} }
@@ -67,11 +63,12 @@ class AppController {
} }
restartCore() async { restartCore() async {
await globalState.restartCore( await clashService?.reStart();
appState: appState, await initCore();
clashConfig: clashConfig,
config: config, if (_ref.read(runTimeProvider.notifier).isStart) {
); await globalState.handleStart();
}
} }
updateStatus(bool isStart) async { updateStatus(bool isStart) async {
@@ -81,13 +78,12 @@ class AppController {
updateTraffic, updateTraffic,
]); ]);
final currentLastModified = final currentLastModified =
await config.getCurrentProfile()?.profileLastModified; await _ref.read(currentProfileProvider)?.profileLastModified;
if (currentLastModified == null || if (currentLastModified == null || lastProfileModified == null) {
globalState.lastProfileModified == null) {
addCheckIpNumDebounce(); addCheckIpNumDebounce();
return; return;
} }
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) { if (currentLastModified <= (lastProfileModified ?? 0)) {
addCheckIpNumDebounce(); addCheckIpNumDebounce();
return; return;
} }
@@ -95,9 +91,10 @@ class AppController {
} else { } else {
await globalState.handleStop(); await globalState.handleStop();
await clashCore.resetTraffic(); await clashCore.resetTraffic();
appFlowingState.traffics = []; _ref.read(trafficsProvider.notifier).clear();
appFlowingState.totalTraffic = Traffic(); _ref.read(totalTrafficProvider.notifier).value = Traffic();
appFlowingState.runTime = null; _ref.read(runTimeProvider.notifier).value = null;
// tray.updateTrayTitle(null);
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
} }
@@ -107,107 +104,214 @@ class AppController {
if (startTime != null) { if (startTime != null) {
final startTimeStamp = startTime.millisecondsSinceEpoch; final startTimeStamp = startTime.millisecondsSinceEpoch;
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch; final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
appFlowingState.runTime = nowTimeStamp - startTimeStamp; _ref.read(runTimeProvider.notifier).value = nowTimeStamp - startTimeStamp;
} else { } else {
appFlowingState.runTime = null; _ref.read(runTimeProvider.notifier).value = null;
} }
} }
updateTraffic() { updateTraffic() async {
globalState.updateTraffic( final traffic = await clashCore.getTraffic();
config: config, _ref.read(trafficsProvider.notifier).addTraffic(traffic);
appFlowingState: appFlowingState, _ref.read(totalTrafficProvider.notifier).value =
); await clashCore.getTotalTraffic();
} }
addProfile(Profile profile) async { addProfile(Profile profile) async {
config.setProfile(profile); _ref.read(profilesProvider.notifier).setProfile(profile);
if (config.currentProfileId != null) return; if (_ref.read(currentProfileIdProvider) != null) return;
await changeProfile(profile.id); _ref.read(currentProfileIdProvider.notifier).value = profile.id;
} }
deleteProfile(String id) async { deleteProfile(String id) async {
config.deleteProfileById(id); _ref.read(profilesProvider.notifier).deleteProfileById(id);
clearEffect(id); clearEffect(id);
if (config.currentProfileId == id) { if (globalState.config.currentProfileId == id) {
if (config.profiles.isNotEmpty) { final profiles = globalState.config.profiles;
final updateId = config.profiles.first.id; final currentProfileId = _ref.read(currentProfileIdProvider.notifier);
changeProfile(updateId); if (profiles.isNotEmpty) {
final updateId = profiles.first.id;
currentProfileId.value = updateId;
} else { } else {
changeProfile(null); currentProfileId.value = null;
updateStatus(false); updateStatus(false);
} }
} }
} }
updateProviders() async { updateProviders() async {
await globalState.updateProviders(appState); _ref.read(providersProvider.notifier).value =
await clashCore.getExternalProviders();
} }
updateLocalIp() async { updateLocalIp() async {
appFlowingState.localIp = null; _ref.read(localIpProvider.notifier).value = null;
await Future.delayed(commonDuration); await Future.delayed(commonDuration);
appFlowingState.localIp = await other.getLocalIpAddress(); _ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress();
} }
Future<void> updateProfile(Profile profile) async { Future<void> updateProfile(Profile profile) async {
final newProfile = await profile.update(); final newProfile = await profile.update();
config.setProfile( _ref
newProfile.copyWith(isUpdating: false), .read(profilesProvider.notifier)
); .setProfile(newProfile.copyWith(isUpdating: false));
if (profile.id == config.currentProfile?.id) { if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce(); applyProfileDebounce();
} }
} }
_setProfile(Profile profile) {
_ref.read(profilesProvider.notifier).setProfile(profile);
}
setProfile(Profile profile) { setProfile(Profile profile) {
config.setProfile(profile); _setProfile(profile);
if (profile.id == config.currentProfile?.id) { if (profile.id == _ref.read(currentProfileIdProvider)) {
applyProfileDebounce(); applyProfileDebounce();
} }
} }
Future<void> updateClashConfig({bool isPatch = true}) async { setProfiles(List<Profile> profiles) {
_ref.read(profilesProvider.notifier).value = profiles;
}
addLog(Log log) {
_ref.read(logsProvider).add(log);
}
updateOrAddHotKeyAction(HotKeyAction hotKeyAction) {
final hotKeyActions = _ref.read(hotKeyActionsProvider);
final index =
hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action);
if (index == -1) {
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
..add(hotKeyAction);
} else {
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
..[index] = hotKeyAction;
}
_ref.read(hotKeyActionsProvider.notifier).value = index == -1
? (List.from(hotKeyActions)..add(hotKeyAction))
: (List.from(hotKeyActions)..[index] = hotKeyAction);
}
List<Group> getCurrentGroups() {
return _ref.read(currentGroupsStateProvider.select((state) => state.value));
}
String getRealTestUrl(String? url) {
return _ref.read(getRealTestUrlProvider(url));
}
int getProxiesColumns() {
return _ref.read(getProxiesColumnsProvider);
}
addSortNum() {
return _ref.read(sortNumProvider.notifier).add();
}
getCurrentGroupName() {
final currentGroupName = _ref.read(currentProfileProvider.select(
(state) => state?.currentGroupName,
));
return currentGroupName;
}
getRealProxyName(proxyName) {
return _ref.read(getRealTestUrlProvider(proxyName));
}
getSelectedProxyName(groupName) {
return _ref.read(getSelectedProxyNameProvider(groupName));
}
updateCurrentGroupName(String groupName) {
final profile = _ref.read(currentProfileProvider);
if (profile == null || profile.currentGroupName == groupName) {
return;
}
_setProfile(
profile.copyWith(currentGroupName: groupName),
);
}
Future<void> updateClashConfig([bool? isPatch]) async {
final commonScaffoldState = globalState.homeScaffoldKey.currentState; final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return; if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async { await commonScaffoldState?.loadingRun(() async {
await globalState.updateClashConfig( await _updateClashConfig(
appState: appState, isPatch,
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
); );
}); });
} }
Future applyProfile({bool isPrue = false}) async { Future<void> _updateClashConfig([bool? isPatch]) async {
if (isPrue) { final profile = _ref.watch(currentProfileProvider);
await globalState.applyProfile( await _ref.read(currentProfileProvider)?.checkAndUpdate();
appState: appState, final patchConfig = _ref.read(patchClashConfigProvider);
config: config, final appSetting = _ref.read(appSettingProvider);
clashConfig: clashConfig, bool enableTun = patchConfig.tun.enable;
); if (enableTun != lastTunEnable &&
lastTunEnable == false &&
!Platform.isAndroid) {
final code = await system.authorizeCore();
switch (code) {
case AuthorizeCode.none:
break;
case AuthorizeCode.success:
lastTunEnable = enableTun;
await restartCore();
return;
case AuthorizeCode.error:
enableTun = false;
}
}
if (appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
}
final res = await clashCore.updateConfig(
globalState.getUpdateConfigParams(isPatch),
);
if (res.isNotEmpty) throw res;
lastTunEnable = enableTun;
lastProfileModified = await profile?.profileLastModified;
}
Future _applyProfile() async {
await clashCore.requestGc();
await updateClashConfig();
await updateGroups();
await updateProviders();
}
Future applyProfile({bool silence = false}) async {
if (silence) {
await _applyProfile();
} else { } else {
final commonScaffoldState = globalState.homeScaffoldKey.currentState; final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return; if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async { await commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile( await _applyProfile();
appState: appState,
config: config,
clashConfig: clashConfig,
);
}); });
} }
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
changeProfile(String? value) async { handleChangeProfile() {
if (value == config.currentProfileId) return; _ref.read(delayDataSourceProvider.notifier).value = {};
config.currentProfileId = value; applyProfile();
}
updateBrightness(Brightness brightness) {
_ref.read(appBrightnessProvider.notifier).value = brightness;
} }
autoUpdateProfiles() async { autoUpdateProfiles() async {
for (final profile in config.profiles) { for (final profile in _ref.read(profilesProvider)) {
if (!profile.autoUpdate) continue; if (!profile.autoUpdate) continue;
final isNotNeedUpdate = profile.lastUpdateDate final isNotNeedUpdate = profile.lastUpdateDate
?.add( ?.add(
@@ -218,20 +322,29 @@ class AppController {
continue; continue;
} }
try { try {
updateProfile(profile); await updateProfile(profile);
} catch (e) { } catch (e) {
appFlowingState.addLog( _ref.read(logsProvider.notifier).addLog(
Log( Log(
logLevel: LogLevel.info, logLevel: LogLevel.info,
payload: e.toString(), payload: e.toString(),
), ),
); );
} }
} }
} }
Future<void> updateGroups() async {
_ref.read(groupsProvider.notifier).value = await retry(
task: () async {
return await clashCore.getProxiesGroups();
},
retryIf: (res) => res.isEmpty,
);
}
updateProfiles() async { updateProfiles() async {
for (final profile in config.profiles) { for (final profile in _ref.read(profilesProvider)) {
if (profile.type == ProfileType.file) { if (profile.type == ProfileType.file) {
continue; continue;
} }
@@ -239,34 +352,33 @@ class AppController {
} }
} }
Future<void> updateGroups() async { updateSystemColorSchemes(ColorSchemes colorSchemes) {
await globalState.updateGroups(appState); _ref.read(appSchemesProvider.notifier).value = colorSchemes;
}
updateSystemColorSchemes(SystemColorSchemes systemColorSchemes) {
appState.systemColorSchemes = systemColorSchemes;
} }
savePreferences() async { savePreferences() async {
debugPrint("[APP] savePreferences"); commonPrint.log("save preferences");
await preferences.saveConfig(config); await preferences.saveConfig(globalState.config);
await preferences.saveClashConfig(clashConfig);
} }
changeProxy({ changeProxy({
required String groupName, required String groupName,
required String proxyName, required String proxyName,
}) async { }) async {
await globalState.changeProxy( await clashCore.changeProxy(
config: config, ChangeProxyParams(
groupName: groupName, groupName: groupName,
proxyName: proxyName, proxyName: proxyName,
),
); );
if (_ref.read(appSettingProvider).closeConnections) {
clashCore.closeConnections();
}
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
handleBackOrExit() async { handleBackOrExit() async {
if (config.appSetting.minimizeOnExit) { if (_ref.read(appSettingProvider).minimizeOnExit) {
if (system.isDesktop) { if (system.isDesktop) {
await savePreferencesDebounce(); await savePreferencesDebounce();
} }
@@ -289,7 +401,7 @@ class AppController {
} }
autoCheckUpdate() async { autoCheckUpdate() async {
if (!config.appSetting.autoCheckUpdate) return; if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
final res = await request.checkForUpdate(); final res = await request.checkForUpdate();
checkUpdateResultHandle(data: res); checkUpdateResultHandle(data: res);
} }
@@ -338,35 +450,61 @@ class AppController {
} }
} }
init() async { _handlePreference() async {
final isDisclaimerAccepted = await handlerDisclaimer(); if (await preferences.isInit) {
if (!isDisclaimerAccepted) { return;
handleExit();
} }
await globalState.initCore( final res = await globalState.showMessage(
appState: appState, title: appLocalizations.tip,
clashConfig: clashConfig, message: TextSpan(text: appLocalizations.cacheCorrupt),
config: config,
); );
if (res == true) {
final file = File(await appPath.sharedPreferencesPath);
final isExists = await file.exists();
if (isExists) {
await file.delete();
}
}
await handleExit();
}
Future<void> initCore() async {
final isInit = await clashCore.isInit;
if (!isInit) {
await clashCore.setState(
globalState.getCoreState(),
);
await clashCore.init();
}
await applyProfile();
}
init() async {
await _handlePreference();
await _handlerDisclaimer();
await initCore();
await _initStatus(); await _initStatus();
updateTray(true);
autoLaunch?.updateStatus( autoLaunch?.updateStatus(
config.appSetting.autoLaunch, _ref.read(appSettingProvider).autoLaunch,
); );
autoUpdateProfiles(); autoUpdateProfiles();
autoCheckUpdate(); autoCheckUpdate();
if (!config.appSetting.silentLaunch) { if (!_ref.read(appSettingProvider).silentLaunch) {
window?.show(); window?.show();
} else { } else {
window?.hide(); window?.hide();
} }
_ref.read(initProvider.notifier).value = true;
} }
_initStatus() async { _initStatus() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
await globalState.updateStartTime(); await globalState.updateStartTime();
} }
final status = final status = globalState.isStart == true
globalState.isStart == true ? true : config.appSetting.autoRun; ? true
: _ref.read(appSettingProvider).autoRun;
await updateStatus(status); await updateStatus(status);
if (!status) { if (!status) {
@@ -375,18 +513,23 @@ class AppController {
} }
setDelay(Delay delay) { setDelay(Delay delay) {
appState.setDelay(delay); _ref.read(delayDataSourceProvider.notifier).setDelay(delay);
} }
toPage( toPage(
int index, { int index, {
bool hasAnimate = false, bool hasAnimate = false,
}) { }) {
if (index > appState.currentNavigationItems.length - 1) { final navigations = _ref.read(currentNavigationsStateProvider).value;
if (index > navigations.length - 1) {
return; return;
} }
appState.currentLabel = appState.currentNavigationItems[index].label; _ref.read(currentPageLabelProvider.notifier).value =
if ((config.appSetting.isAnimateToPage || hasAnimate)) { navigations[index].label;
final isAnimateToPage = _ref.read(appSettingProvider).isAnimateToPage;
final isMobile =
_ref.read(viewWidthProvider.notifier).viewMode == ViewMode.mobile;
if (isAnimateToPage && isMobile || hasAnimate) {
globalState.pageController?.animateToPage( globalState.pageController?.animateToPage(
index, index,
duration: kTabScrollDuration, duration: kTabScrollDuration,
@@ -398,9 +541,9 @@ class AppController {
} }
toProfiles() { toProfiles() {
final index = appState.currentNavigationItems.indexWhere( final index = _ref.read(currentNavigationsStateProvider).value.indexWhere(
(element) => element.label == "profiles", (element) => element.label == PageLabel.profiles,
); );
if (index != -1) { if (index != -1) {
toPage(index); toPage(index);
} }
@@ -460,9 +603,9 @@ class AppController {
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
config.appSetting = config.appSetting.copyWith( _ref.read(appSettingProvider.notifier).updateState(
disclaimerAccepted: true, (state) => state.copyWith(disclaimerAccepted: true),
); );
Navigator.of(context).pop<bool>(true); Navigator.of(context).pop<bool>(true);
}, },
child: Text(appLocalizations.agree), child: Text(appLocalizations.agree),
@@ -473,11 +616,15 @@ class AppController {
false; false;
} }
Future<bool> handlerDisclaimer() async { _handlerDisclaimer() async {
if (config.appSetting.disclaimerAccepted) { if (_ref.read(appSettingProvider).disclaimerAccepted) {
return true; return;
} }
return showDisclaimer(); final isDisclaimerAccepted = await showDisclaimer();
if (!isDisclaimerAccepted) {
await handleExit();
}
return;
} }
addProfileFormURL(String url) async { addProfileFormURL(String url) async {
@@ -531,20 +678,12 @@ class AppController {
updateViewWidth(double width) { updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width; _ref.read(viewWidthProvider.notifier).value = width;
}); });
} }
int? getDelay(String proxyName, [String? url]) { setProvider(ExternalProvider? provider) {
final currentDelayMap = appState.delayMap[getRealTestUrl(url)]; _ref.read(providersProvider.notifier).setProvider(provider);
return currentDelayMap?[appState.getRealProxyName(proxyName)];
}
String getRealTestUrl(String? url) {
if (url == null || url.isEmpty) {
return config.appSetting.testUrl;
}
return url;
} }
List<Proxy> _sortOfName(List<Proxy> proxies) { List<Proxy> _sortOfName(List<Proxy> proxies) {
@@ -557,12 +696,17 @@ class AppController {
); );
} }
List<Proxy> _sortOfDelay(String url, List<Proxy> proxies) { List<Proxy> _sortOfDelay({
required List<Proxy> proxies,
String? testUrl,
}) {
return List.of(proxies) return List.of(proxies)
..sort( ..sort(
(a, b) { (a, b) {
final aDelay = getDelay(a.name, url); final aDelay =
final bDelay = getDelay(b.name, url); _ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl));
final bDelay =
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl));
if (aDelay == null && bDelay == null) { if (aDelay == null && bDelay == null) {
return 0; return 0;
} }
@@ -578,20 +722,16 @@ class AppController {
} }
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) { List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
return switch (config.proxiesStyle.sortType) { return switch (_ref.read(proxiesStyleSettingProvider).sortType) {
ProxiesSortType.none => proxies, ProxiesSortType.none => proxies,
ProxiesSortType.delay => _sortOfDelay(getRealTestUrl(url), proxies), ProxiesSortType.delay => _sortOfDelay(
proxies: proxies,
testUrl: url,
),
ProxiesSortType.name => _sortOfName(proxies), ProxiesSortType.name => _sortOfName(proxies),
}; };
} }
String getCurrentSelectedName(String groupName) {
final group = appState.getGroupWithName(groupName);
return group?.getCurrentSelectedName(
config.currentSelectedMap[groupName] ?? '') ??
'';
}
clearEffect(String profileId) async { clearEffect(String profileId) async {
final profilePath = await appPath.getProfilePath(profileId); final profilePath = await appPath.getProfilePath(profileId);
final providersPath = await appPath.getProvidersPath(profileId); final providersPath = await appPath.getProvidersPath(profileId);
@@ -605,38 +745,67 @@ class AppController {
}); });
} }
bool get isMobileView {
return appState.viewMode == ViewMode.mobile;
}
updateTun() { updateTun() {
clashConfig.tun = clashConfig.tun.copyWith( _ref.read(patchClashConfigProvider.notifier).updateState(
enable: !clashConfig.tun.enable, (state) => state.copyWith.tun(enable: !state.tun.enable),
); );
} }
updateSystemProxy() { updateSystemProxy() {
config.networkProps = config.networkProps.copyWith( _ref.read(networkSettingProvider.notifier).updateState(
systemProxy: !config.networkProps.systemProxy, (state) => state.copyWith(
); systemProxy: state.systemProxy,
),
);
} }
updateStart() { updateStart() {
updateStatus(!appFlowingState.isStart); updateStatus(_ref.read(runTimeProvider.notifier).isStart);
}
updateCurrentSelectedMap(String groupName, String proxyName) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile != null &&
currentProfile.selectedMap[groupName] != proxyName) {
final SelectedMap selectedMap = Map.from(
currentProfile.selectedMap,
)..[groupName] = proxyName;
_ref.read(profilesProvider.notifier).setProfile(
currentProfile.copyWith(
selectedMap: selectedMap,
),
);
}
}
updateCurrentUnfoldSet(Set<String> value) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile == null) {
return;
}
_ref.read(profilesProvider.notifier).setProfile(
currentProfile.copyWith(
unfoldSet: value,
),
);
} }
changeMode(Mode mode) { changeMode(Mode mode) {
clashConfig.mode = mode; _ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(mode: mode),
);
if (mode == Mode.global) { if (mode == Mode.global) {
config.updateCurrentGroupName(GroupName.GLOBAL.name); updateCurrentGroupName(GroupName.GLOBAL.name);
} }
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
updateAutoLaunch() { updateAutoLaunch() {
config.appSetting = config.appSetting.copyWith( _ref.read(appSettingProvider.notifier).updateState(
autoLaunch: !config.appSetting.autoLaunch, (state) => state.copyWith(
); autoLaunch: !state.autoLaunch,
),
);
} }
updateVisible() async { updateVisible() async {
@@ -649,18 +818,24 @@ class AppController {
} }
updateMode() { updateMode() {
final index = Mode.values.indexWhere((item) => item == clashConfig.mode); _ref.read(patchClashConfigProvider.notifier).updateState(
if (index == -1) { (state) {
return; final index = Mode.values.indexWhere((item) => item == state.mode);
} if (index == -1) {
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1; return null;
clashConfig.mode = Mode.values[nextIndex]; }
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
return state.copyWith(
mode: Mode.values[nextIndex],
);
},
);
} }
Future<bool> exportLogs() async { Future<bool> exportLogs() async {
final logsRaw = appFlowingState.logs.map( final logsRaw = _ref.read(logsProvider).list.map(
(item) => item.toString(), (item) => item.toString(),
); );
final data = await Isolate.run<List<int>>(() async { final data = await Isolate.run<List<int>>(() async {
final logsRawString = logsRaw.join("\n"); final logsRawString = logsRaw.join("\n");
return utf8.encode(logsRawString); return utf8.encode(logsRawString);
@@ -673,14 +848,12 @@ class AppController {
} }
Future<List<int>> backupData() async { Future<List<int>> backupData() async {
final homeDirPath = await appPath.getHomeDirPath(); final homeDirPath = await appPath.homeDirPath;
final profilesPath = await appPath.getProfilesPath(); final profilesPath = await appPath.profilesPath;
final configJson = config.toJson(); final configJson = globalState.config.toJson();
final clashConfigJson = clashConfig.toJson();
return Isolate.run<List<int>>(() async { return Isolate.run<List<int>>(() async {
final archive = Archive(); final archive = Archive();
archive.add("config.json", configJson); archive.add("config.json", configJson);
archive.add("clashConfig.json", clashConfigJson);
await archive.addDirectoryToArchive(profilesPath, homeDirPath); await archive.addDirectoryToArchive(profilesPath, homeDirPath);
final zipEncoder = ZipEncoder(); final zipEncoder = ZipEncoder();
return zipEncoder.encode(archive) ?? []; return zipEncoder.encode(archive) ?? [];
@@ -689,11 +862,7 @@ class AppController {
updateTray([bool focus = false]) async { updateTray([bool focus = false]) async {
tray.update( tray.update(
appState: appState, trayState: _ref.read(trayStateProvider),
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
focus: focus,
); );
} }
@@ -705,39 +874,71 @@ class AppController {
final zipDecoder = ZipDecoder(); final zipDecoder = ZipDecoder();
return zipDecoder.decodeBytes(data); return zipDecoder.decodeBytes(data);
}); });
final homeDirPath = await appPath.getHomeDirPath(); final homeDirPath = await appPath.homeDirPath;
final configs = final configs =
archive.files.where((item) => item.name.endsWith(".json")).toList(); archive.files.where((item) => item.name.endsWith(".json")).toList();
final profiles = final profiles =
archive.files.where((item) => !item.name.endsWith(".json")); archive.files.where((item) => !item.name.endsWith(".json"));
final configIndex = final configIndex =
configs.indexWhere((config) => config.name == "config.json"); configs.indexWhere((config) => config.name == "config.json");
final clashConfigIndex = if (configIndex == -1) throw "invalid backup file";
configs.indexWhere((config) => config.name == "clashConfig.json");
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
final configFile = configs[configIndex]; final configFile = configs[configIndex];
final clashConfigFile = configs[clashConfigIndex]; var tempConfig = Config.compatibleFromJson(
final tempConfig = Config.fromJson(
json.decode( json.decode(
utf8.decode(configFile.content), utf8.decode(configFile.content),
), ),
); );
final tempClashConfig = ClashConfig.fromJson(
json.decode(
utf8.decode(clashConfigFile.content),
),
);
for (final profile in profiles) { for (final profile in profiles) {
final filePath = join(homeDirPath, profile.name); final filePath = join(homeDirPath, profile.name);
final file = File(filePath); final file = File(filePath);
await file.create(recursive: true); await file.create(recursive: true);
await file.writeAsBytes(profile.content); await file.writeAsBytes(profile.content);
} }
if (recoveryOption == RecoveryOption.onlyProfiles) { final clashConfigIndex =
config.update(tempConfig, RecoveryOption.onlyProfiles); configs.indexWhere((config) => config.name == "clashConfig.json");
} else { if (clashConfigIndex != -1) {
config.update(tempConfig, RecoveryOption.all); final clashConfigFile = configs[clashConfigIndex];
clashConfig.update(tempClashConfig); tempConfig = tempConfig.copyWith(
patchClashConfig: ClashConfig.fromJson(
json.decode(
utf8.decode(
clashConfigFile.content,
),
),
),
);
} }
_recovery(
tempConfig,
recoveryOption,
);
}
_recovery(Config config, RecoveryOption recoveryOption) {
final profiles = config.profiles;
for (final profile in profiles) {
_ref.read(profilesProvider.notifier).setProfile(profile);
}
final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles;
if (onlyProfiles) {
final currentProfile = _ref.read(currentProfileProvider);
if (currentProfile != null) {
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id;
}
return;
}
_ref.read(patchClashConfigProvider.notifier).value =
config.patchClashConfig;
_ref.read(appSettingProvider.notifier).value = config.appSetting;
_ref.read(currentProfileIdProvider.notifier).value =
config.currentProfileId;
_ref.read(appDAVSettingProvider.notifier).value = config.dav;
_ref.read(themeSettingProvider.notifier).value = config.themeProps;
_ref.read(windowSettingProvider.notifier).value = config.windowProps;
_ref.read(vpnSettingProvider.notifier).value = config.vpnProps;
_ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle;
_ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
_ref.read(networkSettingProvider.notifier).value = config.networkProps;
_ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
} }
} }

View File

@@ -34,7 +34,24 @@ const desktopPlatforms = [
SupportPlatform.Windows, SupportPlatform.Windows,
]; ];
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay } enum GroupType {
Selector,
URLTest,
Fallback,
LoadBalance,
Relay;
static GroupType parseProfileType(String type) {
return switch (type) {
"url-test" => URLTest,
"select" => Selector,
"fallback" => Fallback,
"load-balance" => LoadBalance,
"relay" => Relay,
String() => throw UnimplementedError(),
};
}
}
enum GroupName { GLOBAL, Proxy, Auto, Fallback } enum GroupName { GLOBAL, Proxy, Auto, Fallback }
@@ -45,7 +62,7 @@ extension GroupTypeExtension on GroupType {
) )
.toList(); .toList();
bool get isURLTestOrFallback { bool get isComputedSelected {
return [GroupType.URLTest, GroupType.Fallback].contains(this); return [GroupType.URLTest, GroupType.Fallback].contains(this);
} }
@@ -138,6 +155,13 @@ enum DnsMode {
hosts hosts
} }
enum ExternalControllerStatus {
@JsonValue("")
close,
@JsonValue("127.0.0.1:9090")
open
}
enum KeyboardModifier { enum KeyboardModifier {
alt([ alt([
PhysicalKeyboardKey.altLeft, PhysicalKeyboardKey.altLeft,
@@ -238,6 +262,7 @@ enum ActionMethod {
stopListener, stopListener,
getCountryCode, getCountryCode,
getMemory, getMemory,
getProfile,
///Android, ///Android,
setFdMap, setFdMap,
@@ -270,7 +295,11 @@ enum DebounceTag {
handleWill, handleWill,
updateDelay, updateDelay,
vpnTip, vpnTip,
autoLaunch autoLaunch,
renderPause,
updatePageIndex,
pageChange,
proxiesTabChange,
} }
enum DashboardWidget { enum DashboardWidget {
@@ -341,3 +370,19 @@ enum DashboardWidget {
return dashboardWidgets[index]; return dashboardWidgets[index];
} }
} }
enum GeodataLoader {
standard,
memconservative,
}
enum PageLabel {
dashboard,
proxies,
profiles,
tools,
logs,
requests,
resources,
connections,
}

View File

@@ -4,41 +4,50 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class AccessFragment extends StatefulWidget { class AccessFragment extends ConsumerStatefulWidget {
const AccessFragment({super.key}); const AccessFragment({super.key});
@override @override
State<AccessFragment> createState() => _AccessFragmentState(); ConsumerState<AccessFragment> createState() => _AccessFragmentState();
} }
class _AccessFragmentState extends State<AccessFragment> { class _AccessFragmentState extends ConsumerState<AccessFragment> {
List<String> acceptList = []; List<String> acceptList = [];
List<String> rejectList = []; List<String> rejectList = [];
late ScrollController _controller;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_updateInitList(); _updateInitList();
_controller = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState; final appState = globalState.appState;
if (appState.packages.isEmpty) { if (appState.packages.isEmpty) {
Future.delayed(const Duration(milliseconds: 300), () async { Future.delayed(const Duration(milliseconds: 300), () async {
appState.packages = await app?.getPackages() ?? []; ref.read(packagesProvider.notifier).value =
await app?.getPackages() ?? [];
}); });
} }
}); });
} }
@override
void dispose() {
_controller.dispose();
super.dispose();
}
_updateInitList() { _updateInitList() {
final accessControl = globalState.appController.config.accessControl; acceptList = globalState.config.vpnProps.accessControl.acceptList;
acceptList = accessControl.acceptList; rejectList = globalState.config.vpnProps.accessControl.rejectList;
rejectList = accessControl.rejectList;
} }
Widget _buildSearchButton() { Widget _buildSearchButton() {
@@ -51,9 +60,13 @@ class _AccessFragmentState extends State<AccessFragment> {
acceptList: acceptList, acceptList: acceptList,
rejectList: rejectList, rejectList: rejectList,
), ),
).then((_) => setState(() { ).then(
_updateInitList(); (_) => setState(
})); () {
_updateInitList();
},
),
);
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
); );
@@ -69,28 +82,29 @@ class _AccessFragmentState extends State<AccessFragment> {
return IconButton( return IconButton(
tooltip: tooltip, tooltip: tooltip,
onPressed: () { onPressed: () {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState((state) {
final isAccept = final isAccept =
config.accessControl.mode == AccessControlMode.acceptSelected; state.accessControl.mode == AccessControlMode.acceptSelected;
if (isSelectedAll) { if (isSelectedAll) {
config.accessControl = switch (isAccept) { return switch (isAccept) {
true => config.accessControl.copyWith( true => state.copyWith.accessControl(
acceptList: [], acceptList: [],
), ),
false => config.accessControl.copyWith( false => state.copyWith.accessControl(
rejectList: [], rejectList: [],
), ),
}; };
} else { } else {
config.accessControl = switch (isAccept) { return switch (isAccept) {
true => config.accessControl.copyWith( true => state.copyWith.accessControl(
acceptList: allValueList, acceptList: allValueList,
), ),
false => config.accessControl.copyWith( false => state.copyWith.accessControl(
rejectList: allValueList, rejectList: allValueList,
), ),
}; };
} }
});
}, },
icon: isSelectedAll icon: isSelectedAll
? const Icon(Icons.deselect) ? const Icon(Icons.deselect)
@@ -98,218 +112,239 @@ class _AccessFragmentState extends State<AccessFragment> {
); );
} }
Widget _buildSettingButton() { _intelligentSelected() async {
return IconButton( final appState = globalState.appState;
onPressed: () { final config = globalState.config;
showSheet( final accessControl = config.vpnProps.accessControl;
title: appLocalizations.proxiesSetting, final packageNames = appState.packages
context: context, .where(
body: AccessControlWidget( (item) =>
context: context, accessControl.isFilterSystemApp ? item.isSystem == false : true,
)
.map((item) => item.packageName);
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final selectedPackageNames =
(await commonScaffoldState?.loadingRun<List<String>>(
() async {
return await app?.getChinaPackageNames() ?? [];
},
))
?.toSet() ??
{};
final acceptList = packageNames
.where((item) => !selectedPackageNames.contains(item))
.toList();
final rejectList = packageNames
.where((item) => selectedPackageNames.contains(item))
.toList();
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
acceptList: acceptList,
rejectList: rejectList,
), ),
); );
}
Widget _buildSettingButton() {
return IconButton(
onPressed: () async {
final res = await showSheet<int>(
title: appLocalizations.proxiesSetting,
context: context,
body: AccessControlPanel(),
);
if (res == 1) {
_intelligentSelected();
}
}, },
icon: const Icon(Icons.tune), icon: const Icon(Icons.tune),
); );
} }
_handleSelected(List<String> valueList, Package package, bool? value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
ref.read(vpnSettingProvider.notifier).updateState((state) {
return switch (
state.accessControl.mode == AccessControlMode.acceptSelected) {
true => state.copyWith.accessControl(
acceptList: valueList,
),
false => state.copyWith.accessControl(
rejectList: valueList,
),
};
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<Config, bool>( final state = ref.watch(packageListSelectorStateProvider);
selector: (_, config) => config.isAccessControl, final accessControl = state.accessControl;
builder: (_, isAccessControl, child) { final accessControlMode = accessControl.mode;
return Column( final packages = state.getList(
mainAxisSize: MainAxisSize.max, accessControlMode == AccessControlMode.acceptSelected
children: [ ? acceptList
Flexible( : rejectList,
flex: 0, );
child: ListItem.switchItem( final currentList = accessControl.currentList;
title: Text(appLocalizations.appAccessControl), final packageNameList = packages.map((e) => e.packageName).toList();
delegate: SwitchDelegate( final valueList = currentList.intersection(packageNameList);
value: isAccessControl, final describe = accessControlMode == AccessControlMode.acceptSelected
onChanged: (isAccessControl) { ? appLocalizations.accessControlAllowDesc
final config = context.read<Config>(); : appLocalizations.accessControlNotAllowDesc;
config.isAccessControl = isAccessControl; return Column(
}, mainAxisSize: MainAxisSize.max,
), children: [
), Flexible(
flex: 0,
child: ListItem.switchItem(
title: Text(appLocalizations.appAccessControl),
delegate: SwitchDelegate(
value: accessControl.enable,
onChanged: (enable) {
ref.read(vpnSettingProvider.notifier).updateState(
(state) => state.copyWith.accessControl(
enable: enable,
),
);
},
), ),
const Padding( ),
padding: EdgeInsets.symmetric(horizontal: 16), ),
child: Divider( const Padding(
height: 12, padding: EdgeInsets.symmetric(horizontal: 16),
), child: Divider(
), height: 12,
Flexible( ),
child: child!, ),
), Flexible(
], child: DisabledMask(
); status: !accessControl.enable,
}, child: Column(
child: Selector<AppState, List<Package>>( children: [
selector: (_, appState) => appState.packages, ActivateBox(
builder: (_, packages, ___) { active: accessControl.enable,
return Selector2<AppState, Config, PackageListSelectorState>( child: Padding(
selector: (_, appState, config) => PackageListSelectorState( padding: const EdgeInsets.only(
accessControl: config.accessControl, top: 4,
isAccessControl: config.isAccessControl, bottom: 4,
packages: appState.packages, left: 16,
), right: 8,
builder: (context, state, __) { ),
final accessControl = state.accessControl; child: Row(
final isAccessControl = state.isAccessControl; mainAxisAlignment: MainAxisAlignment.spaceBetween,
final accessControlMode = accessControl.mode; mainAxisSize: MainAxisSize.max,
final packages = state.getList( children: [
accessControlMode == AccessControlMode.acceptSelected Expanded(
? acceptList child: IntrinsicHeight(
: rejectList, child: Column(
); mainAxisSize: MainAxisSize.max,
final currentList = accessControl.currentList; crossAxisAlignment: CrossAxisAlignment.start,
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: [
ActivateBox(
active: 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: [ children: [
Flexible( Expanded(
child: _buildSearchButton(), child: Row(
), children: [
Flexible( Flexible(
child: _buildSelectedAllButton( child: Text(
isSelectedAll: valueList.length == appLocalizations.selected,
packageNameList.length, style: Theme.of(context)
allValueList: packageNameList, .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( Flexible(
child: _buildSettingButton(), 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,
);
}
},
);
},
),
),
],
), ),
); Expanded(
}, flex: 1,
); child: packages.isEmpty
}, ? const Center(
), child: CircularProgressIndicator(),
)
: CommonScrollBar(
controller: _controller,
child: ListView.builder(
controller: _controller,
itemCount: packages.length,
itemExtent: 72,
itemBuilder: (_, index) {
final package = packages[index];
return PackageListItem(
key: Key(package.packageName),
package: package,
value: valueList.contains(package.packageName),
isActive: accessControl.enable,
onChanged: (value) {
_handleSelected(valueList, package, value);
},
);
},
),
),
),
],
),
),
),
],
); );
} }
} }
@@ -330,45 +365,47 @@ class PackageListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActivateBox( return FadeScaleEnterBox(
active: isActive, child: ActivateBox(
child: ListItem.checkbox( active: isActive,
leading: SizedBox( child: ListItem.checkbox(
width: 48, leading: SizedBox(
height: 48, width: 48,
child: FutureBuilder<ImageProvider?>( height: 48,
future: app?.getPackageIcon(package.packageName), child: FutureBuilder<ImageProvider?>(
builder: (_, snapshot) { future: app?.getPackageIcon(package.packageName),
if (!snapshot.hasData && snapshot.data == null) { builder: (_, snapshot) {
return Container(); if (!snapshot.hasData && snapshot.data == null) {
} else { return Container();
return Image( } else {
image: snapshot.data!, return Image(
gaplessPlayback: true, image: snapshot.data!,
width: 48, gaplessPlayback: true,
height: 48, width: 48,
); height: 48,
} );
}, }
},
),
), ),
), title: Text(
title: Text( package.label,
package.label, style: const TextStyle(
style: const TextStyle( overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, ),
maxLines: 1,
), ),
maxLines: 1, subtitle: Text(
), package.packageName,
subtitle: Text( style: const TextStyle(
package.packageName, overflow: TextOverflow.ellipsis,
style: const TextStyle( ),
overflow: TextOverflow.ellipsis, maxLines: 1,
),
delegate: CheckboxDelegate(
value: value,
onChanged: onChanged,
), ),
maxLines: 1,
),
delegate: CheckboxDelegate(
value: value,
onChanged: onChanged,
), ),
), ),
); );
@@ -413,15 +450,31 @@ class AccessControlSearchDelegate extends SearchDelegate {
); );
} }
_handleSelected(
WidgetRef ref, List<String> valueList, Package package, bool? value) {
if (value == true) {
valueList.add(package.packageName);
} else {
valueList.remove(package.packageName);
}
ref.read(vpnSettingProvider.notifier).updateState((state) {
return switch (
state.accessControl.mode == AccessControlMode.acceptSelected) {
true => state.copyWith.accessControl(
acceptList: valueList,
),
false => state.copyWith.accessControl(
rejectList: valueList,
),
};
});
}
Widget _packageList() { Widget _packageList() {
final lowQuery = query.toLowerCase(); final lowQuery = query.toLowerCase();
return Selector2<AppState, Config, PackageListSelectorState>( return Consumer(
selector: (_, appState, config) => PackageListSelectorState( builder: (context, ref, __) {
packages: appState.packages, final state = ref.watch(packageListSelectorStateProvider);
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
),
builder: (context, state, __) {
final accessControl = state.accessControl; final accessControl = state.accessControl;
final accessControlMode = accessControl.mode; final accessControlMode = accessControl.mode;
final packages = state.getList( final packages = state.getList(
@@ -436,7 +489,7 @@ class AccessControlSearchDelegate extends SearchDelegate {
package.packageName.contains(lowQuery), package.packageName.contains(lowQuery),
) )
.toList(); .toList();
final isAccessControl = state.isAccessControl; final isAccessControl = state.accessControl.enable;
final currentList = accessControl.currentList; final currentList = accessControl.currentList;
final packageNameList = packages.map((e) => e.packageName).toList(); final packageNameList = packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList); final valueList = currentList.intersection(packageNameList);
@@ -452,21 +505,12 @@ class AccessControlSearchDelegate extends SearchDelegate {
value: valueList.contains(package.packageName), value: valueList.contains(package.packageName),
isActive: isAccessControl, isActive: isAccessControl,
onChanged: (value) { onChanged: (value) {
if (value == true) { _handleSelected(
valueList.add(package.packageName); ref,
} else { valueList,
valueList.remove(package.packageName); package,
} value,
final config = globalState.appController.config; );
if (accessControlMode == AccessControlMode.acceptSelected) {
config.accessControl = config.accessControl.copyWith(
acceptList: valueList,
);
} else {
config.accessControl = config.accessControl.copyWith(
rejectList: valueList,
);
}
}, },
); );
}, },
@@ -487,14 +531,16 @@ class AccessControlSearchDelegate extends SearchDelegate {
} }
} }
class AccessControlWidget extends StatelessWidget { class AccessControlPanel extends ConsumerStatefulWidget {
final BuildContext context; const AccessControlPanel({
const AccessControlWidget({
super.key, super.key,
required this.context,
}); });
@override
ConsumerState createState() => _AccessControlPanelState();
}
class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
IconData _getIconWithAccessControlMode(AccessControlMode mode) { IconData _getIconWithAccessControlMode(AccessControlMode mode) {
return switch (mode) { return switch (mode) {
AccessControlMode.acceptSelected => Icons.adjust_outlined, AccessControlMode.acceptSelected => Icons.adjust_outlined,
@@ -539,9 +585,11 @@ class AccessControlWidget extends StatelessWidget {
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, AccessControlMode>( child: Consumer(
selector: (_, config) => config.accessControl.mode, builder: (_, ref, __) {
builder: (_, accessControlMode, __) { final accessControlMode = ref.watch(
vpnSettingProvider.select((state) => state.accessControl.mode),
);
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
@@ -553,10 +601,11 @@ class AccessControlWidget extends StatelessWidget {
), ),
isSelected: accessControlMode == item, isSelected: accessControlMode == item,
onPressed: () { onPressed: () {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState(
config.accessControl = config.accessControl.copyWith( (state) => state.copyWith.accessControl(
mode: item, mode: item,
); ),
);
}, },
) )
], ],
@@ -575,9 +624,11 @@ class AccessControlWidget extends StatelessWidget {
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, AccessSortType>( child: Consumer(
selector: (_, config) => config.accessControl.sort, builder: (_, ref, __) {
builder: (_, accessSortType, __) { final accessSortType = ref.watch(
vpnSettingProvider.select((state) => state.accessControl.sort),
);
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
@@ -589,10 +640,11 @@ class AccessControlWidget extends StatelessWidget {
), ),
isSelected: accessSortType == item, isSelected: accessSortType == item,
onPressed: () { onPressed: () {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState(
config.accessControl = config.accessControl.copyWith( (state) => state.copyWith.accessControl(
sort: item, sort: item,
); ),
);
}, },
), ),
], ],
@@ -611,9 +663,12 @@ class AccessControlWidget extends StatelessWidget {
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, bool>( child: Consumer(
selector: (_, config) => config.accessControl.isFilterSystemApp, builder: (_, ref, __) {
builder: (_, isFilterSystemApp, __) { final isFilterSystemApp = ref.watch(
vpnSettingProvider
.select((state) => state.accessControl.isFilterSystemApp),
);
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
@@ -622,10 +677,11 @@ class AccessControlWidget extends StatelessWidget {
_getTextWithIsFilterSystemApp(item), _getTextWithIsFilterSystemApp(item),
isSelected: isFilterSystemApp == item, isSelected: isFilterSystemApp == item,
onPressed: () { onPressed: () {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState(
config.accessControl = config.accessControl.copyWith( (state) => state.copyWith.accessControl(
isFilterSystemApp: item, isFilterSystemApp: item,
); ),
);
}, },
) )
], ],
@@ -637,63 +693,35 @@ class AccessControlWidget extends StatelessWidget {
); );
} }
_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<List<String>>(
() 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 { _copyToClipboard() async {
await globalState.safeRun(() { await globalState.safeRun(() {
final data = globalState.appController.config.accessControl.toJson(); final data = globalState.config.vpnProps.accessControl.toJson();
Clipboard.setData( Clipboard.setData(
ClipboardData( ClipboardData(
text: json.encode(data), text: json.encode(data),
), ),
); );
}); });
if (!context.mounted) return; if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
_pasteToClipboard() async { _pasteToClipboard() async {
await globalState.safeRun(() async { await globalState.safeRun(
final config = globalState.appController.config; () async {
final data = await Clipboard.getData('text/plain'); final data = await Clipboard.getData('text/plain');
final text = data?.text; final text = data?.text;
if (text == null) return; if (text == null) return;
config.accessControl = AccessControl.fromJson( ref.read(vpnSettingProvider.notifier).updateState(
json.decode(text), (state) => state.copyWith(
); accessControl: AccessControl.fromJson(
}); json.decode(text),
if (!context.mounted) return; ),
),
);
},
);
if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
@@ -712,7 +740,9 @@ class AccessControlWidget extends StatelessWidget {
CommonChip( CommonChip(
avatar: const Icon(Icons.auto_awesome), avatar: const Icon(Icons.auto_awesome),
label: appLocalizations.intelligentSelected, label: appLocalizations.intelligentSelected,
onPressed: _intelligentSelected, onPressed: () {
Navigator.of(context).pop(1);
},
), ),
CommonChip( CommonChip(
avatar: const Icon(Icons.paste), avatar: const Icon(Icons.paste),

View File

@@ -1,61 +1,258 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class CloseConnectionsSwitch extends StatelessWidget { class CloseConnectionsItem extends ConsumerWidget {
const CloseConnectionsSwitch({super.key}); const CloseConnectionsItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final closeConnections = ref.watch(
selector: (_, config) => config.appSetting.closeConnections, appSettingProvider.select((state) => state.closeConnections),
builder: (_, closeConnections, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.autoCloseConnections), title: Text(appLocalizations.autoCloseConnections),
subtitle: Text(appLocalizations.autoCloseConnectionsDesc), subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: closeConnections, value: closeConnections,
onChanged: (value) async { onChanged: (value) async {
final config = globalState.appController.config; ref.read(appSettingProvider.notifier).updateState(
config.appSetting = config.appSetting.copyWith( (state) => state.copyWith(
closeConnections: value, closeConnections: value,
),
); );
}, },
), ),
);
},
); );
} }
} }
class UsageSwitch extends StatelessWidget { class UsageItem extends ConsumerWidget {
const UsageSwitch({super.key}); const UsageItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final onlyStatisticsProxy = ref.watch(
selector: (_, config) => config.appSetting.onlyStatisticsProxy, appSettingProvider.select((state) => state.onlyStatisticsProxy),
builder: (_, onlyProxy, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.onlyStatisticsProxy), title: Text(appLocalizations.onlyStatisticsProxy),
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc), subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: onlyProxy, value: onlyStatisticsProxy,
onChanged: (bool value) async { onChanged: (bool value) async {
final config = globalState.appController.config; ref.read(appSettingProvider.notifier).updateState(
config.appSetting = config.appSetting.copyWith( (state) => state.copyWith(
onlyStatisticsProxy: value, onlyStatisticsProxy: value,
),
); );
}, },
), ),
); );
}, }
}
class MinimizeItem extends ConsumerWidget {
const MinimizeItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final minimizeOnExit = ref.watch(
appSettingProvider.select((state) => state.minimizeOnExit),
);
return ListItem.switchItem(
title: Text(appLocalizations.minimizeOnExit),
subtitle: Text(appLocalizations.minimizeOnExitDesc),
delegate: SwitchDelegate(
value: minimizeOnExit,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
minimizeOnExit: value,
),
);
},
),
);
}
}
class AutoLaunchItem extends ConsumerWidget {
const AutoLaunchItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoLaunch = ref.watch(
appSettingProvider.select((state) => state.autoLaunch),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoLaunch),
subtitle: Text(appLocalizations.autoLaunchDesc),
delegate: SwitchDelegate(
value: autoLaunch,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoLaunch: value,
),
);
},
),
);
}
}
class SilentLaunchItem extends ConsumerWidget {
const SilentLaunchItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final silentLaunch = ref.watch(
appSettingProvider.select((state) => state.silentLaunch),
);
return ListItem.switchItem(
title: Text(appLocalizations.silentLaunch),
subtitle: Text(appLocalizations.silentLaunchDesc),
delegate: SwitchDelegate(
value: silentLaunch,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
silentLaunch: value,
),
);
},
),
);
}
}
class AutoRunItem extends ConsumerWidget {
const AutoRunItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoRun = ref.watch(
appSettingProvider.select((state) => state.autoRun),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(
value: autoRun,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoRun: value,
),
);
},
),
);
}
}
class HiddenItem extends ConsumerWidget {
const HiddenItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final hidden = ref.watch(
appSettingProvider.select((state) => state.hidden),
);
return ListItem.switchItem(
title: Text(appLocalizations.exclude),
subtitle: Text(appLocalizations.excludeDesc),
delegate: SwitchDelegate(
value: hidden,
onChanged: (value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
hidden: value,
),
);
},
),
);
}
}
class AnimateTabItem extends ConsumerWidget {
const AnimateTabItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isAnimateToPage = ref.watch(
appSettingProvider.select((state) => state.isAnimateToPage),
);
return ListItem.switchItem(
title: Text(appLocalizations.tabAnimation),
subtitle: Text(appLocalizations.tabAnimationDesc),
delegate: SwitchDelegate(
value: isAnimateToPage,
onChanged: (value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
isAnimateToPage: value,
),
);
},
),
);
}
}
class OpenLogsItem extends ConsumerWidget {
const OpenLogsItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final openLogs = ref.watch(
appSettingProvider.select((state) => state.openLogs),
);
return ListItem.switchItem(
title: Text(appLocalizations.logcat),
subtitle: Text(appLocalizations.logcatDesc),
delegate: SwitchDelegate(
value: openLogs,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
openLogs: value,
),
);
},
),
);
}
}
class AutoCheckUpdateItem extends ConsumerWidget {
const AutoCheckUpdateItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final autoCheckUpdate = ref.watch(
appSettingProvider.select((state) => state.autoCheckUpdate),
);
return ListItem.switchItem(
title: Text(appLocalizations.autoCheckUpdate),
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
delegate: SwitchDelegate(
value: autoCheckUpdate,
onChanged: (bool value) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(
autoCheckUpdate: value,
),
);
},
),
); );
} }
} }
@@ -71,156 +268,20 @@ class ApplicationSettingFragment extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Widget> items = [ List<Widget> items = [
Selector<Config, bool>( MinimizeItem(),
selector: (_, config) => config.appSetting.minimizeOnExit, if (system.isDesktop) ...[
builder: (_, isMinimizeOnExit, child) { AutoLaunchItem(),
return ListItem.switchItem( SilentLaunchItem(),
title: Text(appLocalizations.minimizeOnExit), ],
subtitle: Text(appLocalizations.minimizeOnExitDesc), AutoRunItem(),
delegate: SwitchDelegate( if (Platform.isAndroid) ...[
value: isMinimizeOnExit, HiddenItem(),
onChanged: (bool value) { AnimateTabItem(),
final config = context.read<Config>(); ],
config.appSetting = config.appSetting.copyWith( OpenLogsItem(),
minimizeOnExit: value, CloseConnectionsItem(),
); UsageItem(),
}, AutoCheckUpdateItem(),
),
);
},
),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoLaunch,
builder: (_, autoLaunch, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoLaunch),
subtitle: Text(appLocalizations.autoLaunchDesc),
delegate: SwitchDelegate(
value: autoLaunch,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoLaunch: value,
);
},
),
);
},
),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.silentLaunch,
builder: (_, silentLaunch, child) {
return ListItem.switchItem(
title: Text(appLocalizations.silentLaunch),
subtitle: Text(appLocalizations.silentLaunchDesc),
delegate: SwitchDelegate(
value: silentLaunch,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
silentLaunch: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoRun,
builder: (_, autoRun, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoRun),
subtitle: Text(appLocalizations.autoRunDesc),
delegate: SwitchDelegate(
value: autoRun,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoRun: value,
);
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.hidden,
builder: (_, isExclude, child) {
return ListItem.switchItem(
title: Text(appLocalizations.exclude),
subtitle: Text(appLocalizations.excludeDesc),
delegate: SwitchDelegate(
value: isExclude,
onChanged: (value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
hidden: value,
);
},
),
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.isAnimateToPage,
builder: (_, isAnimateToPage, child) {
return ListItem.switchItem(
title: Text(appLocalizations.tabAnimation),
subtitle: Text(appLocalizations.tabAnimationDesc),
delegate: SwitchDelegate(
value: isAnimateToPage,
onChanged: (value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
isAnimateToPage: value,
);
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.openLogs,
builder: (_, openLogs, child) {
return ListItem.switchItem(
title: Text(appLocalizations.logcat),
subtitle: Text(appLocalizations.logcatDesc),
delegate: SwitchDelegate(
value: openLogs,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
openLogs: value,
);
},
),
);
},
),
const CloseConnectionsSwitch(),
const UsageSwitch(),
Selector<Config, bool>(
selector: (_, config) => config.appSetting.autoCheckUpdate,
builder: (_, autoCheckUpdate, child) {
return ListItem.switchItem(
title: Text(appLocalizations.autoCheckUpdate),
subtitle: Text(appLocalizations.autoCheckUpdateDesc),
delegate: SwitchDelegate(
value: autoCheckUpdate,
onChanged: (bool value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
autoCheckUpdate: value,
);
},
),
);
},
),
]; ];
return ListView.separated( return ListView.separated(
itemBuilder: (_, index) { itemBuilder: (_, index) {

View File

@@ -4,14 +4,15 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/dav_client.dart'; import 'package:fl_clash/common/dav_client.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/fade_box.dart';
import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/list.dart';
import 'package:fl_clash/widgets/text.dart'; import 'package:fl_clash/widgets/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class BackupAndRecovery extends StatelessWidget { class BackupAndRecovery extends ConsumerWidget {
const BackupAndRecovery({super.key}); const BackupAndRecovery({super.key});
_showAddWebDAV(DAV? dav) async { _showAddWebDAV(DAV? dav) async {
@@ -121,139 +122,140 @@ class BackupAndRecovery extends StatelessWidget {
_recoveryOnLocal(context, recoveryOption); _recoveryOnLocal(context, recoveryOption);
} }
@override _handleChange(String? value, WidgetRef ref) {
Widget build(BuildContext context) { if (value == null) {
return Selector<Config, DAV?>( return;
selector: (_, config) => config.dav, }
builder: (_, dav, __) { ref.read(appDAVSettingProvider.notifier).updateState(
final client = dav != null ? DAVClient(dav) : null; (state) => state?.copyWith(
return ListView( fileName: value,
children: [ ),
ListHeader(title: appLocalizations.remote),
if (dav == null)
ListItem(
leading: const Icon(Icons.account_box),
title: Text(appLocalizations.noInfo),
subtitle: Text(appLocalizations.pleaseBindWebDAV),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.bind,
),
),
)
else ...[
ListItem(
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: client!.pingCompleter.future,
builder: (_, snapshot) {
return Center(
child: FadeBox(
child: snapshot.connectionState ==
ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
const SizedBox(
height: 4,
),
ListItem.input(
title: Text(appLocalizations.file),
subtitle: Text(dav.fileName),
delegate: InputDelegate(
title: appLocalizations.file,
value: dav.fileName,
resetValue: defaultDavFileName,
onChanged: (String? value) {
if (value == null) {
return;
}
globalState.appController.config.dav =
globalState.appController.config.dav?.copyWith(
fileName: value,
);
},
),
),
ListItem(
onTap: () {
_backupOnWebDAV(context, client);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.remoteBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnWebDAV(context, client);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.remoteRecoveryDesc),
),
],
ListHeader(title: appLocalizations.local),
ListItem(
onTap: () {
_backupOnLocal(context);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnLocal(context);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
),
],
); );
}, }
@override
Widget build(BuildContext context, ref) {
final dav = ref.watch(appDAVSettingProvider);
final client = dav != null ? DAVClient(dav) : null;
return ListView(
children: [
ListHeader(title: appLocalizations.remote),
if (dav == null)
ListItem(
leading: const Icon(Icons.account_box),
title: Text(appLocalizations.noInfo),
subtitle: Text(appLocalizations.pleaseBindWebDAV),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.bind,
),
),
)
else ...[
ListItem(
leading: const Icon(Icons.account_box),
title: TooltipText(
text: Text(
dav.user,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(appLocalizations.connectivity),
FutureBuilder<bool>(
future: client!.pingCompleter.future,
builder: (_, snapshot) {
return Center(
child: FadeBox(
child: snapshot.connectionState ==
ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 1,
),
)
: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: snapshot.data == true
? Colors.green
: Colors.red,
),
width: 12,
height: 12,
),
),
);
},
),
],
),
),
trailing: FilledButton.tonal(
onPressed: () {
_showAddWebDAV(dav);
},
child: Text(
appLocalizations.edit,
),
),
),
const SizedBox(
height: 4,
),
ListItem.input(
title: Text(appLocalizations.file),
subtitle: Text(dav.fileName),
delegate: InputDelegate(
title: appLocalizations.file,
value: dav.fileName,
resetValue: defaultDavFileName,
onChanged: (value) {
_handleChange(value, ref);
},
),
),
ListItem(
onTap: () {
_backupOnWebDAV(context, client);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.remoteBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnWebDAV(context, client);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.remoteRecoveryDesc),
),
],
ListHeader(title: appLocalizations.local),
ListItem(
onTap: () {
_backupOnLocal(context);
},
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.localBackupDesc),
),
ListItem(
onTap: () {
_handleRecoveryOnLocal(context);
},
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.localRecoveryDesc),
),
],
); );
} }
} }
@@ -302,16 +304,16 @@ class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
} }
} }
class WebDAVFormDialog extends StatefulWidget { class WebDAVFormDialog extends ConsumerStatefulWidget {
final DAV? dav; final DAV? dav;
const WebDAVFormDialog({super.key, this.dav}); const WebDAVFormDialog({super.key, this.dav});
@override @override
State<WebDAVFormDialog> createState() => _WebDAVFormDialogState(); ConsumerState<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
} }
class _WebDAVFormDialogState extends State<WebDAVFormDialog> { class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
late TextEditingController uriController; late TextEditingController uriController;
late TextEditingController userController; late TextEditingController userController;
late TextEditingController passwordController; late TextEditingController passwordController;
@@ -328,7 +330,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
_submit() { _submit() {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
globalState.appController.config.dav = DAV( ref.read(appDAVSettingProvider.notifier).value = DAV(
uri: uriController.text, uri: uriController.text,
user: userController.text, user: userController.text,
password: passwordController.text, password: passwordController.text,
@@ -337,7 +339,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
} }
_delete() { _delete() {
globalState.appController.config.dav = null; ref.read(appDAVSettingProvider.notifier).value = null;
Navigator.pop(context); Navigator.pop(context);
} }

View File

@@ -1,208 +1,172 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class OverrideItem extends StatelessWidget { class OverrideItem extends ConsumerWidget {
const OverrideItem({super.key}); const OverrideItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final override = ref.watch(overrideDnsProvider);
selector: (_, config) => config.overrideDns, return ListItem.switchItem(
builder: (_, override, __) { title: Text(appLocalizations.overrideDns),
return ListItem.switchItem( subtitle: Text(appLocalizations.overrideDnsDesc),
title: Text(appLocalizations.overrideDns), delegate: SwitchDelegate(
subtitle: Text(appLocalizations.overrideDnsDesc), value: override,
delegate: SwitchDelegate( onChanged: (bool value) async {
value: override, ref.read(overrideDnsProvider.notifier).value = value;
onChanged: (bool value) async { },
final config = globalState.appController.config; ),
config.overrideDns = value;
},
),
);
},
); );
} }
} }
class StatusItem extends StatelessWidget { class StatusItem extends ConsumerWidget {
const StatusItem({super.key}); const StatusItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final enable =
selector: (_, clashConfig) => clashConfig.dns.enable, ref.watch(patchClashConfigProvider.select((state) => state.dns.enable));
builder: (_, enable, __) { return ListItem.switchItem(
return ListItem.switchItem( title: Text(appLocalizations.status),
title: Text(appLocalizations.status), subtitle: Text(appLocalizations.statusDesc),
subtitle: Text(appLocalizations.statusDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: enable,
value: enable, onChanged: (bool value) async {
onChanged: (bool value) async { ref
final clashConfig = globalState.appController.clashConfig; .read(patchClashConfigProvider.notifier)
final dns = clashConfig.dns; .updateState((state) => state.copyWith.dns(enable: value));
clashConfig.dns = dns.copyWith( },
enable: value, ),
);
},
),
);
},
); );
} }
} }
class PreferH3Item extends StatelessWidget { class PreferH3Item extends ConsumerWidget {
const PreferH3Item({super.key}); const PreferH3Item({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final preferH3 = ref
selector: (_, clashConfig) => clashConfig.dns.preferH3, .watch(patchClashConfigProvider.select((state) => state.dns.preferH3));
builder: (_, preferH3, __) { return ListItem.switchItem(
return ListItem.switchItem( title: const Text("PreferH3"),
title: const Text("PreferH3"), subtitle: Text(appLocalizations.preferH3Desc),
subtitle: Text(appLocalizations.preferH3Desc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: preferH3,
value: preferH3, onChanged: (bool value) async {
onChanged: (bool value) async { ref
final clashConfig = globalState.appController.clashConfig; .read(patchClashConfigProvider.notifier)
final dns = clashConfig.dns; .updateState((state) => state.copyWith.dns(preferH3: value));
clashConfig.dns = dns.copyWith( },
preferH3: value, ),
);
},
),
);
},
); );
} }
} }
class IPv6Item extends StatelessWidget { class IPv6Item extends ConsumerWidget {
const IPv6Item({super.key}); const IPv6Item({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final ipv6 = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.ipv6, patchClashConfigProvider.select((state) => state.dns.ipv6),
builder: (_, ipv6, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: const Text("IPv6"), title: const Text("IPv6"),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: ipv6, value: ipv6,
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(ipv6: value));
ipv6: value, },
); ),
},
),
);
},
); );
} }
} }
class RespectRulesItem extends StatelessWidget { class RespectRulesItem extends ConsumerWidget {
const RespectRulesItem({super.key}); const RespectRulesItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final respectRules = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.respectRules, patchClashConfigProvider.select((state) => state.dns.respectRules),
builder: (_, respectRules, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.respectRules), title: Text(appLocalizations.respectRules),
subtitle: Text(appLocalizations.respectRulesDesc), subtitle: Text(appLocalizations.respectRulesDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: respectRules, value: respectRules,
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(respectRules: value));
respectRules: value, },
); ),
},
),
);
},
); );
} }
} }
class DnsModeItem extends StatelessWidget { class DnsModeItem extends ConsumerWidget {
const DnsModeItem({super.key}); const DnsModeItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, DnsMode>( final enhancedMode = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.enhancedMode, patchClashConfigProvider.select((state) => state.dns.enhancedMode),
builder: (_, enhancedMode, __) { );
return ListItem<DnsMode>.options( return ListItem<DnsMode>.options(
title: Text(appLocalizations.dnsMode), title: Text(appLocalizations.dnsMode),
subtitle: Text(enhancedMode.name), subtitle: Text(enhancedMode.name),
delegate: OptionsDelegate( delegate: OptionsDelegate(
title: appLocalizations.dnsMode, title: appLocalizations.dnsMode,
options: DnsMode.values, options: DnsMode.values,
onChanged: (value) { onChanged: (value) {
if (value == null) { if (value == null) {
return; return;
} }
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith(enhancedMode: value); .updateState((state) => state.copyWith.dns(enhancedMode: value));
}, },
textBuilder: (dnsMode) => dnsMode.name, textBuilder: (dnsMode) => dnsMode.name,
value: enhancedMode, value: enhancedMode,
), ),
);
},
); );
} }
} }
class FakeIpRangeItem extends StatelessWidget { class FakeIpRangeItem extends ConsumerWidget {
const FakeIpRangeItem({super.key}); const FakeIpRangeItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, String>( final fakeIpRange = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange, patchClashConfigProvider.select((state) => state.dns.fakeIpRange),
builder: (_, fakeIpRange, __) { );
return ListItem.input( return ListItem.input(
title: Text(appLocalizations.fakeipRange), title: Text(appLocalizations.fakeipRange),
subtitle: Text(fakeIpRange), subtitle: Text(fakeIpRange),
delegate: InputDelegate( delegate: InputDelegate(
title: appLocalizations.fakeipRange, title: appLocalizations.fakeipRange,
value: fakeIpRange, value: fakeIpRange,
onChanged: (String? value) { onChanged: (String? value) {
if (value != null) { if (value == null) {
try { return;
final clashConfig = globalState.appController.clashConfig; }
clashConfig.dns = clashConfig.dns.copyWith( ref
fakeIpRange: value, .read(patchClashConfigProvider.notifier)
); .updateState((state) => state.copyWith.dns(fakeIpRange: value));
} catch (e) { },
globalState.showMessage( ),
title: appLocalizations.fakeipRange,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
); );
} }
} }
@@ -217,20 +181,22 @@ class FakeIpFilterItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.fakeipFilter, title: appLocalizations.fakeipFilter,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter, builder: (_, ref, __) {
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), final fakeIpFilter = ref.watch(
builder: (_, fakeIpFilter, __) { patchClashConfigProvider
.select((state) => state.dns.fakeIpFilter),
);
return ListPage( return ListPage(
title: appLocalizations.fakeipFilter, title: appLocalizations.fakeipFilter,
items: fakeIpFilter, items: fakeIpFilter,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items) { onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(
fakeIpFilter: List.from(items), fakeIpFilter: List.from(items),
); ));
}, },
); );
}, },
@@ -252,24 +218,24 @@ class DefaultNameserverItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.defaultNameserver, title: appLocalizations.defaultNameserver,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver, final defaultNameserver = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider
builder: (_, defaultNameserver, __) { .select((state) => state.dns.defaultNameserver),
return ListPage( );
title: appLocalizations.defaultNameserver, return ListPage(
items: defaultNameserver, title: appLocalizations.defaultNameserver,
titleBuilder: (item) => Text(item), items: defaultNameserver,
onChange: (items) { titleBuilder: (item) => Text(item),
final clashConfig = globalState.appController.clashConfig; onChange: (items) {
final dns = clashConfig.dns; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.dns = dns.copyWith( (state) => state.copyWith.dns(
defaultNameserver: List.from(items), defaultNameserver: List.from(items),
); ),
}, );
); },
}, );
), }),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
@@ -287,78 +253,71 @@ class NameserverItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
title: appLocalizations.nameserver, title: appLocalizations.nameserver,
isBlur: false, isBlur: false,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.nameserver, final nameserver = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider.select((state) => state.dns.nameserver),
builder: (_, nameserver, __) { );
return ListPage( return ListPage(
title: "域名服务器", title: "域名服务器",
items: nameserver, items: nameserver,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items) { onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
final dns = clashConfig.dns; (state) => state.copyWith.dns(
clashConfig.dns = dns.copyWith( nameserver: List.from(items),
nameserver: List.from(items), ),
); );
}, },
); );
}, }),
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
} }
} }
class UseHostsItem extends StatelessWidget { class UseHostsItem extends ConsumerWidget {
const UseHostsItem({super.key}); const UseHostsItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final useHosts = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.useHosts, patchClashConfigProvider.select((state) => state.dns.useHosts),
builder: (_, useHosts, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.useHosts), title: Text(appLocalizations.useHosts),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: useHosts, value: useHosts,
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(useHosts: value));
useHosts: value, },
); ),
},
),
);
},
); );
} }
} }
class UseSystemHostsItem extends StatelessWidget { class UseSystemHostsItem extends ConsumerWidget {
const UseSystemHostsItem({super.key}); const UseSystemHostsItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final useSystemHosts = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts, patchClashConfigProvider.select((state) => state.dns.useSystemHosts),
builder: (_, useSystemHosts, __) { );
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.useSystemHosts), title: Text(appLocalizations.useSystemHosts),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: useSystemHosts, value: useSystemHosts,
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(
useSystemHosts: value, useSystemHosts: value,
); ));
}, },
), ),
);
},
); );
} }
} }
@@ -374,26 +333,25 @@ class NameserverPolicyItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.nameserverPolicy, title: appLocalizations.nameserverPolicy,
widget: Selector<ClashConfig, Map<String, String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy, final nameserverPolicy = ref.watch(
shouldRebuild: (prev, next) => patchClashConfigProvider
!const MapEquality<String, String>().equals(prev, next), .select((state) => state.dns.nameserverPolicy),
builder: (_, nameserverPolicy, __) { );
return ListPage( return ListPage(
title: appLocalizations.nameserverPolicy, title: appLocalizations.nameserverPolicy,
items: nameserverPolicy.entries, items: nameserverPolicy.entries,
titleBuilder: (item) => Text(item.key), titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value), subtitleBuilder: (item) => Text(item.value),
onChange: (items) { onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
final dns = clashConfig.dns; (state) => state.copyWith.dns(
clashConfig.dns = dns.copyWith( nameserverPolicy: Map.fromEntries(items),
nameserverPolicy: Map.fromEntries(items), ),
); );
}, },
); );
}, }),
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
@@ -411,20 +369,22 @@ class ProxyServerNameserverItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.proxyNameserver, title: appLocalizations.proxyNameserver,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver, builder: (_, ref, __) {
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), final proxyServerNameserver = ref.watch(
builder: (_, proxyServerNameserver, __) { patchClashConfigProvider
.select((state) => state.dns.proxyServerNameserver),
);
return ListPage( return ListPage(
title: appLocalizations.proxyNameserver, title: appLocalizations.proxyNameserver,
items: proxyServerNameserver, items: proxyServerNameserver,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items) { onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
final dns = clashConfig.dns; (state) => state.copyWith.dns(
clashConfig.dns = dns.copyWith( proxyServerNameserver: List.from(items),
proxyServerNameserver: List.from(items), ),
); );
}, },
); );
}, },
@@ -446,93 +406,80 @@ class FallbackItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.fallback, title: appLocalizations.fallback,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.fallback, final fallback = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider.select((state) => state.dns.fallback),
builder: (_, fallback, __) { );
return ListPage( return ListPage(
title: appLocalizations.fallback, title: appLocalizations.fallback,
items: fallback, items: fallback,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items) { onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
final dns = clashConfig.dns; (state) => state.copyWith.dns(
clashConfig.dns = dns.copyWith( fallback: List.from(items),
fallback: List.from(items), ),
); );
}, },
); );
}, }),
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
} }
} }
class GeoipItem extends StatelessWidget { class GeoipItem extends ConsumerWidget {
const GeoipItem({super.key}); const GeoipItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final geoip = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip, patchClashConfigProvider
builder: (_, geoip, __) { .select((state) => state.dns.fallbackFilter.geoip),
return ListItem.switchItem( );
title: const Text("Geoip"), return ListItem.switchItem(
delegate: SwitchDelegate( title: const Text("Geoip"),
value: geoip, delegate: SwitchDelegate(
onChanged: (bool value) async { value: geoip,
final clashConfig = globalState.appController.clashConfig; onChanged: (bool value) async {
final dns = clashConfig.dns; ref
clashConfig.dns = dns.copyWith( .read(patchClashConfigProvider.notifier)
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value), .updateState((state) => state.copyWith.dns.fallbackFilter(
); geoip: value,
}, ));
), },
); ),
},
); );
} }
} }
class GeoipCodeItem extends StatelessWidget { class GeoipCodeItem extends ConsumerWidget {
const GeoipCodeItem({super.key}); const GeoipCodeItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, String>( final geoipCode = ref.watch(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode, patchClashConfigProvider
builder: (_, geoipCode, __) { .select((state) => state.dns.fallbackFilter.geoipCode),
return ListItem.input( );
title: Text(appLocalizations.geoipCode), return ListItem.input(
subtitle: Text(geoipCode), title: Text(appLocalizations.geoipCode),
delegate: InputDelegate( subtitle: Text(geoipCode),
title: appLocalizations.geoipCode, delegate: InputDelegate(
value: geoipCode, title: appLocalizations.geoipCode,
onChanged: (String? value) { value: geoipCode,
if (value != null) { onChanged: (String? value) {
try { if (value == null) {
final clashConfig = globalState.appController.clashConfig; return;
final dns = clashConfig.dns; }
clashConfig.dns = dns.copyWith( ref.read(patchClashConfigProvider.notifier).updateState(
fallbackFilter: dns.fallbackFilter.copyWith( (state) => state.copyWith.dns.fallbackFilter(
geoipCode: value, geoipCode: value,
), ),
); );
} catch (e) { },
globalState.showMessage( ),
title: appLocalizations.geoipCode,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
);
},
); );
} }
} }
@@ -547,26 +494,24 @@ class GeositeItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: "Geosite", title: "Geosite",
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite, final geosite = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider
builder: (_, geosite, __) { .select((state) => state.dns.fallbackFilter.geosite),
return ListPage( );
title: "Geosite", return ListPage(
items: geosite, title: "Geosite",
titleBuilder: (item) => Text(item), items: geosite,
onChange: (items) { titleBuilder: (item) => Text(item),
final clashConfig = globalState.appController.clashConfig; onChange: (items) {
final dns = clashConfig.dns; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.dns = dns.copyWith( (state) => state.copyWith.dns.fallbackFilter(
fallbackFilter: dns.fallbackFilter.copyWith( geosite: List.from(items),
geosite: List.from(items), ),
), );
); },
}, );
); }),
},
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
@@ -583,26 +528,24 @@ class IpcidrItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.ipcidr, title: appLocalizations.ipcidr,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, ___) {
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr, final ipcidr = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider
builder: (_, ipcidr, __) { .select((state) => state.dns.fallbackFilter.ipcidr),
return ListPage( );
title: appLocalizations.ipcidr, return ListPage(
items: ipcidr, title: appLocalizations.ipcidr,
titleBuilder: (item) => Text(item), items: ipcidr,
onChange: (items) { titleBuilder: (item) => Text(item),
final clashConfig = globalState.appController.clashConfig; onChange: (items) {
final dns = clashConfig.dns; ref
clashConfig.dns = dns.copyWith( .read(patchClashConfigProvider.notifier)
fallbackFilter: dns.fallbackFilter.copyWith( .updateState((state) => state.copyWith.dns.fallbackFilter(
ipcidr: List.from(items), ipcidr: List.from(items),
), ));
); },
}, );
); }),
},
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
@@ -619,26 +562,24 @@ class DomainItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.domain, title: appLocalizations.domain,
widget: Selector<ClashConfig, List<String>>( widget: Consumer(builder: (_, ref, __) {
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain, final domain = ref.watch(
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), patchClashConfigProvider
builder: (_, domain, __) { .select((state) => state.dns.fallbackFilter.domain),
return ListPage( );
title: appLocalizations.domain, return ListPage(
items: domain, title: appLocalizations.domain,
titleBuilder: (item) => Text(item), items: domain,
onChange: (items) { titleBuilder: (item) => Text(item),
final clashConfig = globalState.appController.clashConfig; onChange: (items) {
final dns = clashConfig.dns; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.dns = dns.copyWith( (state) => state.copyWith.dns.fallbackFilter(
fallbackFilter: dns.fallbackFilter.copyWith( domain: List.from(items),
domain: List.from(items), ),
), );
); },
}, );
); }),
},
),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
@@ -700,14 +641,12 @@ const dnsItems = <Widget>[
FallbackFilterOptions(), FallbackFilterOptions(),
]; ];
class DnsListView extends StatelessWidget { class DnsListView extends ConsumerWidget {
const DnsListView({super.key}); const DnsListView({super.key});
_initActions(BuildContext context) { _initActions(BuildContext context, WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () async { onPressed: () async {
final res = await globalState.showMessage( final res = await globalState.showMessage(
@@ -719,7 +658,12 @@ class DnsListView extends StatelessWidget {
if (res != true) { if (res != true) {
return; return;
} }
globalState.appController.clashConfig.dns = defaultDns;
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
dns: defaultDns,
),
);
}, },
tooltip: appLocalizations.reset, tooltip: appLocalizations.reset,
icon: const Icon( icon: const Icon(
@@ -731,8 +675,8 @@ class DnsListView extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
_initActions(context); _initActions(context, ref);
return generateListView( return generateListView(
dnsItems, dnsItems,
); );

View File

@@ -1,198 +1,190 @@
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class LogLevelItem extends StatelessWidget { class LogLevelItem extends ConsumerWidget {
const LogLevelItem({super.key}); const LogLevelItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, LogLevel>( final logLevel =
selector: (_, clashConfig) => clashConfig.logLevel, ref.watch(patchClashConfigProvider.select((state) => state.logLevel));
builder: (_, value, __) { return ListItem<LogLevel>.options(
return ListItem<LogLevel>.options( leading: const Icon(Icons.info_outline),
leading: const Icon(Icons.info_outline), title: Text(appLocalizations.logLevel),
title: Text(appLocalizations.logLevel), subtitle: Text(logLevel.name),
subtitle: Text(value.name), delegate: OptionsDelegate<LogLevel>(
delegate: OptionsDelegate<LogLevel>( title: appLocalizations.logLevel,
title: appLocalizations.logLevel, options: LogLevel.values,
options: LogLevel.values, onChanged: (LogLevel? value) {
onChanged: (LogLevel? value) { if (value == null) {
if (value == null) { return;
return; }
} ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.logLevel = value; logLevel: value,
}, ),
textBuilder: (logLevel) => logLevel.name, );
value: value, },
), textBuilder: (logLevel) => logLevel.name,
); value: logLevel,
}, ),
); );
} }
} }
class UaItem extends StatelessWidget { class UaItem extends ConsumerWidget {
const UaItem({super.key}); const UaItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, String?>( final globalUa =
selector: (_, clashConfig) => clashConfig.globalRealUa, ref.watch(patchClashConfigProvider.select((state) => state.globalUa));
builder: (_, value, __) { return ListItem<String?>.options(
return ListItem<String?>.options( leading: const Icon(Icons.computer_outlined),
leading: const Icon(Icons.computer_outlined), title: const Text("UA"),
title: const Text("UA"), subtitle: Text(globalUa ?? appLocalizations.defaultText),
subtitle: Text(value ?? appLocalizations.defaultText), delegate: OptionsDelegate<String?>(
delegate: OptionsDelegate<String?>( title: "UA",
title: "UA", options: [
options: [ null,
null, "clash-verge/v1.6.6",
"clash-verge/v1.6.6", "ClashforWindows/0.19.23",
"ClashforWindows/0.19.23", ],
], value: globalUa,
value: value, onChanged: (value) {
onChanged: (ua) { ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.globalRealUa = ua; globalUa: value,
}, ),
textBuilder: (ua) => ua ?? appLocalizations.defaultText, );
), },
); textBuilder: (ua) => ua ?? appLocalizations.defaultText,
}, ),
); );
} }
} }
class KeepAliveIntervalItem extends StatelessWidget { class KeepAliveIntervalItem extends ConsumerWidget {
const KeepAliveIntervalItem({super.key}); const KeepAliveIntervalItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, int>( final keepAliveInterval = ref.watch(
selector: (_, config) => config.keepAliveInterval, patchClashConfigProvider.select((state) => state.keepAliveInterval));
builder: (_, value, __) { return ListItem.input(
return ListItem.input( leading: const Icon(Icons.timer_outlined),
leading: const Icon(Icons.timer_outlined), title: Text(appLocalizations.keepAliveIntervalDesc),
title: Text(appLocalizations.keepAliveIntervalDesc), subtitle: Text("$keepAliveInterval ${appLocalizations.seconds}"),
subtitle: Text("$value ${appLocalizations.seconds}"), delegate: InputDelegate(
delegate: InputDelegate( title: appLocalizations.keepAliveIntervalDesc,
title: appLocalizations.keepAliveIntervalDesc, suffixText: appLocalizations.seconds,
suffixText: appLocalizations.seconds, resetValue: "$defaultKeepAliveInterval",
resetValue: "$defaultKeepAliveInterval", value: "$keepAliveInterval",
value: "$value", onChanged: (String? value) {
onChanged: (String? value) { if (value == null) {
if (value != null) { return;
try { }
final intValue = int.parse(value); globalState.safeRun(
if (intValue <= 0) { () {
throw "Invalid keepAliveInterval"; final intValue = int.parse(value);
} if (intValue <= 0) {
globalState.appController.clashConfig.keepAliveInterval = throw "Invalid keepAliveInterval";
intValue; }
} catch (e) { ref.read(patchClashConfigProvider.notifier).updateState(
globalState.showMessage( (state) => state.copyWith(
title: appLocalizations.keepAliveIntervalDesc, keepAliveInterval: intValue,
message: TextSpan(
text: e.toString(),
), ),
); );
}
}
}, },
), silence: false,
); title: appLocalizations.keepAliveIntervalDesc,
}, );
},
),
); );
} }
} }
class TestUrlItem extends StatelessWidget { class TestUrlItem extends ConsumerWidget {
const TestUrlItem({super.key}); const TestUrlItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, String>( final testUrl =
selector: (_, config) => config.appSetting.testUrl, ref.watch(appSettingProvider.select((state) => state.testUrl));
builder: (_, value, __) { return ListItem.input(
return ListItem.input( leading: const Icon(Icons.timeline),
leading: const Icon(Icons.timeline), title: Text(appLocalizations.testUrl),
title: Text(appLocalizations.testUrl), subtitle: Text(testUrl),
subtitle: Text(value), delegate: InputDelegate(
delegate: InputDelegate( resetValue: defaultTestUrl,
resetValue: defaultTestUrl, title: appLocalizations.testUrl,
title: appLocalizations.testUrl, value: testUrl,
value: value, onChanged: (String? value) {
onChanged: (String? value) { if (value == null) {
if (value != null) { return;
try { }
if (!value.isUrl) { globalState.safeRun(
throw "Invalid url"; () {
} if (!value.isUrl) {
final config = globalState.appController.config; throw "Invalid url";
config.appSetting = config.appSetting.copyWith(
testUrl: value,
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
);
} }
} ref.read(appSettingProvider.notifier).updateState(
}, (state) => state.copyWith(
), testUrl: value,
); ),
}, );
},
silence: false,
title: appLocalizations.testUrl,
);
}),
); );
} }
} }
class MixedPortItem extends StatelessWidget { class MixedPortItem extends ConsumerWidget {
const MixedPortItem({super.key}); const MixedPortItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, int>( final mixedPort =
selector: (_, clashConfig) => clashConfig.mixedPort, ref.watch(patchClashConfigProvider.select((state) => state.mixedPort));
builder: (_, value, __) { return ListItem.input(
return ListItem.input( leading: const Icon(Icons.adjust_outlined),
leading: const Icon(Icons.adjust_outlined), title: Text(appLocalizations.proxyPort),
title: Text(appLocalizations.proxyPort), subtitle: Text("$mixedPort"),
subtitle: Text("$value"), delegate: InputDelegate(
delegate: InputDelegate( title: appLocalizations.proxyPort,
title: appLocalizations.proxyPort, value: "$mixedPort",
value: "$value", onChanged: (String? value) {
onChanged: (String? value) { if (value == null) {
if (value != null) { return;
try { }
final mixedPort = int.parse(value); globalState.safeRun(
if (mixedPort < 1024 || mixedPort > 49151) { () {
throw "Invalid port"; final mixedPort = int.parse(value);
} if (mixedPort < 1024 || mixedPort > 49151) {
globalState.appController.clashConfig.mixedPort = mixedPort; throw "Invalid port";
} catch (e) { }
globalState.showMessage( ref.read(patchClashConfigProvider.notifier).updateState(
title: appLocalizations.proxyPort, (state) => state.copyWith(
message: TextSpan( mixedPort: mixedPort,
text: e.toString(),
), ),
); );
}
}
}, },
resetValue: "$defaultMixedPort", silence: false,
), title: appLocalizations.proxyPort,
); );
}, },
resetValue: "$defaultMixedPort",
),
); );
} }
} }
@@ -209,20 +201,21 @@ class HostsItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: "Hosts", title: "Hosts",
widget: Selector<ClashConfig, HostsMap>( widget: Consumer(
selector: (_, clashConfig) => clashConfig.hosts, builder: (_, ref, __) {
shouldRebuild: (prev, next) => final hosts = ref
!const MapEquality<String, String>().equals(prev, next), .watch(patchClashConfigProvider.select((state) => state.hosts));
builder: (_, hosts, ___) {
final entries = hosts.entries;
return ListPage( return ListPage(
title: "Hosts", title: "Hosts",
items: entries, items: hosts.entries,
titleBuilder: (item) => Text(item.key), titleBuilder: (item) => Text(item.key),
subtitleBuilder: (item) => Text(item.value), subtitleBuilder: (item) => Text(item.value),
onChange: (items){ onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.hosts = Map.fromEntries(items); (state) => state.copyWith(
hosts: Map.fromEntries(items),
),
);
}, },
); );
}, },
@@ -233,190 +226,192 @@ class HostsItem extends StatelessWidget {
} }
} }
class Ipv6Item extends StatelessWidget { class Ipv6Item extends ConsumerWidget {
const Ipv6Item({super.key}); const Ipv6Item({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final ipv6 =
selector: (_, clashConfig) => clashConfig.ipv6, ref.watch(patchClashConfigProvider.select((state) => state.ipv6));
builder: (_, ipv6, __) { return ListItem.switchItem(
return ListItem.switchItem( leading: const Icon(Icons.water_outlined),
leading: const Icon(Icons.water_outlined), title: const Text("IPv6"),
title: const Text("IPv6"), subtitle: Text(appLocalizations.ipv6Desc),
subtitle: Text(appLocalizations.ipv6Desc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: ipv6,
value: ipv6, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.ipv6 = value; ipv6: value,
}, ),
), );
); },
}, ),
); );
} }
} }
class AllowLanItem extends StatelessWidget { class AllowLanItem extends ConsumerWidget {
const AllowLanItem({super.key}); const AllowLanItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final allowLan =
selector: (_, clashConfig) => clashConfig.allowLan, ref.watch(patchClashConfigProvider.select((state) => state.allowLan));
builder: (_, allowLan, __) { return ListItem.switchItem(
return ListItem.switchItem( leading: const Icon(Icons.device_hub),
leading: const Icon(Icons.device_hub), title: Text(appLocalizations.allowLan),
title: Text(appLocalizations.allowLan), subtitle: Text(appLocalizations.allowLanDesc),
subtitle: Text(appLocalizations.allowLanDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: allowLan,
value: allowLan, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(patchClashConfigProvider.notifier).updateState(
final clashConfig = context.read<ClashConfig>(); (state) => state.copyWith(
clashConfig.allowLan = value; allowLan: value,
}, ),
), );
); },
}, ),
); );
} }
} }
class UnifiedDelayItem extends StatelessWidget { class UnifiedDelayItem extends ConsumerWidget {
const UnifiedDelayItem({super.key}); const UnifiedDelayItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final unifiedDelay = ref
selector: (_, clashConfig) => clashConfig.unifiedDelay, .watch(patchClashConfigProvider.select((state) => state.unifiedDelay));
builder: (_, unifiedDelay, __) {
return ListItem.switchItem( return ListItem.switchItem(
leading: const Icon(Icons.compress_outlined), leading: const Icon(Icons.compress_outlined),
title: Text(appLocalizations.unifiedDelay), title: Text(appLocalizations.unifiedDelay),
subtitle: Text(appLocalizations.unifiedDelayDesc), subtitle: Text(appLocalizations.unifiedDelayDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: unifiedDelay, value: unifiedDelay,
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; ref.read(patchClashConfigProvider.notifier).updateState(
appController.clashConfig.unifiedDelay = value; (state) => state.copyWith(
}, unifiedDelay: value,
), ),
); );
}, },
),
); );
} }
} }
class FindProcessItem extends StatelessWidget { class FindProcessItem extends ConsumerWidget {
const FindProcessItem({super.key}); const FindProcessItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final findProcess = ref.watch(patchClashConfigProvider
selector: (_, clashConfig) => .select((state) => state.findProcessMode == FindProcessMode.always));
clashConfig.findProcessMode == FindProcessMode.always,
builder: (_, findProcess, __) { return ListItem.switchItem(
return ListItem.switchItem( leading: const Icon(Icons.polymer_outlined),
leading: const Icon(Icons.polymer_outlined), title: Text(appLocalizations.findProcessMode),
title: Text(appLocalizations.findProcessMode), subtitle: Text(appLocalizations.findProcessModeDesc),
subtitle: Text(appLocalizations.findProcessModeDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: findProcess,
value: findProcess, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.findProcessMode = findProcessMode:
value ? FindProcessMode.always : FindProcessMode.off; value ? FindProcessMode.always : FindProcessMode.off,
}, ),
), );
); },
}, ),
); );
} }
} }
class TcpConcurrentItem extends StatelessWidget { class TcpConcurrentItem extends ConsumerWidget {
const TcpConcurrentItem({super.key}); const TcpConcurrentItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final tcpConcurrent = ref
selector: (_, clashConfig) => clashConfig.tcpConcurrent, .watch(patchClashConfigProvider.select((state) => state.tcpConcurrent));
builder: (_, tcpConcurrent, __) { return ListItem.switchItem(
return ListItem.switchItem( leading: const Icon(Icons.double_arrow_outlined),
leading: const Icon(Icons.double_arrow_outlined), title: Text(appLocalizations.tcpConcurrent),
title: Text(appLocalizations.tcpConcurrent), subtitle: Text(appLocalizations.tcpConcurrentDesc),
subtitle: Text(appLocalizations.tcpConcurrentDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: tcpConcurrent,
value: tcpConcurrent, onChanged: (value) async {
onChanged: (bool value) async { ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.tcpConcurrent = value; tcpConcurrent: value,
}, ),
), );
); },
}, ),
); );
} }
} }
class GeodataLoaderItem extends StatelessWidget { class GeodataLoaderItem extends ConsumerWidget {
const GeodataLoaderItem({super.key}); const GeodataLoaderItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final isMemconservative = ref.watch(patchClashConfigProvider.select(
selector: (_, clashConfig) => (state) => state.geodataLoader == GeodataLoader.memconservative));
clashConfig.geodataLoader == geodataLoaderMemconservative, return ListItem.switchItem(
builder: (_, memconservative, __) { leading: const Icon(Icons.memory),
return ListItem.switchItem( title: Text(appLocalizations.geodataLoader),
leading: const Icon(Icons.memory), subtitle: Text(appLocalizations.geodataLoaderDesc),
title: Text(appLocalizations.geodataLoader), delegate: SwitchDelegate(
subtitle: Text(appLocalizations.geodataLoaderDesc), value: isMemconservative,
delegate: SwitchDelegate( onChanged: (bool value) async {
value: memconservative, ref.read(patchClashConfigProvider.notifier).updateState(
onChanged: (bool value) async { (state) => state.copyWith(
final appController = globalState.appController; geodataLoader: value
appController.clashConfig.geodataLoader = ? GeodataLoader.memconservative
value ? geodataLoaderMemconservative : geodataLoaderStandard; : GeodataLoader.standard,
}, ),
), );
); },
}, ),
); );
} }
} }
class ExternalControllerItem extends StatelessWidget { class ExternalControllerItem extends ConsumerWidget {
const ExternalControllerItem({super.key}); const ExternalControllerItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final hasExternalController = ref.watch(patchClashConfigProvider.select(
selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty, (state) => state.externalController == ExternalControllerStatus.open));
builder: (_, hasExternalController, __) { return ListItem.switchItem(
return ListItem.switchItem( leading: const Icon(Icons.api_outlined),
leading: const Icon(Icons.api_outlined), title: Text(appLocalizations.externalController),
title: Text(appLocalizations.externalController), subtitle: Text(appLocalizations.externalControllerDesc),
subtitle: Text(appLocalizations.externalControllerDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: hasExternalController,
value: hasExternalController, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(patchClashConfigProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.externalController = externalController: value
value ? defaultExternalController : ''; ? ExternalControllerStatus.open
}, : ExternalControllerStatus.close,
), ),
); );
}, },
),
); );
} }
} }
final generalItems = const [ final generalItems = <Widget>[
LogLevelItem(), LogLevelItem(),
UaItem(), UaItem(),
KeepAliveIntervalItem(), if (system.isDesktop) KeepAliveIntervalItem(),
TestUrlItem(), TestUrlItem(),
MixedPortItem(), MixedPortItem(),
HostsItem(), HostsItem(),

View File

@@ -3,200 +3,185 @@ import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class VPNItem extends StatelessWidget { class VPNItem extends ConsumerWidget {
const VPNItem({super.key}); const VPNItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final enable =
selector: (_, config) => config.vpnProps.enable, ref.watch(vpnSettingProvider.select((state) => state.enable));
builder: (_, enable, __) { return ListItem.switchItem(
return ListItem.switchItem( title: const Text("VPN"),
title: const Text("VPN"), subtitle: Text(appLocalizations.vpnEnableDesc),
subtitle: Text(appLocalizations.vpnEnableDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: enable,
value: enable, onChanged: (value) async {
onChanged: (value) async { ref.read(vpnSettingProvider.notifier).updateState(
final config = globalState.appController.config; (state) => state.copyWith(
config.vpnProps = config.vpnProps.copyWith( enable: value,
enable: value, ),
); );
}, },
), ),
);
},
); );
} }
} }
class TUNItem extends StatelessWidget { class TUNItem extends ConsumerWidget {
const TUNItem({super.key}); const TUNItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final enable =
selector: (_, clashConfig) => clashConfig.tun.enable, ref.watch(patchClashConfigProvider.select((state) => state.tun.enable));
builder: (_, enable, __) {
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.tun), title: Text(appLocalizations.tun),
subtitle: Text(appLocalizations.tunDesc), subtitle: Text(appLocalizations.tunDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: enable, value: enable,
onChanged: (value) async { onChanged: (value) async {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.tun = clashConfig.tun.copyWith( (state) => state.copyWith.tun(
enable: value, enable: value,
),
); );
}, },
), ),
);
},
); );
} }
} }
class AllowBypassItem extends StatelessWidget { class AllowBypassItem extends ConsumerWidget {
const AllowBypassItem({super.key}); const AllowBypassItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final allowBypass =
selector: (_, config) => config.vpnProps.allowBypass, ref.watch(vpnSettingProvider.select((state) => state.allowBypass));
builder: (_, allowBypass, __) { return ListItem.switchItem(
return ListItem.switchItem( title: Text(appLocalizations.allowBypass),
title: Text(appLocalizations.allowBypass), subtitle: Text(appLocalizations.allowBypassDesc),
subtitle: Text(appLocalizations.allowBypassDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: allowBypass,
value: allowBypass, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(vpnSettingProvider.notifier).updateState(
final config = globalState.appController.config; (state) => state.copyWith(
final vpnProps = config.vpnProps; allowBypass: value,
config.vpnProps = vpnProps.copyWith( ),
allowBypass: value,
); );
}, },
), ),
);
},
); );
} }
} }
class VpnSystemProxyItem extends StatelessWidget { class VpnSystemProxyItem extends ConsumerWidget {
const VpnSystemProxyItem({super.key}); const VpnSystemProxyItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final systemProxy =
selector: (_, config) => config.vpnProps.systemProxy, ref.watch(vpnSettingProvider.select((state) => state.systemProxy));
builder: (_, systemProxy, __) { return ListItem.switchItem(
return ListItem.switchItem( title: Text(appLocalizations.systemProxy),
title: Text(appLocalizations.systemProxy), subtitle: Text(appLocalizations.systemProxyDesc),
subtitle: Text(appLocalizations.systemProxyDesc), delegate: SwitchDelegate(
delegate: SwitchDelegate( value: systemProxy,
value: systemProxy, onChanged: (bool value) async {
onChanged: (bool value) async { ref.read(vpnSettingProvider.notifier).updateState(
final config = globalState.appController.config; (state) => state.copyWith(
final vpnProps = config.vpnProps; systemProxy: value,
config.vpnProps = vpnProps.copyWith( ),
systemProxy: value,
); );
}, },
), ),
);
},
); );
} }
} }
class SystemProxyItem extends StatelessWidget { class SystemProxyItem extends ConsumerWidget {
const SystemProxyItem({super.key}); const SystemProxyItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final systemProxy =
selector: (_, config) => config.networkProps.systemProxy, ref.watch(networkSettingProvider.select((state) => state.systemProxy));
builder: (_, systemProxy, __) {
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.systemProxy), title: Text(appLocalizations.systemProxy),
subtitle: Text(appLocalizations.systemProxyDesc), subtitle: Text(appLocalizations.systemProxyDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: systemProxy, value: systemProxy,
onChanged: (bool value) async { onChanged: (bool value) async {
final config = globalState.appController.config; ref.read(networkSettingProvider.notifier).updateState(
final networkProps = config.networkProps; (state) => state.copyWith(
config.networkProps = networkProps.copyWith( systemProxy: value,
systemProxy: value, ),
); );
}, },
), ),
);
},
); );
} }
} }
class Ipv6Item extends StatelessWidget { class Ipv6Item extends ConsumerWidget {
const Ipv6Item({super.key}); const Ipv6Item({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, bool>( final ipv6 = ref.watch(vpnSettingProvider.select((state) => state.ipv6));
selector: (_, config) => config.vpnProps.ipv6, return ListItem.switchItem(
builder: (_, ipv6, __) { title: const Text("IPv6"),
return ListItem.switchItem( subtitle: Text(appLocalizations.ipv6InboundDesc),
title: const Text("IPv6"), delegate: SwitchDelegate(
subtitle: Text(appLocalizations.ipv6InboundDesc), value: ipv6,
delegate: SwitchDelegate( onChanged: (bool value) async {
value: ipv6, ref.read(vpnSettingProvider.notifier).updateState(
onChanged: (bool value) async { (state) => state.copyWith(
final config = globalState.appController.config; ipv6: value,
final vpnProps = config.vpnProps; ),
config.vpnProps = vpnProps.copyWith(
ipv6: value,
); );
}, },
), ),
);
},
); );
} }
} }
class TunStackItem extends StatelessWidget { class TunStackItem extends ConsumerWidget {
const TunStackItem({super.key}); const TunStackItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, TunStack>( final stack =
selector: (_, clashConfig) => clashConfig.tun.stack, ref.watch(patchClashConfigProvider.select((state) => state.tun.stack));
builder: (_, stack, __) {
return ListItem.options( return ListItem.options(
title: Text(appLocalizations.stackMode), title: Text(appLocalizations.stackMode),
subtitle: Text(stack.name), subtitle: Text(stack.name),
delegate: OptionsDelegate<TunStack>( delegate: OptionsDelegate<TunStack>(
value: stack, value: stack,
options: TunStack.values, options: TunStack.values,
textBuilder: (value) => value.name, textBuilder: (value) => value.name,
onChanged: (value) { onChanged: (value) {
if (value == null) { if (value == null) {
return; return;
} }
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.tun = clashConfig.tun.copyWith( (state) => state.copyWith.tun(
stack: value, stack: value,
),
); );
}, },
title: appLocalizations.stackMode, title: appLocalizations.stackMode,
), ),
);
},
); );
} }
} }
@@ -204,11 +189,9 @@ class TunStackItem extends StatelessWidget {
class BypassDomainItem extends StatelessWidget { class BypassDomainItem extends StatelessWidget {
const BypassDomainItem({super.key}); const BypassDomainItem({super.key});
_initActions(BuildContext context) { _initActions(BuildContext context, WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () async { onPressed: () async {
final res = await globalState.showMessage( final res = await globalState.showMessage(
@@ -220,10 +203,11 @@ class BypassDomainItem extends StatelessWidget {
if (res != true) { if (res != true) {
return; return;
} }
final config = globalState.appController.config; ref.read(networkSettingProvider.notifier).updateState(
config.networkProps = config.networkProps.copyWith( (state) => state.copyWith(
bypassDomain: defaultBypassDomain, bypassDomain: defaultBypassDomain,
); ),
);
}, },
tooltip: appLocalizations.reset, tooltip: appLocalizations.reset,
icon: const Icon( icon: const Icon(
@@ -243,20 +227,21 @@ class BypassDomainItem extends StatelessWidget {
isBlur: false, isBlur: false,
isScaffold: true, isScaffold: true,
title: appLocalizations.bypassDomain, title: appLocalizations.bypassDomain,
widget: Selector<Config, List<String>>( widget: Consumer(
selector: (_, config) => config.networkProps.bypassDomain, builder: (_, ref, __) {
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next), _initActions(context, ref);
builder: (context, bypassDomain, __) { final bypassDomain = ref.watch(
_initActions(context); networkSettingProvider.select((state) => state.bypassDomain));
return ListPage( return ListPage(
title: appLocalizations.bypassDomain, title: appLocalizations.bypassDomain,
items: bypassDomain, items: bypassDomain,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items) { onChange: (items) {
final config = globalState.appController.config; ref.read(networkSettingProvider.notifier).updateState(
config.networkProps = config.networkProps.copyWith( (state) => state.copyWith(
bypassDomain: List.from(items), bypassDomain: List.from(items),
); ),
);
}, },
); );
}, },
@@ -267,77 +252,74 @@ class BypassDomainItem extends StatelessWidget {
} }
} }
class RouteModeItem extends StatelessWidget { class RouteModeItem extends ConsumerWidget {
const RouteModeItem({super.key}); const RouteModeItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, RouteMode>( final routeMode =
selector: (_, clashConfig) => clashConfig.routeMode, ref.watch(networkSettingProvider.select((state) => state.routeMode));
builder: (_, value, __) { return ListItem<RouteMode>.options(
return ListItem<RouteMode>.options( title: Text(appLocalizations.routeMode),
title: Text(appLocalizations.routeMode), subtitle: Text(Intl.message("routeMode_${routeMode.name}")),
subtitle: Text(Intl.message("routeMode_${value.name}")), delegate: OptionsDelegate<RouteMode>(
delegate: OptionsDelegate<RouteMode>( title: appLocalizations.routeMode,
title: appLocalizations.routeMode, options: RouteMode.values,
options: RouteMode.values, onChanged: (RouteMode? value) {
onChanged: (RouteMode? value) { if (value == null) {
if (value == null) { return;
return; }
} ref.read(networkSettingProvider.notifier).updateState(
final appController = globalState.appController; (state) => state.copyWith(
appController.clashConfig.routeMode = value; routeMode: value,
}, ),
textBuilder: (routeMode) => Intl.message( );
"routeMode_${routeMode.name}", },
), textBuilder: (routeMode) => Intl.message(
value: value, "routeMode_${routeMode.name}",
), ),
); value: routeMode,
}, ),
); );
} }
} }
class RouteAddressItem extends StatelessWidget { class RouteAddressItem extends ConsumerWidget {
const RouteAddressItem({super.key}); const RouteAddressItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<ClashConfig, bool>( final bypassPrivate = ref.watch(networkSettingProvider
selector: (_, clashConfig) => clashConfig.routeMode == RouteMode.config, .select((state) => state.routeMode == RouteMode.bypassPrivate));
builder: (_, value, child) { if (bypassPrivate) {
if (value) { return Container();
return child!; }
} return ListItem.open(
return Container(); title: Text(appLocalizations.routeAddress),
}, subtitle: Text(appLocalizations.routeAddressDesc),
child: ListItem.open( delegate: OpenDelegate(
title: Text(appLocalizations.routeAddress), isBlur: false,
subtitle: Text(appLocalizations.routeAddressDesc), isScaffold: true,
delegate: OpenDelegate( title: appLocalizations.routeAddress,
isBlur: false, widget: Consumer(
isScaffold: true, builder: (_, ref, __) {
title: appLocalizations.routeAddress, final routeAddress = ref.watch(patchClashConfigProvider
widget: Selector<ClashConfig, List<String>>( .select((state) => state.tun.routeAddress));
selector: (_, clashConfig) => clashConfig.includeRouteAddress, return ListPage(
shouldRebuild: (prev, next) => title: appLocalizations.routeAddress,
!stringListEquality.equals(prev, next), items: routeAddress,
builder: (context, routeAddress, __) { titleBuilder: (item) => Text(item),
return ListPage( onChange: (items) {
title: appLocalizations.routeAddress, ref.read(patchClashConfigProvider.notifier).updateState(
items: routeAddress, (state) => state.copyWith.tun(
titleBuilder: (item) => Text(item), routeAddress: List.from(items),
onChange: (items) { ),
final clashConfig = globalState.appController.clashConfig; );
clashConfig.includeRouteAddress = },
Set<String>.from(items).toList(); );
}, },
);
},
),
extendPageWidth: 360,
), ),
extendPageWidth: 360,
), ),
); );
} }
@@ -349,7 +331,8 @@ final networkItems = [
...generateSection( ...generateSection(
title: "VPN", title: "VPN",
items: [ items: [
const SystemProxyItem(), const VpnSystemProxyItem(),
const BypassDomainItem(),
const AllowBypassItem(), const AllowBypassItem(),
const Ipv6Item(), const Ipv6Item(),
], ],
@@ -373,14 +356,12 @@ final networkItems = [
), ),
]; ];
class NetworkListView extends StatelessWidget { class NetworkListView extends ConsumerWidget {
const NetworkListView({super.key}); const NetworkListView({super.key});
_initActions(BuildContext context) { _initActions(BuildContext context, WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () async { onPressed: () async {
final res = await globalState.showMessage( final res = await globalState.showMessage(
@@ -392,9 +373,14 @@ class NetworkListView extends StatelessWidget {
if (res != true) { if (res != true) {
return; return;
} }
final appController = globalState.appController; ref.read(vpnSettingProvider.notifier).updateState(
appController.config.vpnProps = defaultVpnProps; (state) => defaultVpnProps,
appController.clashConfig.tun = defaultTun; );
ref.read(patchClashConfigProvider.notifier).updateState(
(state) => state.copyWith(
tun: defaultTun,
),
);
}, },
tooltip: appLocalizations.reset, tooltip: appLocalizations.reset,
icon: const Icon( icon: const Icon(
@@ -406,8 +392,8 @@ class NetworkListView extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
_initActions(context); _initActions(context, ref);
return generateListView( return generateListView(
networkItems, networkItems,
); );

View File

@@ -0,0 +1,150 @@
import 'dart:async';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'item.dart';
class ConnectionsFragment extends ConsumerStatefulWidget {
const ConnectionsFragment({super.key});
@override
ConsumerState<ConnectionsFragment> createState() =>
_ConnectionsFragmentState();
}
class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
with PageMixin {
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
const ConnectionsState(),
);
final ScrollController _scrollController = ScrollController(
keepScrollOffset: false,
);
Timer? timer;
@override
List<Widget> get actions => [
IconButton(
onPressed: () async {
clashCore.closeConnections();
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
),
];
@override
get onSearch => (value) {
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(
query: value,
);
};
@override
get onKeywordsUpdate => (keywords) {
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(keywords: keywords);
};
_updateConnections() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
_connectionsStateNotifier.value =
_connectionsStateNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
timer = Timer(Duration(seconds: 1), () async {
_updateConnections();
});
});
}
@override
void initState() {
super.initState();
ref.listenManual(
isCurrentPageProvider(
PageLabel.connections,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
_updateConnections();
}
_handleBlockConnection(String id) async {
clashCore.closeConnection(id);
_connectionsStateNotifier.value = _connectionsStateNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
}
@override
void dispose() {
timer?.cancel();
_connectionsStateNotifier.dispose();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ConnectionsState>(
valueListenable: _connectionsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullConnectionsDesc,
);
}
return CommonScrollBar(
controller: _scrollController,
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
);
},
);
}
}

View File

@@ -0,0 +1,155 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FindProcessBuilder extends StatelessWidget {
final Widget Function(bool value) builder;
const FindProcessBuilder({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
return Consumer(
builder: (_, ref, __) {
final value = ref.watch(
patchClashConfigProvider.select(
(state) =>
state.findProcessMode == FindProcessMode.always &&
Platform.isAndroid,
),
);
return builder(value);
},
);
}
}
class ConnectionItem extends StatelessWidget {
final Connection connection;
final Function(String)? onClick;
final Widget? trailing;
const ConnectionItem({
super.key,
required this.connection,
this.onClick,
this.trailing,
});
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
return await app?.getPackageIcon(connection.metadata.process);
}
String _getSourceText(Connection connection) {
final metadata = connection.metadata;
if (metadata.process.isEmpty) {
return connection.start.lastUpdateTimeDesc;
}
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
}
@override
Widget build(BuildContext context) {
final title = Text(
connection.desc,
style: context.textTheme.bodyLarge,
);
final subTitle = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8,
),
Text(
_getSourceText(connection),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final chain in connection.chains)
CommonChip(
label: chain,
onPressed: () {
if (onClick == null) return;
onClick!(chain);
},
),
],
),
],
);
if (!Platform.isAndroid) {
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
title: title,
subtitle: subTitle,
trailing: trailing,
);
}
return FindProcessBuilder(
builder: (bool value) {
final leading = value
? GestureDetector(
onTap: () {
if (onClick == null) return;
final process = connection.metadata.process;
if (process.isEmpty) return;
onClick!(process);
},
child: Container(
margin: const EdgeInsets.only(top: 4),
width: 48,
height: 48,
child: FutureBuilder<ImageProvider?>(
future: _getPackageIcon(connection),
builder: (_, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Container();
} else {
return Image(
image: snapshot.data!,
gaplessPlayback: true,
width: 48,
height: 48,
);
}
},
),
),
)
: null;
return ListItem(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
leading: leading,
title: title,
subtitle: subTitle,
trailing: trailing,
);
},
);
}
}

View File

@@ -0,0 +1,212 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'item.dart';
double _preOffset = 0;
class RequestsFragment extends ConsumerStatefulWidget {
const RequestsFragment({super.key});
@override
ConsumerState<RequestsFragment> createState() => _RequestsFragmentState();
}
class _RequestsFragmentState extends ConsumerState<RequestsFragment>
with PageMixin {
final _requestsStateNotifier =
ValueNotifier<ConnectionsState>(const ConnectionsState());
List<Connection> _requests = [];
final ScrollController _scrollController = ScrollController(
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
);
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0;
@override
get onSearch => (value) {
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
query: value,
);
};
@override
get onKeywordsUpdate => (keywords) {
_requestsStateNotifier.value =
_requestsStateNotifier.value.copyWith(keywords: keywords);
};
@override
void initState() {
super.initState();
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: globalState.appState.requests.list,
);
ref.listenManual(
isCurrentPageProvider(
PageLabel.requests,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
ref.listenManual(
requestsProvider.select((state) => state.list),
(prev, next) {
if (!connectionListEquality.equals(prev, next)) {
_requests = next;
updateRequestsThrottler();
}
},
fireImmediately: true,
);
}
double _calcCacheHeight(Connection item) {
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
if (cacheHeight != null) {
return cacheHeight;
}
final size = globalState.measure.computeTextSize(
Text(
item.desc,
style: context.textTheme.bodyLarge,
),
maxWidth: _currentMaxWidth,
);
final chainsText = item.chains.join("");
final length = item.chains.length;
final chainSize = globalState.measure.computeTextSize(
Text(
chainsText,
style: context.textTheme.bodyMedium,
),
maxWidth: (_currentMaxWidth - (length - 1) * 6 - length * 24),
);
final baseHeight = globalState.measure.bodyMediumHeight;
final lines = (chainSize.height / baseHeight).round();
final computerHeight =
size.height + chainSize.height + 24 + 24 * (lines - 1);
_cacheDynamicHeightMap.put(item.id, computerHeight);
return computerHeight;
}
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear();
}
}
@override
void dispose() {
_requestsStateNotifier.dispose();
_scrollController.dispose();
_currentMaxWidth = 0;
_cacheDynamicHeightMap.clear();
super.dispose();
}
updateRequestsThrottler() {
throttler.call("request", () {
final isEquality = connectionListEquality.equals(
_requests,
_requestsStateNotifier.value.connections,
);
if (isEquality) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: _requests,
);
});
}, duration: commonDuration);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
return FindProcessBuilder(builder: (value) {
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return ValueListenableBuilder<ConnectionsState>(
valueListenable: _requestsStateNotifier,
builder: (_, state, __) {
final connections = state.list;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
final items = connections
.map<Widget>(
(connection) => ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index, __) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
),
),
),
);
},
);
});
},
);
}
}

View File

@@ -1,349 +0,0 @@
import 'dart:async';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ConnectionsFragment extends StatefulWidget {
const ConnectionsFragment({super.key});
@override
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
}
class _ConnectionsFragmentState extends State<ConnectionsFragment> {
final connectionsNotifier =
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
final ScrollController _scrollController = ScrollController(
keepScrollOffset: false,
);
Timer? timer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(
const Duration(seconds: 1),
(timer) async {
if (!context.mounted) {
return;
}
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
},
);
});
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
showSearch(
context: context,
delegate: ConnectionsSearchDelegate(
state: connectionsNotifier.value,
),
);
},
icon: const Icon(Icons.search),
),
IconButton(
onPressed: () async {
clashCore.closeConnections();
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
),
];
},
);
}
_addKeyword(String keyword) {
final isContains = connectionsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(connectionsNotifier.value.keywords)
..add(keyword);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = connectionsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(connectionsNotifier.value.keywords)
..remove(keyword);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
keywords: keywords,
);
}
_handleBlockConnection(String id) async {
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
}
@override
void dispose() {
timer?.cancel();
connectionsNotifier.dispose();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'connections' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: ValueListenableBuilder<ConnectionsAndKeywords>(
valueListenable: connectionsNotifier,
builder: (_, state, __) {
var connections = state.filteredConnections;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullConnectionsDesc,
);
}
connections = connections.reversed.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.keywords.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: _addKeyword,
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
)
],
);
},
),
);
}
}
class ConnectionsSearchDelegate extends SearchDelegate {
ValueNotifier<ConnectionsAndKeywords> connectionsNotifier;
ConnectionsSearchDelegate({
required ConnectionsAndKeywords state,
}) : connectionsNotifier = ValueNotifier<ConnectionsAndKeywords>(state);
get state => connectionsNotifier.value;
List<Connection> get _results {
final lowerQuery = query.toLowerCase().trim();
return connectionsNotifier.value.filteredConnections.where((request) {
final lowerNetwork = request.metadata.network.toLowerCase();
final lowerHost = request.metadata.host.toLowerCase();
final lowerDestinationIP = request.metadata.destinationIP.toLowerCase();
final lowerProcess = request.metadata.process.toLowerCase();
final lowerChains = request.chains.join("").toLowerCase();
return lowerNetwork.contains(lowerQuery) ||
lowerHost.contains(lowerQuery) ||
lowerDestinationIP.contains(lowerQuery) ||
lowerProcess.contains(lowerQuery) ||
lowerChains.contains(lowerQuery);
}).toList();
}
_addKeyword(String keyword) {
final isContains = connectionsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(connectionsNotifier.value.keywords)
..add(keyword);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = connectionsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(connectionsNotifier.value.keywords)
..remove(keyword);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
keywords: keywords,
);
}
_handleBlockConnection(String id) async {
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
}
@override
List<Widget>? buildActions(BuildContext context) {
return [
IconButton(
onPressed: () {
if (query.isEmpty) {
close(context, null);
return;
}
query = '';
},
icon: const Icon(Icons.clear),
),
const SizedBox(
width: 8,
)
];
}
@override
Widget? buildLeading(BuildContext context) {
return IconButton(
onPressed: () {
close(context, null);
},
icon: const Icon(Icons.arrow_back),
);
}
@override
Widget buildResults(BuildContext context) {
return buildSuggestions(context);
}
@override
void dispose() {
connectionsNotifier.dispose();
super.dispose();
}
@override
Widget buildSuggestions(BuildContext context) {
return ValueListenableBuilder(
valueListenable: connectionsNotifier,
builder: (_, __, ___) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.keywords.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
itemBuilder: (_, index) {
final connection = _results[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: _addKeyword,
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: _results.length,
),
)
],
);
},
);
}
}

View File

@@ -1,32 +1,43 @@
import 'dart:math'; import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'widgets/start_button.dart'; import 'widgets/start_button.dart';
class DashboardFragment extends StatefulWidget { class DashboardFragment extends ConsumerStatefulWidget {
const DashboardFragment({super.key}); const DashboardFragment({super.key});
@override @override
State<DashboardFragment> createState() => _DashboardFragmentState(); ConsumerState<DashboardFragment> createState() => _DashboardFragmentState();
} }
class _DashboardFragmentState extends State<DashboardFragment> { class _DashboardFragmentState extends ConsumerState<DashboardFragment>
with PageMixin {
final key = GlobalKey<SuperGridState>(); final key = GlobalKey<SuperGridState>();
_initScaffold(bool isCurrent) { @override
if (!isCurrent) { initState() {
return; ref.listenManual(
} isCurrentPageProvider(PageLabel.dashboard),
WidgetsBinding.instance.addPostFrameCallback((_) { (prev, next) {
final commonScaffoldState = if (prev != next && next == true) {
context.findAncestorStateOfType<CommonScaffoldState>(); initPageState();
commonScaffoldState?.floatingActionButton = const StartButton(); }
commonScaffoldState?.actions = [ },
fireImmediately: true,
);
return super.initState();
}
@override
Widget? get floatingActionButton => const StartButton();
@override
List<Widget> get actions => [
ValueListenableBuilder( ValueListenableBuilder(
valueListenable: key.currentState!.addedChildrenNotifier, valueListenable: key.currentState!.addedChildrenNotifier,
builder: (_, addedChildren, child) { builder: (_, addedChildren, child) {
@@ -67,72 +78,59 @@ class _DashboardFragmentState extends State<DashboardFragment> {
}, },
), ),
]; ];
});
_handleSave(List<GridItem> girdItems, WidgetRef ref) {
final dashboardWidgets = girdItems
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActiveBuilder( final dashboardState = ref.watch(dashboardStateProvider);
label: "dashboard", final columns = max(4 * ((dashboardState.viewWidth / 350).ceil()), 8);
builder: (isCurrent, child) { return Align(
_initScaffold(isCurrent); alignment: Alignment.topCenter,
return child!; child: SingleChildScrollView(
}, padding: const EdgeInsets.all(16).copyWith(
child: Align( bottom: 88,
alignment: Alignment.topCenter, ),
child: SingleChildScrollView( child: SuperGrid(
padding: const EdgeInsets.all(16).copyWith( key: key,
bottom: 88, crossAxisCount: columns,
), crossAxisSpacing: 16,
child: Selector2<AppState, Config, DashboardState>( mainAxisSpacing: 16,
selector: (_, appState, config) => DashboardState( children: [
dashboardWidgets: config.appSetting.dashboardWidgets, ...dashboardState.dashboardWidgets
viewWidth: appState.viewWidth, .where(
), (item) => item.platforms.contains(
builder: (_, state, ___) { SupportPlatform.currentPlatform,
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8); ),
return SuperGrid( )
key: key, .map(
crossAxisCount: columns, (item) => item.widget,
crossAxisSpacing: 16, ),
mainAxisSpacing: 16, ],
children: [ onSave: (girdItems) {
...state.dashboardWidgets _handleSave(girdItems, ref);
.where( },
(item) => item.platforms.contains( addedItemsBuilder: (girdItems) {
SupportPlatform.currentPlatform, return DashboardWidget.values
), .where(
) (item) =>
.map( !girdItems.contains(item.widget) &&
(item) => item.widget, item.platforms.contains(
SupportPlatform.currentPlatform,
), ),
], )
onSave: (girdItems) { .map((item) => item.widget)
final dashboardWidgets = girdItems .toList();
.map( },
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
dashboardWidgets: dashboardWidgets,
);
},
addedItemsBuilder: (girdItems) {
return DashboardWidget.values
.where(
(item) =>
!girdItems.contains(item.widget) &&
item.platforms.contains(
SupportPlatform.currentPlatform,
),
)
.map((item) => item.widget)
.toList();
},
);
},
),
), ),
), ),
); );

View File

@@ -1,58 +0,0 @@
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CoreInfo extends StatelessWidget {
const CoreInfo({super.key});
@override
Widget build(BuildContext context) {
return Selector<AppState, VersionInfo?>(
selector: (_, appState) => appState.versionInfo,
builder: (_, versionInfo, __) {
return CommonCard(
onPressed: () {},
info: Info(
label: appLocalizations.coreInfo,
iconData: Icons.memory,
),
child: Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.all(16).copyWith(top: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
flex: 1,
child: Text(
versionInfo?.clashName ?? '',
style: context
.textTheme
.titleMedium
?.toSoftBold,
),
),
const SizedBox(
height: 8,
),
Flexible(
flex: 1,
child: Text(
versionInfo?.version ?? '',
style: context
.textTheme
.titleLarge
?.toSoftBold,
),
),
],
),
),
);
},
);
}
}

View File

@@ -1,9 +1,9 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.dart'; import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class IntranetIP extends StatelessWidget { class IntranetIP extends StatelessWidget {
const IntranetIP({super.key}); const IntranetIP({super.key});
@@ -28,15 +28,15 @@ class IntranetIP extends StatelessWidget {
children: [ children: [
SizedBox( SizedBox(
height: globalState.measure.bodyMediumHeight + 2, height: globalState.measure.bodyMediumHeight + 2,
child: Selector<AppFlowingState, String?>( child: Consumer(
selector: (_, appFlowingState) => appFlowingState.localIp, builder: (_, ref, __) {
builder: (_, value, __) { final localIp = ref.watch(localIpProvider);
return FadeBox( return FadeBox(
child: value != null child: localIp != null
? TooltipText( ? TooltipText(
text: Text( text: Text(
value.isNotEmpty localIp.isNotEmpty
? value ? localIp
: appLocalizations.noNetwork, : appLocalizations.noNetwork,
style: context.textTheme.bodyMedium?.toLight style: context.textTheme.bodyMedium?.toLight
.adjustSize(1), .adjustSize(1),
@@ -48,7 +48,9 @@ class IntranetIP extends StatelessWidget {
padding: EdgeInsets.all(2), padding: EdgeInsets.all(2),
child: AspectRatio( child: AspectRatio(
aspectRatio: 1, aspectRatio: 1,
child: CircularProgressIndicator(), child: CircularProgressIndicator(
strokeWidth: 2,
),
), ),
), ),
); );

View File

@@ -47,6 +47,11 @@ class _MemoryInfoState extends State<MemoryInfo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final darkenLighter = context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1)
.toLighter;
final darken = context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1);
return SizedBox( return SizedBox(
height: getWidgetHeight(2), height: getWidgetHeight(2),
child: CommonCard( child: CommonCard(
@@ -90,17 +95,14 @@ class _MemoryInfoState extends State<MemoryInfo> {
child: WaveView( child: WaveView(
waveAmplitude: 12.0, waveAmplitude: 12.0,
waveFrequency: 0.35, waveFrequency: 0.35,
waveColor: context.colorScheme.secondaryContainer waveColor: darkenLighter,
.blendDarken(context, factor: 0.1)
.toLighter,
), ),
), ),
Positioned.fill( Positioned.fill(
child: WaveView( child: WaveView(
waveAmplitude: 12.0, waveAmplitude: 12.0,
waveFrequency: 0.9, waveFrequency: 0.9,
waveColor: context.colorScheme.secondaryContainer waveColor: darken,
.blendDarken(context, factor: 0.1),
), ),
), ),
], ],

View File

@@ -4,10 +4,11 @@ import 'package:dio/dio.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
final _networkDetectionState = ValueNotifier<NetworkDetectionState>( final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState( const NetworkDetectionState(
@@ -16,14 +17,14 @@ final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
), ),
); );
class NetworkDetection extends StatefulWidget { class NetworkDetection extends ConsumerStatefulWidget {
const NetworkDetection({super.key}); const NetworkDetection({super.key});
@override @override
State<NetworkDetection> createState() => _NetworkDetectionState(); ConsumerState<NetworkDetection> createState() => _NetworkDetectionState();
} }
class _NetworkDetectionState extends State<NetworkDetection> { class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
bool? _preIsStart; bool? _preIsStart;
Timer? _setTimeoutTimer; Timer? _setTimeoutTimer;
CancelToken? cancelToken; CancelToken? cancelToken;
@@ -31,6 +32,11 @@ class _NetworkDetectionState extends State<NetworkDetection> {
@override @override
void initState() { void initState() {
ref.listenManual(checkIpNumProvider, (prev, next) {
if (prev != next) {
_startCheck();
}
});
super.initState(); super.initState();
} }
@@ -47,12 +53,13 @@ class _NetworkDetectionState extends State<NetworkDetection> {
} }
_checkIp() async { _checkIp() async {
final appState = globalState.appController.appState; final appState = globalState.appState;
final appFlowingState = globalState.appController.appFlowingState;
final isInit = appState.isInit; final isInit = appState.isInit;
if (!isInit) return; if (!isInit) return;
final isStart = appFlowingState.isStart; final isStart = appState.runTime != null;
if (_preIsStart == false && _preIsStart == isStart) return; if (_preIsStart == false &&
_preIsStart == isStart &&
_networkDetectionState.value.ipInfo != null) return;
_clearSetTimeoutTimer(); _clearSetTimeoutTimer();
_networkDetectionState.value = _networkDetectionState.value.copyWith( _networkDetectionState.value = _networkDetectionState.value.copyWith(
isTesting: true, isTesting: true,
@@ -109,24 +116,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
} }
} }
_checkIpContainer(Widget child) {
return Selector<AppState, num>(
selector: (_, appState) {
return appState.checkIpNum;
},
shouldRebuild: (prev, next) {
if (prev != next) {
_startCheck();
}
return prev != next;
},
builder: (_, checkIpNum, child) {
return child!;
},
child: child,
);
}
_countryCodeToEmoji(String countryCode) { _countryCodeToEmoji(String countryCode) {
final String code = countryCode.toUpperCase(); final String code = countryCode.toUpperCase();
if (code.length != 2) { if (code.length != 2) {
@@ -141,109 +130,130 @@ class _NetworkDetectionState extends State<NetworkDetection> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: getWidgetHeight(1), height: getWidgetHeight(1),
child: _checkIpContainer( child: ValueListenableBuilder<NetworkDetectionState>(
ValueListenableBuilder<NetworkDetectionState>( valueListenable: _networkDetectionState,
valueListenable: _networkDetectionState, builder: (_, state, __) {
builder: (_, state, __) { final ipInfo = state.ipInfo;
final ipInfo = state.ipInfo; final isTesting = state.isTesting;
final isTesting = state.isTesting; return CommonCard(
return CommonCard( onPressed: () {},
onPressed: () {}, child: Column(
child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ Container(
Container( height: globalState.measure.titleMediumHeight + 16,
height: globalState.measure.titleMediumHeight + 16, padding: baseInfoEdgeInsets.copyWith(
padding: baseInfoEdgeInsets.copyWith( bottom: 0,
bottom: 0, ),
), child: Row(
child: Row( mainAxisSize: MainAxisSize.max,
mainAxisSize: MainAxisSize.max, children: [
children: [ ipInfo != null
ipInfo != null ? Text(
? Text( _countryCodeToEmoji(
_countryCodeToEmoji( ipInfo.countryCode,
ipInfo.countryCode,
),
style: Theme.of(context)
.textTheme
.titleMedium
?.toLight
.copyWith(
fontFamily: FontFamily.twEmoji.value,
),
)
: Icon(
Icons.network_check,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
), ),
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.networkDetection,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleSmall .titleMedium
?.copyWith( ?.toLight
color: context.colorScheme.onSurfaceVariant, .copyWith(
fontFamily: FontFamily.twEmoji.value,
), ),
)
: Icon(
Icons.network_check,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
), ),
const SizedBox(
width: 8,
),
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.networkDetection,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
), ),
), ),
], ),
), SizedBox(width: 2),
AspectRatio(
aspectRatio: 1,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
text: appLocalizations.detectionTip,
),
cancelable: false,
);
},
icon: Icon(
size: 16,
Icons.info_outline,
color: context.colorScheme.onSurfaceVariant,
),
),
)
],
), ),
Container( ),
padding: baseInfoEdgeInsets.copyWith( Container(
top: 0, padding: baseInfoEdgeInsets.copyWith(
), top: 0,
child: SizedBox( ),
height: globalState.measure.bodyMediumHeight + 2, child: SizedBox(
child: FadeBox( height: globalState.measure.bodyMediumHeight + 2,
child: ipInfo != null child: FadeBox(
? TooltipText( child: ipInfo != null
text: Text( ? TooltipText(
ipInfo.ip, text: Text(
style: context.textTheme.bodyMedium?.toLight ipInfo.ip,
.adjustSize(1), style: context.textTheme.bodyMedium?.toLight
maxLines: 1, .adjustSize(1),
overflow: TextOverflow.ellipsis, maxLines: 1,
), overflow: TextOverflow.ellipsis,
) ),
: FadeBox( )
child: isTesting == false && ipInfo == null : FadeBox(
? Text( child: isTesting == false && ipInfo == null
"timeout", ? Text(
style: context.textTheme.bodyMedium "timeout",
?.copyWith(color: Colors.red) style: context.textTheme.bodyMedium
.adjustSize(1), ?.copyWith(color: Colors.red)
maxLines: 1, .adjustSize(1),
overflow: TextOverflow.ellipsis, maxLines: 1,
) overflow: TextOverflow.ellipsis,
: Container( )
padding: const EdgeInsets.all(2), : Container(
child: const AspectRatio( padding: const EdgeInsets.all(2),
aspectRatio: 1, child: const AspectRatio(
child: CircularProgressIndicator(), aspectRatio: 1,
child: CircularProgressIndicator(
strokeWidth: 2,
), ),
), ),
), ),
), ),
), ),
) ),
], )
), ],
); ),
}, );
), },
), ),
); );
} }

View File

@@ -1,8 +1,9 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class NetworkSpeed extends StatefulWidget { class NetworkSpeed extends StatefulWidget {
const NetworkSpeed({super.key}); const NetworkSpeed({super.key});
@@ -49,9 +50,9 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
label: appLocalizations.networkSpeed, label: appLocalizations.networkSpeed,
iconData: Icons.speed_sharp, iconData: Icons.speed_sharp,
), ),
child: Selector<AppFlowingState, List<Traffic>>( child: Consumer(
selector: (_, appFlowingState) => appFlowingState.traffics, builder: (_, ref, __) {
builder: (_, traffics, __) { final traffics = ref.watch(trafficsProvider).list;
return Stack( return Stack(
children: [ children: [
Positioned.fill( Positioned.fill(

View File

@@ -1,11 +1,11 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class OutboundMode extends StatelessWidget { class OutboundMode extends StatelessWidget {
const OutboundMode({super.key}); const OutboundMode({super.key});
@@ -15,9 +15,10 @@ class OutboundMode extends StatelessWidget {
final height = getWidgetHeight(2); final height = getWidgetHeight(2);
return SizedBox( return SizedBox(
height: height, height: height,
child: Selector<ClashConfig, Mode>( child: Consumer(
selector: (_, clashConfig) => clashConfig.mode, builder: (_, ref, __) {
builder: (_, mode, __) { final mode =
ref.watch(patchClashConfigProvider.select((state) => state.mode));
return CommonCard( return CommonCard(
onPressed: () {}, onPressed: () {},
info: Info( info: Info(

View File

@@ -1,78 +1,76 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/fragments/config/network.dart'; import 'package:fl_clash/fragments/config/network.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class TUNButton extends StatelessWidget { class TUNButton extends StatelessWidget {
const TUNButton({super.key}); const TUNButton({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LocaleBuilder( return SizedBox(
builder: (_) => SizedBox( height: getWidgetHeight(1),
height: getWidgetHeight(1), child: CommonCard(
child: CommonCard( onPressed: () {
onPressed: () { showSheet(
showSheet( context: context,
context: context, body: generateListView(generateSection(
body: generateListView(generateSection( items: [
items: [ if (system.isDesktop) const TUNItem(),
if (system.isDesktop) const TUNItem(), const TunStackItem(),
const TunStackItem(), ],
], )),
)), title: appLocalizations.tun,
title: appLocalizations.tun, );
); },
}, info: Info(
info: Info( label: appLocalizations.tun,
label: appLocalizations.tun, iconData: Icons.stacked_line_chart,
iconData: Icons.stacked_line_chart, ),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
), ),
child: Container( child: Row(
padding: baseInfoEdgeInsets.copyWith( mainAxisSize: MainAxisSize.max,
top: 4, mainAxisAlignment: MainAxisAlignment.spaceBetween,
bottom: 8, children: [
right: 8, Flexible(
), flex: 1,
child: Row( child: TooltipText(
mainAxisSize: MainAxisSize.max, text: Text(
mainAxisAlignment: MainAxisAlignment.spaceBetween, appLocalizations.options,
children: [ maxLines: 1,
Flexible( overflow: TextOverflow.ellipsis,
flex: 1, style: Theme.of(context)
child: TooltipText( .textTheme
text: Text( .titleSmall
appLocalizations.options, ?.adjustSize(-2)
maxLines: 1, .toLight,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
),
), ),
), ),
Selector<ClashConfig, bool>( ),
selector: (_, clashConfig) => clashConfig.tun.enable, Consumer(
builder: (_, enable, __) { builder: (_, ref, __) {
return Switch( final enable = ref.watch(patchClashConfigProvider
value: enable, .select((state) => state.tun.enable));
onChanged: (value) { return Switch(
final clashConfig = value: enable,
globalState.appController.clashConfig; onChanged: (value) {
clashConfig.tun = clashConfig.tun.copyWith( ref.read(patchClashConfigProvider.notifier).updateState(
enable: value, (state) => state.copyWith.tun(
); enable: value,
}, ),
); );
}, },
) );
], },
), )
],
), ),
), ),
), ),
@@ -87,67 +85,68 @@ class SystemProxyButton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: getWidgetHeight(1), height: getWidgetHeight(1),
child: LocaleBuilder( child: CommonCard(
builder: (_) => CommonCard( onPressed: () {
onPressed: () { showSheet(
showSheet( context: context,
context: context, body: generateListView(
body: generateListView( generateSection(
generateSection( items: [
items: [ SystemProxyItem(),
SystemProxyItem(), BypassDomainItem(),
BypassDomainItem(), ],
],
),
), ),
title: appLocalizations.systemProxy,
);
},
info: Info(
label: appLocalizations.systemProxy,
iconData: Icons.shuffle,
),
child: Container(
padding: baseInfoEdgeInsets.copyWith(
top: 4,
bottom: 8,
right: 8,
), ),
child: Row( title: appLocalizations.systemProxy,
mainAxisSize: MainAxisSize.max, );
mainAxisAlignment: MainAxisAlignment.spaceBetween, },
children: [ info: Info(
Flexible( label: appLocalizations.systemProxy,
flex: 1, iconData: Icons.shuffle,
child: TooltipText( ),
text: Text( child: Container(
appLocalizations.options, padding: baseInfoEdgeInsets.copyWith(
maxLines: 1, top: 4,
overflow: TextOverflow.ellipsis, bottom: 8,
style: Theme.of(context) right: 8,
.textTheme ),
.titleSmall child: Row(
?.adjustSize(-2) mainAxisSize: MainAxisSize.max,
.toLight, mainAxisAlignment: MainAxisAlignment.spaceBetween,
), children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
appLocalizations.options,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleSmall
?.adjustSize(-2)
.toLight,
), ),
), ),
Selector<Config, bool>( ),
selector: (_, config) => config.networkProps.systemProxy, Consumer(
builder: (_, systemProxy, __) { builder: (_, ref, __) {
return Switch( final systemProxy = ref.watch(networkSettingProvider
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, .select((state) => state.systemProxy));
value: systemProxy, return Switch(
onChanged: (value) { materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
final config = globalState.appController.config; value: systemProxy,
config.networkProps = onChanged: (value) {
config.networkProps.copyWith(systemProxy: value); ref.read(networkSettingProvider.notifier).updateState(
}, (state) => state.copyWith(
); systemProxy: value,
}, ),
) );
], },
), );
},
)
],
), ),
), ),
), ),

View File

@@ -1,8 +1,9 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class StartButton extends StatefulWidget { class StartButton extends StatefulWidget {
const StartButton({super.key}); const StartButton({super.key});
@@ -19,7 +20,7 @@ class _StartButtonState extends State<StartButton>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
isStart = globalState.appController.appFlowingState.isStart; isStart = globalState.appState.runTime != null;
_controller = AnimationController( _controller = AnimationController(
vsync: this, vsync: this,
value: isStart ? 1 : 0, value: isStart ? 1 : 0,
@@ -34,11 +35,10 @@ class _StartButtonState extends State<StartButton>
} }
handleSwitchStart() { handleSwitchStart() {
final appController = globalState.appController; if (isStart == globalState.appState.isStart) {
if (isStart == appController.appFlowingState.isStart) {
isStart = !isStart; isStart = !isStart;
updateController(); updateController();
appController.updateStatus(isStart); globalState.appController.updateStatus(isStart);
} }
} }
@@ -50,31 +50,24 @@ class _StartButtonState extends State<StartButton>
} }
} }
Widget _updateControllerContainer(Widget child) {
return Selector<AppFlowingState, bool>(
selector: (_, appFlowingState) => appFlowingState.isStart,
builder: (_, isStart, child) {
if (isStart != this.isStart) {
this.isStart = isStart;
updateController();
}
return child!;
},
child: child,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector2<AppState, Config, StartButtonSelectorState>( return Consumer(
selector: (_, appState, config) => StartButtonSelectorState( builder: (_, ref, child) {
isInit: appState.isInit, final state = ref.watch(startButtonSelectorStateProvider);
hasProfile: config.profiles.isNotEmpty,
),
builder: (_, state, child) {
if (!state.isInit || !state.hasProfile) { if (!state.isInit || !state.hasProfile) {
return Container(); return Container();
} }
ref.listenManual(
runTimeProvider.select((state) => state != null),
(prev, next) {
if (next != isStart) {
isStart = next;
updateController();
}
},
fireImmediately: true,
);
final textWidth = globalState.measure final textWidth = globalState.measure
.computeTextSize( .computeTextSize(
Text( Text(
@@ -86,53 +79,51 @@ class _StartButtonState extends State<StartButton>
) )
.width + .width +
16; 16;
return _updateControllerContainer( return AnimatedBuilder(
AnimatedBuilder( animation: _controller.view,
animation: _controller.view, builder: (_, child) {
builder: (_, child) { return SizedBox(
return SizedBox( width: 56 + textWidth * _controller.value,
width: 56 + textWidth * _controller.value, height: 56,
height: 56, child: FloatingActionButton(
child: FloatingActionButton( heroTag: null,
heroTag: null, onPressed: () {
onPressed: () { handleSwitchStart();
handleSwitchStart(); },
}, child: Row(
child: Row( children: [
children: [ Container(
Container( width: 56,
width: 56, height: 56,
height: 56, alignment: Alignment.center,
alignment: Alignment.center, child: AnimatedIcon(
child: AnimatedIcon( icon: AnimatedIcons.play_pause,
icon: AnimatedIcons.play_pause, progress: _controller,
progress: _controller,
),
), ),
Expanded( ),
child: ClipRect( Expanded(
child: OverflowBox( child: ClipRect(
maxWidth: textWidth, child: OverflowBox(
child: Container( maxWidth: textWidth,
alignment: Alignment.centerLeft, child: Container(
child: child!, alignment: Alignment.centerLeft,
), child: child!,
), ),
), ),
), ),
], ),
), ],
), ),
); ),
}, );
child: child, },
), child: child,
); );
}, },
child: Selector<AppFlowingState, int?>( child: Consumer(
selector: (_, appFlowingState) => appFlowingState.runTime, builder: (_, ref, __) {
builder: (_, int? value, __) { final runTime = ref.watch(runTimeProvider);
final text = other.getTimeText(value); final text = other.getTimeText(runTime);
return Text( return Text(
text, text,
style: Theme.of(context).textTheme.titleMedium?.toSoftBold, style: Theme.of(context).textTheme.titleMedium?.toSoftBold,

View File

@@ -2,10 +2,11 @@ import 'dart:math';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class TrafficUsage extends StatelessWidget { class TrafficUsage extends StatelessWidget {
const TrafficUsage({super.key}); const TrafficUsage({super.key});
@@ -62,9 +63,9 @@ class TrafficUsage extends StatelessWidget {
iconData: Icons.data_saver_off, iconData: Icons.data_saver_off,
), ),
onPressed: () {}, onPressed: () {},
child: Selector<AppFlowingState, Traffic>( child: Consumer(
selector: (_, appFlowingState) => appFlowingState.totalTraffic, builder: (_, ref, __) {
builder: (_, totalTraffic, __) { final totalTraffic = ref.watch(totalTrafficProvider);
final upTotalTrafficValue = totalTraffic.up; final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down; final downTotalTrafficValue = totalTraffic.down;
return Padding( return Padding(

View File

@@ -3,11 +3,11 @@ export 'dashboard/dashboard.dart';
export 'tools.dart'; export 'tools.dart';
export 'profiles/profiles.dart'; export 'profiles/profiles.dart';
export 'logs.dart'; export 'logs.dart';
export 'connections.dart';
export 'access.dart'; export 'access.dart';
export 'config/config.dart'; export 'config/config.dart';
export 'application_setting.dart'; export 'application_setting.dart';
export 'about.dart'; export 'about.dart';
export 'backup_and_recovery.dart'; export 'backup_and_recovery.dart';
export 'resources.dart'; export 'resources.dart';
export 'requests.dart'; export 'connection/requests.dart';
export 'connection/connections.dart';

View File

@@ -1,13 +1,14 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/card.dart'; import 'package:fl_clash/widgets/card.dart';
import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/list.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
extension IntlExt on Intl { extension IntlExt on Intl {
static actionMessage(String messageText) => static actionMessage(String messageText) =>
@@ -38,29 +39,20 @@ class HotKeyFragment extends StatelessWidget {
itemCount: HotAction.values.length, itemCount: HotAction.values.length,
itemBuilder: (_, index) { itemBuilder: (_, index) {
final hotAction = HotAction.values[index]; final hotAction = HotAction.values[index];
return Selector<Config, HotKeyAction>( return Consumer(
selector: (_, config) { builder: (_, ref, __) {
final index = config.hotKeyActions.indexWhere( final hotKeyAction = ref.watch(getHotKeyActionProvider(hotAction));
(item) => item.action == hotAction,
);
return index != -1
? config.hotKeyActions[index]
: HotKeyAction(
action: hotAction,
);
},
builder: (_, value, __) {
return ListItem( return ListItem(
title: Text(IntlExt.actionMessage(hotAction.name)), title: Text(IntlExt.actionMessage(hotAction.name)),
subtitle: Text( subtitle: Text(
getSubtitle(value), getSubtitle(hotKeyAction),
style: context.textTheme.bodyMedium style: context.textTheme.bodyMedium
?.copyWith(color: context.colorScheme.primary), ?.copyWith(color: context.colorScheme.primary),
), ),
onTap: () { onTap: () {
globalState.showCommonDialog( globalState.showCommonDialog(
child: HotKeyRecorder( child: HotKeyRecorder(
hotKeyAction: value, hotKeyAction: hotKeyAction,
), ),
); );
}, },
@@ -121,8 +113,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
_handleRemove() { _handleRemove() {
Navigator.of(context).pop(); Navigator.of(context).pop();
final config = globalState.appController.config; globalState.appController.updateOrAddHotKeyAction(
config.updateOrAddHotKeyAction(
hotKeyActionNotifier.value.copyWith( hotKeyActionNotifier.value.copyWith(
modifiers: {}, modifiers: {},
key: null, key: null,
@@ -132,7 +123,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
_handleConfirm() { _handleConfirm() {
Navigator.of(context).pop(); Navigator.of(context).pop();
final config = globalState.appController.config; final config = globalState.config;
final currentHotkeyAction = hotKeyActionNotifier.value; final currentHotkeyAction = hotKeyActionNotifier.value;
if (currentHotkeyAction.key == null || if (currentHotkeyAction.key == null ||
currentHotkeyAction.modifiers.isEmpty) { currentHotkeyAction.modifiers.isEmpty) {
@@ -158,7 +149,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
); );
return; return;
} }
config.updateOrAddHotKeyAction( globalState.appController.updateOrAddHotKeyAction(
currentHotkeyAction, currentHotkeyAction,
); );
} }

View File

@@ -1,61 +1,106 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../widgets/widgets.dart'; import '../widgets/widgets.dart';
class LogsFragment extends StatefulWidget { double _preOffset = 0;
class LogsFragment extends ConsumerStatefulWidget {
const LogsFragment({super.key}); const LogsFragment({super.key});
@override @override
State<LogsFragment> createState() => _LogsFragmentState(); ConsumerState<LogsFragment> createState() => _LogsFragmentState();
} }
class _LogsFragmentState extends State<LogsFragment> { class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final logsNotifier = ValueNotifier<LogsAndKeywords>(const LogsAndKeywords()); final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
final scrollController = ScrollController( final _scrollController = ScrollController(
keepScrollOffset: false, initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
); );
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
double _currentMaxWidth = 0;
Timer? timer; List<Log> _logs = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { _logsStateNotifier.value = _logsStateNotifier.value.copyWith(
final appFlowingState = globalState.appController.appFlowingState; logs: globalState.appState.logs.list,
logsNotifier.value = );
logsNotifier.value.copyWith(logs: appFlowingState.logs); ref.listenManual(
if (timer != null) { logsProvider.select((state) => state.list),
timer?.cancel(); (prev, next) {
timer = null; if (prev != next) {
} final isEquality = logListEquality.equals(prev, next);
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) { if (!isEquality) {
final logs = appFlowingState.logs; _logs = next;
if (!logListEquality.equals( updateLogsThrottler();
logsNotifier.value.logs, }
logs,
)) {
logsNotifier.value = logsNotifier.value.copyWith(
logs: logs,
);
} }
}); },
}); fireImmediately: true,
);
ref.listenManual(
isCurrentPageProvider(
PageLabel.logs,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
);
} }
@override
List<Widget> get actions => [
IconButton(
onPressed: () {
_handleExport();
},
icon: const Icon(
Icons.file_download_outlined,
),
),
];
@override
get onSearch => (value) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
query: value,
);
};
@override
get onKeywordsUpdate => (keywords) {
_logsStateNotifier.value =
_logsStateNotifier.value.copyWith(keywords: keywords);
};
@override @override
void dispose() { void dispose() {
timer?.cancel(); _logsStateNotifier.dispose();
logsNotifier.dispose(); _scrollController.dispose();
scrollController.dispose(); _cacheDynamicHeightMap.clear();
timer = null;
super.dispose(); super.dispose();
} }
_handleTryClearCache(double maxWidth) {
if (_currentMaxWidth != maxWidth) {
_currentMaxWidth = maxWidth;
_cacheDynamicHeightMap.clear();
}
}
_handleExport() async { _handleExport() async {
final commonScaffoldState = context.commonScaffoldState; final commonScaffoldState = context.commonScaffoldState;
final res = await commonScaffoldState?.loadingRun<bool>( final res = await commonScaffoldState?.loadingRun<bool>(
@@ -71,295 +116,115 @@ class _LogsFragmentState extends State<LogsFragment> {
); );
} }
_initActions() { double _calcCacheHeight(String text) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { final cacheHeight = _cacheDynamicHeightMap.get(text);
final commonScaffoldState = if (cacheHeight != null) {
context.findAncestorStateOfType<CommonScaffoldState>(); return cacheHeight;
commonScaffoldState?.actions = [ }
IconButton( final size = globalState.measure.computeTextSize(
onPressed: () { Text(
showSearch( text,
context: context, style: globalState.appController.context.textTheme.bodyLarge,
delegate: LogsSearchDelegate( ),
logs: logsNotifier.value, maxWidth: _currentMaxWidth,
), );
); _cacheDynamicHeightMap.put(text, size.height);
}, return size.height;
icon: const Icon(Icons.search),
),
IconButton(
onPressed: () {
_handleExport();
},
icon: const Icon(
Icons.file_download_outlined,
),
),
];
});
} }
_addKeyword(String keyword) { double _getItemHeight(Log log) {
final isContains = logsNotifier.value.keywords.contains(keyword); final measure = globalState.measure;
if (isContains) return; final bodySmallHeight = measure.bodySmallHeight;
final keywords = List<String>.from(logsNotifier.value.keywords) final bodyMediumHeight = measure.bodyMediumHeight;
..add(keyword); final height = _calcCacheHeight(log.payload ?? "");
logsNotifier.value = logsNotifier.value.copyWith( return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
keywords: keywords,
);
} }
_deleteKeyword(String keyword) { updateLogsThrottler() {
final isContains = logsNotifier.value.keywords.contains(keyword); throttler.call("logs", () {
if (!isContains) return; final isEquality = logListEquality.equals(
final keywords = List<String>.from(logsNotifier.value.keywords) _logs,
..remove(keyword); _logsStateNotifier.value.logs,
logsNotifier.value = logsNotifier.value.copyWith( );
keywords: keywords, if (isEquality) {
); return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: _logs,
);
});
}, duration: commonDuration);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<AppState, bool?>( return LayoutBuilder(
selector: (_, appState) => builder: (_, constraints) {
appState.currentLabel == 'logs' || _handleTryClearCache(constraints.maxWidth - 40);
appState.viewMode == ViewMode.mobile && return Align(
appState.currentLabel == "tools", alignment: Alignment.topCenter,
builder: (_, isCurrent, child) { child: ValueListenableBuilder<LogsState>(
if (isCurrent == null || isCurrent) { valueListenable: _logsStateNotifier,
_initActions(); builder: (_, state, __) {
} final logs = state.list;
return child!; if (logs.isEmpty) {
}, return NullStatus(
child: ValueListenableBuilder<LogsAndKeywords>( label: appLocalizations.nullLogsDesc,
valueListenable: logsNotifier, );
builder: (_, state, __) { }
final logs = state.filteredLogs; final items = logs
if (logs.isEmpty) { .map<Widget>(
return NullStatus( (log) => LogItem(
label: appLocalizations.nullLogsDesc, key: Key(log.dateTime.toString()),
); log: log,
} onClick: (value) {
final reversedLogs = logs.reversed.toList(); context.commonScaffoldState?.addKeyword(value);
final logWidgets = reversedLogs },
.map<Widget>( ),
(log) => LogItem( )
key: Key(log.dateTime.toString()), .separated(
log: log, const Divider(
onClick: _addKeyword, height: 0,
), ),
) )
.separated( .toList();
const Divider( return NotificationListener<ScrollEndNotification>(
height: 0, onNotification: (details) {
), _preOffset = details.metrics.pixels;
) return false;
.toList(); },
return Column( child: CommonScrollBar(
crossAxisAlignment: CrossAxisAlignment.start, controller: _scrollController,
children: [ child: ListView.builder(
if (state.keywords.isNotEmpty) reverse: true,
Padding( shrinkWrap: true,
padding: const EdgeInsets.symmetric( physics: NextClampingScrollPhysics(),
horizontal: 16, controller: _scrollController,
vertical: 16, itemBuilder: (_, index) {
), return items[index];
child: Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: LayoutBuilder(
builder: (_, constraints) {
return ScrollConfiguration(
behavior: ShowBarScrollBehavior(),
child: ListView.builder(
controller: scrollController,
itemExtentBuilder: (index, __) {
final widget = logWidgets[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyLargeSize = measure.bodyLargeSize;
final bodySmallHeight = measure.bodySmallHeight;
final bodyMediumHeight = measure.bodyMediumHeight;
final log = reversedLogs[(index / 2).floor()];
final width = (log.payload?.length ?? 0) *
bodyLargeSize.width +
200;
final lines = (width / constraints.maxWidth).ceil();
return lines * bodyLargeSize.height +
bodySmallHeight +
8 +
bodyMediumHeight +
40;
},
itemBuilder: (_, index) {
return logWidgets[index];
},
itemCount: logWidgets.length,
));
},
),
)
],
);
},
),
);
}
}
class LogsSearchDelegate extends SearchDelegate {
ValueNotifier<LogsAndKeywords> logsNotifier;
LogsSearchDelegate({
required LogsAndKeywords logs,
}) : logsNotifier = ValueNotifier(logs);
@override
void dispose() {
logsNotifier.dispose();
super.dispose();
}
get state => logsNotifier.value;
List<Log> get _results {
final lowQuery = query.toLowerCase();
return logsNotifier.value.filteredLogs
.where(
(log) =>
(log.payload?.toLowerCase().contains(lowQuery) ?? false) ||
log.logLevel.name.contains(lowQuery),
)
.toList();
}
@override
List<Widget>? buildActions(BuildContext context) {
return [
IconButton(
onPressed: () {
if (query.isEmpty) {
close(context, null);
return;
}
query = '';
},
icon: const Icon(Icons.clear),
),
const SizedBox(
width: 8,
)
];
}
@override
Widget? buildLeading(BuildContext context) {
return IconButton(
onPressed: () {
close(context, null);
},
icon: const Icon(Icons.arrow_back),
);
}
@override
Widget buildResults(BuildContext context) {
return buildSuggestions(context);
}
_addKeyword(String keyword) {
final isContains = logsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(logsNotifier.value.keywords)
..add(keyword);
logsNotifier.value = logsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = logsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(logsNotifier.value.keywords)
..remove(keyword);
logsNotifier.value = logsNotifier.value.copyWith(
keywords: keywords,
);
}
@override
Widget buildSuggestions(BuildContext context) {
return ValueListenableBuilder(
valueListenable: logsNotifier,
builder: (_, __, ___) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.keywords.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
itemBuilder: (_, index) {
final log = _results[index];
return LogItem(
key: Key(log.dateTime.toString()),
log: log,
onClick: (value) {
_addKeyword(value);
}, },
); itemExtentBuilder: (index, __) {
}, final item = items[index];
separatorBuilder: (BuildContext context, int index) { if (item.runtimeType == Divider) {
return const Divider( return 0;
height: 0, }
); final log = logs[(index / 2).floor()];
}, return _getItemHeight(log);
itemCount: _results.length, },
), itemCount: items.length,
) ),
], ),
);
},
),
); );
}, },
); );
} }
} }
class LogItem extends StatefulWidget { class LogItem extends StatelessWidget {
final Log log; final Log log;
final Function(String)? onClick; final Function(String)? onClick;
@@ -369,14 +234,8 @@ class LogItem extends StatefulWidget {
this.onClick, this.onClick,
}); });
@override
State<LogItem> createState() => _LogItemState();
}
class _LogItemState extends State<LogItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final log = widget.log;
return ListItem( return ListItem(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
@@ -384,14 +243,16 @@ class _LogItemState extends State<LogItem> {
), ),
title: SelectableText( title: SelectableText(
log.payload ?? '', log.payload ?? '',
style: context.textTheme.bodyLarge,
), ),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SelectableText( SelectableText(
"${log.dateTime}", "${log.dateTime}",
style: context.textTheme.bodySmall style: context.textTheme.bodySmall?.copyWith(
?.copyWith(color: context.colorScheme.primary), color: context.colorScheme.primary,
),
), ),
const SizedBox( const SizedBox(
height: 8, height: 8,
@@ -400,8 +261,8 @@ class _LogItemState extends State<LogItem> {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: CommonChip( child: CommonChip(
onPressed: () { onPressed: () {
if (widget.onClick == null) return; if (onClick == null) return;
widget.onClick!(log.logLevel.name); onClick!(log.logLevel.name);
}, },
label: log.logLevel.name, label: log.logLevel.name,
), ),
@@ -411,3 +272,11 @@ class _LogItemState extends State<LogItem> {
); );
} }
} }
class NoGlowScrollBehavior extends ScrollBehavior {
@override
Widget buildOverscrollIndicator(
BuildContext context, Widget child, ScrollableDetails details) {
return child; // 禁用过度滚动效果
}
}

View File

@@ -0,0 +1,54 @@
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
class CustomProfile extends StatefulWidget {
final String profileId;
const CustomProfile({
super.key,
required this.profileId,
});
@override
State<CustomProfile> createState() => _CustomProfileState();
}
class _CustomProfileState extends State<CustomProfile> {
final _currentClashConfigNotifier = ValueNotifier<ClashConfig?>(null);
@override
void initState() {
super.initState();
_initCurrentClashConfig();
}
_initCurrentClashConfig() async {
// final currentProfileId = globalState.config.currentProfileId;
// if (currentProfileId == null) {
// return;
// }
// _currentClashConfigNotifier.value =
// await clashCore.getProfile(currentProfileId);
}
@override
Widget build(BuildContext context) {
return CommonScaffold(
body: ValueListenableBuilder(
valueListenable: _currentClashConfigNotifier,
builder: (_, clashConfig, ___) {
if (clashConfig == null) {
return Center(
child: CircularProgressIndicator(),
);
}
return Column(
children: [],
);
},
),
title: "自定义",
);
}
}

View File

@@ -4,11 +4,11 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart'; import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
import 'add_profile.dart'; import 'add_profile.dart';
@@ -19,7 +19,7 @@ class ProfilesFragment extends StatefulWidget {
State<ProfilesFragment> createState() => _ProfilesFragmentState(); State<ProfilesFragment> createState() => _ProfilesFragmentState();
} }
class _ProfilesFragmentState extends State<ProfilesFragment> { class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
Function? applyConfigDebounce; Function? applyConfigDebounce;
_handleShowAddExtendPage() { _handleShowAddExtendPage() {
@@ -33,21 +33,19 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
} }
_updateProfiles() async { _updateProfiles() async {
final appController = globalState.appController; final profiles = globalState.config.profiles;
final config = appController.config;
final profiles = appController.config.profiles;
final messages = []; final messages = [];
final updateProfiles = profiles.map<Future>( final updateProfiles = profiles.map<Future>(
(profile) async { (profile) async {
if (profile.type == ProfileType.file) return; if (profile.type == ProfileType.file) return;
config.setProfile( globalState.appController.setProfile(
profile.copyWith(isUpdating: true), profile.copyWith(isUpdating: true),
); );
try { try {
await appController.updateProfile(profile); await globalState.appController.updateProfile(profile);
} catch (e) { } catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n"); messages.add("${profile.label ?? profile.id}: $e \n");
config.setProfile( globalState.appController.setProfile(
profile.copyWith( profile.copyWith(
isUpdating: false, isUpdating: false,
), ),
@@ -70,97 +68,90 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
} }
} }
_initScaffold() { @override
WidgetsBinding.instance.addPostFrameCallback( List<Widget> get actions => [
(_) { IconButton(
if (!mounted) return; onPressed: () {
final commonScaffoldState = _updateProfiles();
context.findAncestorStateOfType<CommonScaffoldState>(); },
commonScaffoldState?.actions = [ icon: const Icon(Icons.sync),
IconButton( ),
onPressed: () { IconButton(
_updateProfiles(); onPressed: () {
}, final profiles = globalState.config.profiles;
icon: const Icon(Icons.sync), showSheet(
), title: appLocalizations.profilesSort,
IconButton( context: context,
onPressed: () { body: SizedBox(
final profiles = globalState.appController.config.profiles; height: 400,
showSheet( child: ReorderableProfiles(profiles: profiles),
title: appLocalizations.profilesSort, ),
context: context, );
body: SizedBox( },
height: 400, icon: const Icon(Icons.sort),
child: ReorderableProfiles(profiles: profiles), iconSize: 26,
), ),
); ];
},
icon: const Icon(Icons.sort), @override
iconSize: 26, Widget? get floatingActionButton => FloatingActionButton(
), heroTag: null,
]; onPressed: _handleShowAddExtendPage,
commonScaffoldState?.floatingActionButton = FloatingActionButton( child: const Icon(
heroTag: null, Icons.add,
onPressed: _handleShowAddExtendPage, ),
child: const Icon( );
Icons.add,
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActiveBuilder( return Consumer(
label: "profiles", builder: (_, ref, __) {
builder: (isCurrent, child) { ref.listenManual(
if (isCurrent) { isCurrentPageProvider(PageLabel.profiles),
_initScaffold(); (prev, next) {
} if (prev != next && next == true) {
return child!; initPageState();
}, }
child: Selector2<AppState, Config, ProfilesSelectorState>( },
selector: (_, appState, config) => ProfilesSelectorState( fireImmediately: true,
profiles: config.profiles, );
currentProfileId: config.currentProfileId, final profilesSelectorState = ref.watch(profilesSelectorStateProvider);
columns: other.getProfilesColumns(appState.viewWidth), if (profilesSelectorState.profiles.isEmpty) {
), return NullStatus(
builder: (context, state, child) { label: appLocalizations.nullProfileDesc,
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
),
); );
}, }
), return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: profilesSelectorState.columns,
children: [
for (int i = 0; i < profilesSelectorState.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(profilesSelectorState.profiles[i].id),
profile: profilesSelectorState.profiles[i],
groupValue: profilesSelectorState.currentProfileId,
onChanged: (profileId) {
ref.read(currentProfileIdProvider.notifier).value =
profileId;
},
),
),
],
),
),
);
},
); );
} }
} }
@@ -196,18 +187,17 @@ class ProfileItem extends StatelessWidget {
Future updateProfile() async { Future updateProfile() async {
final appController = globalState.appController; final appController = globalState.appController;
final config = appController.config;
if (profile.type == ProfileType.file) return; if (profile.type == ProfileType.file) return;
await globalState.safeRun(silence: false, () async { await globalState.safeRun(silence: false, () async {
try { try {
config.setProfile( appController.setProfile(
profile.copyWith( profile.copyWith(
isUpdating: true, isUpdating: true,
), ),
); );
await appController.updateProfile(profile); await appController.updateProfile(profile);
} catch (e) { } catch (e) {
config.setProfile( appController.setProfile(
profile.copyWith( profile.copyWith(
isUpdating: false, isUpdating: false,
), ),
@@ -257,16 +247,16 @@ class ProfileItem extends StatelessWidget {
]; ];
} }
_handleCopyLink(BuildContext context) async { // _handleCopyLink(BuildContext context) async {
await Clipboard.setData( // await Clipboard.setData(
ClipboardData( // ClipboardData(
text: profile.url, // text: profile.url,
), // ),
); // );
if (context.mounted) { // if (context.mounted) {
context.showNotifier(appLocalizations.copySuccess); // context.showNotifier(appLocalizations.copySuccess);
} // }
} // }
_handleExportFile(BuildContext context) async { _handleExportFile(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState; final commonScaffoldState = context.commonScaffoldState;
@@ -287,8 +277,18 @@ class ProfileItem extends StatelessWidget {
} }
} }
// _handlePushCustomPage(BuildContext context, String id) {
// BaseNavigator.push(
// context,
// CustomProfile(
// profileId: id,
// ),
// );
// }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final key = GlobalKey<CommonPopupBoxState>();
return CommonCard( return CommonCard(
isSelected: profile.id == groupValue, isSelected: profile.id == groupValue,
onPressed: () { onPressed: () {
@@ -308,6 +308,7 @@ class ProfileItem extends StatelessWidget {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
) )
: CommonPopupBox( : CommonPopupBox(
key: key,
popup: CommonPopupMenu( popup: CommonPopupMenu(
items: [ items: [
ActionItemData( ActionItemData(
@@ -325,14 +326,21 @@ class ProfileItem extends StatelessWidget {
_handleUpdateProfile(); _handleUpdateProfile();
}, },
), ),
ActionItemData( // ActionItemData(
icon: Icons.copy, // icon: Icons.copy,
label: appLocalizations.copyLink, // label: appLocalizations.copyLink,
onPressed: () { // onPressed: () {
_handleCopyLink(context); // _handleCopyLink(context);
}, // },
), // ),
], ],
// ActionItemData(
// icon: Icons.extension_outlined,
// label: "自定义",
// onPressed: () {
// _handlePushCustomPage(context, profile.id);
// },
// ),
ActionItemData( ActionItemData(
icon: Icons.file_copy_outlined, icon: Icons.file_copy_outlined,
label: appLocalizations.exportFile, label: appLocalizations.exportFile,
@@ -352,7 +360,9 @@ class ProfileItem extends StatelessWidget {
], ],
), ),
target: IconButton( target: IconButton(
onPressed: () {}, onPressed: () {
key.currentState?.pop();
},
icon: Icon(Icons.more_vert), icon: Icon(Icons.more_vert),
), ),
), ),
@@ -496,7 +506,7 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
child: FilledButton.tonal( child: FilledButton.tonal(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
globalState.appController.config.profiles = profiles; globalState.appController.setProfiles(profiles);
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all( padding: WidgetStateProperty.all(

View File

@@ -2,10 +2,11 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/proxies/common.dart'; import 'package:fl_clash/fragments/proxies/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class ProxyCard extends StatelessWidget { class ProxyCard extends StatelessWidget {
final String groupName; final String groupName;
@@ -35,30 +36,28 @@ class ProxyCard extends StatelessWidget {
Widget _buildDelayText() { Widget _buildDelayText() {
return SizedBox( return SizedBox(
height: measure.labelSmallHeight, height: measure.labelSmallHeight,
child: Selector<AppState, int?>( child: Consumer(
selector: (context, appState) => builder: (context, ref, __) {
globalState.appController.getDelay(proxy.name,testUrl), final delay = ref.watch(getDelayProvider(
builder: (context, delay, __) { proxyName: proxy.name,
return FadeBox( testUrl: testUrl,
child: Builder( ));
builder: (_) { return delay == 0 || delay == null
if (delay == 0 || delay == null) { ? SizedBox(
return SizedBox( height: measure.labelSmallHeight,
height: measure.labelSmallHeight, width: measure.labelSmallHeight,
width: measure.labelSmallHeight, child: delay == 0
child: delay == 0 ? const CircularProgressIndicator(
? const CircularProgressIndicator( strokeWidth: 2,
strokeWidth: 2, )
) : IconButton(
: IconButton( icon: const Icon(Icons.bolt),
icon: const Icon(Icons.bolt), iconSize: globalState.measure.labelSmallHeight,
iconSize: globalState.measure.labelSmallHeight, padding: EdgeInsets.zero,
padding: EdgeInsets.zero, onPressed: _handleTestCurrentDelay,
onPressed: _handleTestCurrentDelay, ),
), )
); : GestureDetector(
}
return GestureDetector(
onTap: _handleTestCurrentDelay, onTap: _handleTestCurrentDelay,
child: Text( child: Text(
delay > 0 ? '$delay ms' : "Timeout", delay > 0 ? '$delay ms' : "Timeout",
@@ -70,9 +69,6 @@ class ProxyCard extends StatelessWidget {
), ),
), ),
); );
},
),
);
}, },
), ),
); );
@@ -102,18 +98,17 @@ class ProxyCard extends StatelessWidget {
} }
} }
_changeProxy(BuildContext context) async { _changeProxy(WidgetRef ref) async {
final appController = globalState.appController; final isComputedSelected = groupType.isComputedSelected;
final isURLTestOrFallback = groupType.isURLTestOrFallback;
final isSelector = groupType == GroupType.Selector; final isSelector = groupType == GroupType.Selector;
if (isURLTestOrFallback || isSelector) { if (isComputedSelected || isSelector) {
final currentProxyName = final currentProxyName = ref.read(getProxyNameProvider(groupName));
appController.config.currentSelectedMap[groupName]; final nextProxyName = switch (isComputedSelected) {
final nextProxyName = switch (isURLTestOrFallback) {
true => currentProxyName == proxy.name ? "" : proxy.name, true => currentProxyName == proxy.name ? "" : proxy.name,
false => proxy.name, false => proxy.name,
}; };
appController.config.updateCurrentSelectedMap( final appController = globalState.appController;
appController.updateCurrentSelectedMap(
groupName, groupName,
nextProxyName, nextProxyName,
); );
@@ -130,108 +125,136 @@ class ProxyCard extends StatelessWidget {
final measure = globalState.measure; final measure = globalState.measure;
final delayText = _buildDelayText(); final delayText = _buildDelayText();
final proxyNameText = _buildProxyNameText(context); final proxyNameText = _buildProxyNameText(context);
return currentSelectedProxyNameBuilder( return Stack(
groupName: groupName, children: [
builder: (currentGroupName) { Consumer(
return Stack( builder: (_, ref, child) {
children: [ final selectedProxyName =
CommonCard( ref.watch(getSelectedProxyNameProvider(groupName));
return CommonCard(
key: key, key: key,
enterAnimated: true,
onPressed: () { onPressed: () {
_changeProxy(context); _changeProxy(ref);
}, },
isSelected: currentGroupName == proxy.name, isSelected: selectedProxyName == proxy.name,
child: Container( child: child!,
padding: const EdgeInsets.all(12), );
child: Column( },
mainAxisSize: MainAxisSize.min, child: Container(
crossAxisAlignment: CrossAxisAlignment.start, alignment: Alignment.centerLeft,
children: [ padding: const EdgeInsets.symmetric(horizontal: 12),
proxyNameText, child: Column(
const SizedBox( mainAxisSize: MainAxisSize.min,
height: 8, crossAxisAlignment: CrossAxisAlignment.start,
children: [
proxyNameText,
const SizedBox(
height: 8,
),
if (type == ProxyCardType.expand) ...[
SizedBox(
height: measure.bodySmallHeight,
child: _ProxyDesc(
proxy: proxy,
), ),
if (type == ProxyCardType.expand) ...[ ),
SizedBox( const SizedBox(
height: measure.bodySmallHeight, height: 6,
child: Selector<AppState, String>( ),
selector: (context, appState) => appState.getDesc( delayText,
proxy.type, ] else
proxy.name, SizedBox(
), height: measure.bodySmallHeight,
builder: (_, desc, __) { child: Row(
return EmojiText( mainAxisAlignment: MainAxisAlignment.spaceBetween,
desc, crossAxisAlignment: CrossAxisAlignment.center,
overflow: TextOverflow.ellipsis, children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
proxy.type,
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: color:
context.textTheme.bodySmall?.color?.toLight, context.textTheme.bodySmall?.color?.toLight,
), ),
);
},
),
),
const SizedBox(
height: 8,
),
delayText,
] else
SizedBox(
height: measure.bodySmallHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
flex: 1,
child: TooltipText(
text: Text(
proxy.type,
style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis,
color: context
.textTheme.bodySmall?.color?.toLight,
),
),
),
), ),
delayText, ),
],
), ),
), delayText,
], ],
),
),
),
if (groupType.isURLTestOrFallback)
Selector<Config, String>(
selector: (_, config) {
final selectedProxyName =
config.currentSelectedMap[groupName];
return selectedProxyName ?? '';
},
builder: (_, value, child) {
if (value != proxy.name) return Container();
return child!;
},
child: Positioned.fill(
child: Container(
alignment: Alignment.topRight,
margin: const EdgeInsets.all(8),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.secondaryContainer,
),
child: const SelectIcon(),
), ),
), ),
), ],
) ),
], ),
); ),
}, if (groupType.isComputedSelected)
Positioned(
top: 0,
right: 0,
child: _ProxyComputedMark(
groupName: groupName,
proxy: proxy,
),
)
],
);
}
}
class _ProxyDesc extends ConsumerWidget {
final Proxy proxy;
const _ProxyDesc({
required this.proxy,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final desc = ref.watch(
getProxyDescProvider(proxy),
);
return EmojiText(
desc,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color: context.textTheme.bodySmall?.color?.toLight,
),
);
}
}
class _ProxyComputedMark extends ConsumerWidget {
final String groupName;
final Proxy proxy;
const _ProxyComputedMark({
required this.groupName,
required this.proxy,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final proxyName = ref.watch(
getProxyNameProvider(groupName),
);
if (proxyName != proxy.name) {
return SizedBox();
}
return Container(
alignment: Alignment.topRight,
margin: const EdgeInsets.all(8),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.secondaryContainer,
),
child: const SelectIcon(),
),
); );
} }
} }

View File

@@ -3,24 +3,6 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Widget currentSelectedProxyNameBuilder({
required String groupName,
required Widget Function(String currentGroupName) builder,
}) {
return Selector2<AppState, Config, String>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName);
final selectedProxyName = config.currentSelectedMap[groupName];
return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? "";
},
builder: (_, currentSelectedProxyName, ___) {
return builder(currentSelectedProxyName);
},
);
}
double get listHeaderHeight { double get listHeaderHeight {
final measure = globalState.measure; final measure = globalState.measure;
@@ -30,9 +12,9 @@ double get listHeaderHeight {
double getItemHeight(ProxyCardType proxyCardType) { double getItemHeight(ProxyCardType proxyCardType) {
final measure = globalState.measure; final measure = globalState.measure;
final baseHeight = final baseHeight =
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8; 16 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8 + 4;
return switch (proxyCardType) { return switch (proxyCardType) {
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8, ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 6,
ProxyCardType.shrink => baseHeight, ProxyCardType.shrink => baseHeight,
ProxyCardType.min => baseHeight - measure.bodyMediumHeight, ProxyCardType.min => baseHeight - measure.bodyMediumHeight,
}; };
@@ -40,16 +22,16 @@ double getItemHeight(ProxyCardType proxyCardType) {
proxyDelayTest(Proxy proxy, [String? testUrl]) async { proxyDelayTest(Proxy proxy, [String? testUrl]) async {
final appController = globalState.appController; final appController = globalState.appController;
final proxyName = appController.appState.getRealProxyName(proxy.name); final proxyName = appController.getRealProxyName(proxy.name);
final url = appController.getRealTestUrl(testUrl); final url = appController.getRealTestUrl(testUrl);
globalState.appController.setDelay( appController.setDelay(
Delay( Delay(
url: url, url: url,
name: proxyName, name: proxyName,
value: 0, value: 0,
), ),
); );
globalState.appController.setDelay( appController.setDelay(
await clashCore.getDelay( await clashCore.getDelay(
url, url,
proxyName, proxyName,
@@ -60,21 +42,21 @@ proxyDelayTest(Proxy proxy, [String? testUrl]) async {
delayTest(List<Proxy> proxies, [String? testUrl]) async { delayTest(List<Proxy> proxies, [String? testUrl]) async {
final appController = globalState.appController; final appController = globalState.appController;
final proxyNames = proxies final proxyNames = proxies
.map((proxy) => appController.appState.getRealProxyName(proxy.name)) .map((proxy) => appController.getRealProxyName(proxy.name))
.toSet() .toSet()
.toList(); .toList();
final url = appController.getRealTestUrl(testUrl); final url = appController.getRealTestUrl(testUrl);
final delayProxies = proxyNames.map<Future>((proxyName) async { final delayProxies = proxyNames.map<Future>((proxyName) async {
globalState.appController.setDelay( appController.setDelay(
Delay( Delay(
url: url, url: url,
name: proxyName, name: proxyName,
value: 0, value: 0,
), ),
); );
globalState.appController.setDelay( appController.setDelay(
await clashCore.getDelay( await clashCore.getDelay(
url, url,
proxyName, proxyName,
@@ -86,7 +68,7 @@ delayTest(List<Proxy> proxies, [String? testUrl]) async {
for (final batchDelayProxies in batchesDelayProxies) { for (final batchDelayProxies in batchesDelayProxies) {
await Future.wait(batchDelayProxies); await Future.wait(batchDelayProxies);
} }
appController.appState.sortNum++; appController.addSortNum();
} }
double getScrollToSelectedOffset({ double getScrollToSelectedOffset({
@@ -94,14 +76,11 @@ double getScrollToSelectedOffset({
required List<Proxy> proxies, required List<Proxy> proxies,
}) { }) {
final appController = globalState.appController; final appController = globalState.appController;
final columns = other.getProxiesColumns( final columns = appController.getProxiesColumns();
appController.appState.viewWidth, final proxyCardType = globalState.config.proxiesStyle.cardType;
appController.config.proxiesStyle.layout, final selectedProxyName = appController.getSelectedProxyName(groupName);
);
final proxyCardType = appController.config.proxiesStyle.cardType;
final selectedName = appController.getCurrentSelectedName(groupName);
final findSelectedIndex = proxies.indexWhere( final findSelectedIndex = proxies.indexWhere(
(proxy) => proxy.name == selectedName, (proxy) => proxy.name == selectedProxyName,
); );
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0; final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
final rows = (selectedIndex / columns).floor(); final rows = (selectedIndex / columns).floor();

View File

@@ -3,10 +3,13 @@ import 'dart:math';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/providers/state.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'card.dart'; import 'card.dart';
import 'common.dart'; import 'common.dart';
@@ -78,7 +81,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
} else { } else {
tempUnfoldSet.add(groupName); tempUnfoldSet.add(groupName);
} }
globalState.appController.config.updateCurrentUnfoldSet( globalState.appController.updateCurrentUnfoldSet(
tempUnfoldSet, tempUnfoldSet,
); );
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -105,7 +108,8 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
return itemHeightList; return itemHeightList;
} }
List<Widget> _buildItems({ List<Widget> _buildItems(
WidgetRef ref, {
required List<String> groupNames, required List<String> groupNames,
required int columns, required int columns,
required Set<String> currentUnfoldSet, required Set<String> currentUnfoldSet,
@@ -115,7 +119,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
final GroupNameProxiesMap groupNameProxiesMap = {}; final GroupNameProxiesMap groupNameProxiesMap = {};
for (final groupName in groupNames) { for (final groupName in groupNames) {
final group = final group =
globalState.appController.appState.getGroupWithName(groupName)!; ref.read(groupsProvider.select((state) => state.getGroup(groupName)));
if (group == null) {
continue;
}
final isExpand = currentUnfoldSet.contains(groupName); final isExpand = currentUnfoldSet.contains(groupName);
items.addAll([ items.addAll([
ListHeader( ListHeader(
@@ -186,16 +193,21 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
return items; return items;
} }
_buildHeader({ _buildHeader(
WidgetRef ref, {
required String groupName, required String groupName,
required Set<String> currentUnfoldSet, required Set<String> currentUnfoldSet,
}) { }) {
final group = final group =
globalState.appController.appState.getGroupWithName(groupName)!; ref.read(groupsProvider.select((state) => state.getGroup(groupName)));
if (group == null) {
return SizedBox();
}
final isExpand = currentUnfoldSet.contains(groupName); final isExpand = currentUnfoldSet.contains(groupName);
return SizedBox( return SizedBox(
height: listHeaderHeight, height: listHeaderHeight,
child: ListHeader( child: ListHeader(
enterAnimated: false,
onScrollToSelected: _scrollToGroupSelected, onScrollToSelected: _scrollToGroupSelected,
key: Key(groupName), key: Key(groupName),
isExpand: isExpand, isExpand: isExpand,
@@ -212,7 +224,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
return; return;
} }
final appController = globalState.appController; final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups; final currentGroups = appController.getCurrentGroups();
final groupNames = currentGroups.map((e) => e.name).toList(); final groupNames = currentGroups.map((e) => e.name).toList();
final findIndex = groupNames.indexWhere((item) => item == groupName); final findIndex = groupNames.indexWhere((item) => item == groupName);
final index = findIndex != -1 ? findIndex : 0; final index = findIndex != -1 ? findIndex : 0;
@@ -235,51 +247,24 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesListSelectorState>( return Consumer(
selector: (_, appState, config) { builder: (_, ref, __) {
final currentGroups = appState.currentGroups; final state = ref.watch(proxiesListSelectorStateProvider);
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesListSelectorState(
groupNames: groupNames,
currentUnfoldSet: config.currentUnfoldSet,
proxyCardType: config.proxiesStyle.cardType,
proxiesSortType: config.proxiesStyle.sortType,
columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesStyle.layout,
),
sortNum: appState.sortNum,
);
},
shouldRebuild: (prev, next) {
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
_headerStateNotifier.value = const ProxiesListHeaderSelectorState(
offset: 0,
currentIndex: 0,
);
}
return prev != next;
},
builder: (_, state, __) {
if (state.groupNames.isEmpty) { if (state.groupNames.isEmpty) {
return NullStatus( return NullStatus(
label: appLocalizations.nullProxies, label: appLocalizations.nullProxies,
); );
} }
final items = _buildItems( final items = _buildItems(
ref,
groupNames: state.groupNames, groupNames: state.groupNames,
currentUnfoldSet: state.currentUnfoldSet, currentUnfoldSet: state.currentUnfoldSet,
columns: state.columns, columns: state.columns,
type: state.proxyCardType, type: state.proxyCardType,
); );
final itemsOffset = _getItemHeightList(items, state.proxyCardType); final itemsOffset = _getItemHeightList(items, state.proxyCardType);
return Scrollbar( return CommonScrollBar(
controller: _controller, controller: _controller,
thumbVisibility: true,
trackVisibility: true,
thickness: 8,
radius: const Radius.circular(8),
interactive: true,
child: Stack( child: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
@@ -323,6 +308,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
bottom: 8, bottom: 8,
), ),
child: _buildHeader( child: _buildHeader(
ref,
groupName: state.groupNames[index], groupName: state.groupNames[index],
currentUnfoldSet: state.currentUnfoldSet, currentUnfoldSet: state.currentUnfoldSet,
), ),
@@ -348,8 +334,11 @@ class ListHeader extends StatefulWidget {
final Function(String groupName) onScrollToSelected; final Function(String groupName) onScrollToSelected;
final bool isExpand; final bool isExpand;
final bool enterAnimated;
const ListHeader({ const ListHeader({
super.key, super.key,
this.enterAnimated = true,
required this.group, required this.group,
required this.onChange, required this.onChange,
required this.onScrollToSelected, required this.onScrollToSelected,
@@ -422,57 +411,53 @@ class _ListHeaderState extends State<ListHeader>
} }
Widget _buildIcon() { Widget _buildIcon() {
return Selector<Config, ProxiesIconStyle>( return Consumer(
selector: (_, config) => config.proxiesStyle.iconStyle, builder: (_, ref, child) {
builder: (_, iconStyle, child) { final iconStyle = ref.watch(
return Selector<Config, String>( proxiesStyleSettingProvider.select((state) => state.iconStyle));
selector: (_, config) { final icon = ref.watch(proxiesStyleSettingProvider.select((state) {
final iconMapEntryList = final iconMapEntryList = state.iconMap.entries.toList();
config.proxiesStyle.iconMap.entries.toList(); final index = iconMapEntryList.indexWhere((item) {
final index = iconMapEntryList.indexWhere((item) { try {
try { return RegExp(item.key).hasMatch(groupName);
return RegExp(item.key).hasMatch(groupName); } catch (_) {
} catch (_) { return false;
return false;
}
});
if (index != -1) {
return iconMapEntryList[index].value;
} }
return icon; });
}, if (index != -1) {
builder: (_, icon, __) { return iconMapEntryList[index].value;
return switch (iconStyle) { }
ProxiesIconStyle.standard => Container( return this.icon;
height: 48, }));
width: 48, return switch (iconStyle) {
margin: const EdgeInsets.only( ProxiesIconStyle.standard => Container(
right: 16, height: 48,
), width: 48,
padding: const EdgeInsets.all(8), margin: const EdgeInsets.only(
decoration: BoxDecoration( right: 16,
color: context.colorScheme.secondaryContainer, ),
borderRadius: BorderRadius.circular(12), padding: const EdgeInsets.all(8),
), decoration: BoxDecoration(
clipBehavior: Clip.antiAlias, color: context.colorScheme.secondaryContainer,
child: CommonTargetIcon( borderRadius: BorderRadius.circular(12),
src: icon, ),
size: 32, clipBehavior: Clip.antiAlias,
), child: CommonTargetIcon(
), src: icon,
ProxiesIconStyle.icon => Container( size: 32,
margin: const EdgeInsets.only( ),
right: 16, ),
), ProxiesIconStyle.icon => Container(
child: CommonTargetIcon( margin: const EdgeInsets.only(
src: icon, right: 16,
size: 42, ),
), child: CommonTargetIcon(
), src: icon,
ProxiesIconStyle.none => Container(), size: 42,
}; ),
}, ),
); ProxiesIconStyle.none => Container(),
};
}, },
); );
} }
@@ -480,13 +465,15 @@ class _ListHeaderState extends State<ListHeader>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CommonCard( return CommonCard(
enterAnimated: widget.enterAnimated,
key: widget.key, key: widget.key,
borderSide: WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll( backgroundColor: WidgetStatePropertyAll(
context.colorScheme.surfaceContainer, context.colorScheme.surfaceContainer,
), ),
radius: 14, radius: 14,
type: CommonCardType.filled, type: CommonCardType.filled,
child: Container( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 12, vertical: 12,
@@ -523,9 +510,13 @@ class _ListHeaderState extends State<ListHeader>
), ),
Flexible( Flexible(
flex: 1, flex: 1,
child: currentSelectedProxyNameBuilder( child: Consumer(
groupName: groupName, builder: (_, ref, __) {
builder: (currentGroupName) { final proxyName = ref
.watch(getSelectedProxyNameProvider(
groupName,
))
.getSafeValue("");
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: mainAxisAlignment:
@@ -533,12 +524,12 @@ class _ListHeaderState extends State<ListHeader>
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.center, CrossAxisAlignment.center,
children: [ children: [
if (currentGroupName.isNotEmpty) ...[ if (proxyName.isNotEmpty) ...[
Flexible( Flexible(
flex: 1, flex: 1,
child: EmojiText( child: EmojiText(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
" · $currentGroupName", " · $proxyName",
style: context.textTheme style: context.textTheme
.labelMedium?.toLight, .labelMedium?.toLight,
), ),

View File

@@ -3,34 +3,32 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.dart';
import 'package:fl_clash/models/core.dart'; import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
typedef UpdatingMap = Map<String, bool>; typedef UpdatingMap = Map<String, bool>;
class Providers extends StatefulWidget { class ProvidersView extends ConsumerStatefulWidget {
const Providers({ const ProvidersView({
super.key, super.key,
}); });
@override @override
State<Providers> createState() => _ProvidersState(); ConsumerState<ProvidersView> createState() => _ProvidersViewState();
} }
class _ProvidersState extends State<Providers> { class _ProvidersViewState extends ConsumerState<ProvidersView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
(_) { (_) {
globalState.appController.updateProviders(); globalState.appController.updateProviders();
final commonScaffoldState = context.commonScaffoldState?.actions = [
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () {
_updateProviders(); _updateProviders();
@@ -45,12 +43,12 @@ class _ProvidersState extends State<Providers> {
} }
_updateProviders() async { _updateProviders() async {
final appState = globalState.appController.appState; final providers = ref.read(providersProvider);
final providers = globalState.appController.appState.providers; final providersNotifier = ref.read(providersProvider.notifier);
final messages = []; final messages = [];
final updateProviders = providers.map<Future>( final updateProviders = providers.map<Future>(
(provider) async { (provider) async {
appState.setProvider( providersNotifier.setProvider(
provider.copyWith(isUpdating: true), provider.copyWith(isUpdating: true),
); );
final message = await clashCore.updateExternalProvider( final message = await clashCore.updateExternalProvider(
@@ -59,7 +57,7 @@ class _ProvidersState extends State<Providers> {
if (message.isNotEmpty) { if (message.isNotEmpty) {
messages.add("${provider.name}: $message \n"); messages.add("${provider.name}: $message \n");
} }
appState.setProvider( providersNotifier.setProvider(
await clashCore.getExternalProvider(provider.name), await clashCore.getExternalProvider(provider.name),
); );
}, },
@@ -85,35 +83,29 @@ class _ProvidersState extends State<Providers> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<AppState, List<ExternalProvider>>( final providers = ref.watch(providersProvider);
selector: (_, appState) => appState.providers, final proxyProviders = providers.where((item) => item.type == "Proxy").map(
builder: (_, providers, ___) { (item) => ProviderItem(
final proxyProviders = provider: item,
providers.where((item) => item.type == "Proxy").map( ),
(item) => ProviderItem(
provider: item,
),
);
final ruleProviders =
providers.where((item) => item.type == "Rule").map(
(item) => ProviderItem(
provider: item,
),
);
final proxySection = generateSection(
title: appLocalizations.proxyProviders,
items: proxyProviders,
); );
final ruleSection = generateSection( final ruleProviders = providers.where((item) => item.type == "Rule").map(
title: appLocalizations.ruleProviders, (item) => ProviderItem(
items: ruleProviders, provider: item,
),
); );
return generateListView([ final proxySection = generateSection(
...proxySection, title: appLocalizations.proxyProviders,
...ruleSection, items: proxyProviders,
]);
},
); );
final ruleSection = generateSection(
title: appLocalizations.ruleProviders,
items: ruleProviders,
);
return generateListView([
...proxySection,
...ruleSection,
]);
} }
} }
@@ -126,11 +118,11 @@ class ProviderItem extends StatelessWidget {
}); });
_handleUpdateProvider() async { _handleUpdateProvider() async {
final appState = globalState.appController.appState; final appController = globalState.appController;
if (provider.vehicleType != "HTTP") return; if (provider.vehicleType != "HTTP") return;
await globalState.safeRun( await globalState.safeRun(
() async { () async {
appState.setProvider( appController.setProvider(
provider.copyWith( provider.copyWith(
isUpdating: true, isUpdating: true,
), ),
@@ -142,7 +134,7 @@ class ProviderItem extends StatelessWidget {
}, },
silence: false, silence: false,
); );
appState.setProvider( appController.setProvider(
await clashCore.getExternalProvider(provider.name), await clashCore.getExternalProvider(provider.name),
); );
await globalState.appController.updateGroupsDebounce(); await globalState.appController.updateGroupsDebounce();
@@ -151,7 +143,6 @@ class ProviderItem extends StatelessWidget {
_handleSideLoadProvider() async { _handleSideLoadProvider() async {
await globalState.safeRun<void>(() async { await globalState.safeRun<void>(() async {
final platformFile = await picker.pickerFile(); final platformFile = await picker.pickerFile();
final appState = globalState.appController.appState;
final bytes = platformFile?.bytes; final bytes = platformFile?.bytes;
if (bytes == null || provider.path == null) return; if (bytes == null || provider.path == null) return;
final file = await File(provider.path!).create(recursive: true); final file = await File(provider.path!).create(recursive: true);
@@ -162,7 +153,7 @@ class ProviderItem extends StatelessWidget {
data: utf8.decode(bytes), data: utf8.decode(bytes),
); );
if (message.isNotEmpty) throw message; if (message.isNotEmpty) throw message;
appState.setProvider( globalState.appController.setProvider(
await clashCore.getExternalProvider(provider.name), await clashCore.getExternalProvider(provider.name),
); );
if (message.isNotEmpty) throw message; if (message.isNotEmpty) throw message;

View File

@@ -1,38 +1,38 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/proxies/list.dart'; import 'package:fl_clash/fragments/proxies/list.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/fragments/proxies/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers.dart'; import 'common.dart';
import 'setting.dart'; import 'setting.dart';
import 'tab.dart'; import 'tab.dart';
class ProxiesFragment extends StatefulWidget { class ProxiesFragment extends ConsumerStatefulWidget {
const ProxiesFragment({super.key}); const ProxiesFragment({super.key});
@override @override
State<ProxiesFragment> createState() => _ProxiesFragmentState(); ConsumerState<ProxiesFragment> createState() => _ProxiesFragmentState();
} }
class _ProxiesFragmentState extends State<ProxiesFragment> { class _ProxiesFragmentState extends ConsumerState<ProxiesFragment>
with PageMixin {
final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey(); final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey();
bool _hasProviders = false;
bool _isTab = false;
_initActions(ProxiesType proxiesType, bool hasProvider) { @override
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { get actions => [
final commonScaffoldState = if (_hasProviders)
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
if (hasProvider) ...[
IconButton( IconButton(
onPressed: () { onPressed: () {
showExtendPage( showExtendPage(
isScaffold: true, isScaffold: true,
extendPageWidth: 360, extendPageWidth: 360,
context, context,
body: const Providers(), body: const ProvidersView(),
title: appLocalizations.providers, title: appLocalizations.providers,
); );
}, },
@@ -40,71 +40,28 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
Icons.poll_outlined, Icons.poll_outlined,
), ),
), ),
], _isTab
if (proxiesType == ProxiesType.tab) ...[ ? IconButton(
IconButton( onPressed: () {
onPressed: () { _proxiesTabKey.currentState?.scrollToGroupSelected();
_proxiesTabKey.currentState?.scrollToGroupSelected(); },
}, icon: const Icon(
icon: const Icon( Icons.adjust_outlined,
Icons.adjust_outlined,
),
),
] else ...[
IconButton(
onPressed: () {
showExtendPage(
context,
extendPageWidth: 360,
title: appLocalizations.iconConfiguration,
body: Selector<Config, Map<String, String>>(
selector: (_, config) => config.proxiesStyle.iconMap,
shouldRebuild: (prev, next) {
return !stringAndStringMapEntryIterableEquality.equals(
prev.entries,
next.entries,
);
},
builder: (_, iconMap, __) {
final entries = iconMap.entries.toList();
return ListPage(
title: appLocalizations.iconConfiguration,
items: entries,
keyLabel: appLocalizations.regExp,
valueLabel: appLocalizations.icon,
keyBuilder: (item) => Key(item.key),
titleBuilder: (item) => Text(item.key),
leadingBuilder: (item) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: CommonTargetIcon(
src: item.value,
size: 42,
),
),
subtitleBuilder: (item) => Text(
item.value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onChange: (entries) {
final config = globalState.appController.config;
config.proxiesStyle = config.proxiesStyle.copyWith(
iconMap: Map.fromEntries(entries),
);
},
);
},
), ),
); )
}, : IconButton(
icon: const Icon( onPressed: () {
Icons.style_outlined, showExtendPage(
), context,
), extendPageWidth: 360,
], title: appLocalizations.iconConfiguration,
body: _IconConfigView(),
);
},
icon: const Icon(
Icons.style_outlined,
),
),
IconButton( IconButton(
onPressed: () { onPressed: () {
showSheet( showSheet(
@@ -118,28 +75,88 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
), ),
) )
]; ];
});
@override
get floatingActionButton => _isTab
? DelayTestButton(
onClick: () async {
await delayTest(
currentTabProxies,
currentTabTestUrl,
);
},
)
: null;
@override
void initState() {
ref.listenManual(
proxiesActionsStateProvider,
fireImmediately: true,
(prev, next) {
if (prev == next) {
return;
}
if (next.pageLabel == PageLabel.proxies) {
_hasProviders = next.hasProviders;
_isTab = next.type == ProxiesType.tab;
initPageState();
return;
}
},
);
super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<Config, ProxiesType>( final proxiesType =
selector: (_, config) => config.proxiesStyle.type, ref.watch(proxiesStyleSettingProvider.select((state) => state.type));
builder: (_, proxiesType, __) { return switch (proxiesType) {
return ProxiesActionsBuilder( ProxiesType.tab => ProxiesTabFragment(
builder: (state, child) { key: _proxiesTabKey,
if (state.isCurrent) { ),
_initActions(proxiesType, state.hasProvider); ProxiesType.list => const ProxiesListFragment(),
} };
return child!; }
}, }
child: switch (proxiesType) {
ProxiesType.tab => ProxiesTabFragment( class _IconConfigView extends ConsumerWidget {
key: _proxiesTabKey, const _IconConfigView();
@override
Widget build(BuildContext context, WidgetRef ref) {
final iconMap =
ref.watch(proxiesStyleSettingProvider.select((state) => state.iconMap));
final entries = iconMap.entries.toList();
return ListPage(
title: appLocalizations.iconConfiguration,
items: entries,
keyLabel: appLocalizations.regExp,
valueLabel: appLocalizations.icon,
keyBuilder: (item) => Key(item.key),
titleBuilder: (item) => Text(item.key),
leadingBuilder: (item) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: CommonTargetIcon(
src: item.value,
size: 42,
),
),
subtitleBuilder: (item) => Text(
item.value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onChange: (entries) {
ref.read(proxiesStyleSettingProvider.notifier).updateState(
(state) => state.copyWith(
iconMap: Map.fromEntries(entries),
), ),
ProxiesType.list => const ProxiesListFragment(), );
},
);
}, },
); );
} }

View File

@@ -1,11 +1,10 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class ProxiesSetting extends StatelessWidget { class ProxiesSetting extends StatelessWidget {
const ProxiesSetting({super.key}); const ProxiesSetting({super.key});
@@ -56,10 +55,11 @@ class ProxiesSetting extends StatelessWidget {
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesType>( child: Consumer(
selector: (_, config) => config.proxiesStyle.type, builder: (_, ref, __) {
builder: (_, proxiesType, __) { final proxiesType = ref.watch(proxiesStyleSettingProvider.select(
final config = globalState.appController.config; (state) => state.type,
));
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
@@ -71,9 +71,13 @@ class ProxiesSetting extends StatelessWidget {
), ),
isSelected: proxiesType == item, isSelected: proxiesType == item,
onPressed: () { onPressed: () {
config.proxiesStyle = config.proxiesStyle.copyWith( ref
type: item, .read(proxiesStyleSettingProvider.notifier)
); .updateState((state) {
return state.copyWith(
type: item,
);
});
}, },
) )
], ],
@@ -92,10 +96,11 @@ class ProxiesSetting extends StatelessWidget {
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesSortType>( child: Consumer(
selector: (_, config) => config.proxiesStyle.sortType, builder: (_, ref, __) {
builder: (_, proxiesSortType, __) { final sortType = ref.watch(proxiesStyleSettingProvider.select(
final config = globalState.appController.config; (state) => state.sortType,
));
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
@@ -105,11 +110,15 @@ class ProxiesSetting extends StatelessWidget {
label: _getStringProxiesSortType(item), label: _getStringProxiesSortType(item),
iconData: _getIconWithProxiesSortType(item), iconData: _getIconWithProxiesSortType(item),
), ),
isSelected: proxiesSortType == item, isSelected: sortType == item,
onPressed: () { onPressed: () {
config.proxiesStyle = config.proxiesStyle.copyWith( ref
sortType: item, .read(proxiesStyleSettingProvider.notifier)
); .updateState((state) {
return state.copyWith(
sortType: item,
);
});
}, },
), ),
], ],
@@ -128,21 +137,26 @@ class ProxiesSetting extends StatelessWidget {
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, ProxyCardType>( child: Consumer(
selector: (_, config) => config.proxiesStyle.cardType, builder: (_, ref, __) {
builder: (_, proxyCardType, __) { final cardType = ref.watch(proxiesStyleSettingProvider.select(
final config = globalState.appController.config; (state) => state.cardType,
));
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
for (final item in ProxyCardType.values) for (final item in ProxyCardType.values)
SettingTextCard( SettingTextCard(
Intl.message(item.name), Intl.message(item.name),
isSelected: item == proxyCardType, isSelected: item == cardType,
onPressed: () { onPressed: () {
config.proxiesStyle = config.proxiesStyle.copyWith( ref
cardType: item, .read(proxiesStyleSettingProvider.notifier)
); .updateState((state) {
return state.copyWith(
cardType: item,
);
});
}, },
) )
], ],
@@ -163,21 +177,26 @@ class ProxiesSetting extends StatelessWidget {
horizontal: 16, horizontal: 16,
), ),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesLayout>( child: Consumer(
selector: (_, config) => config.proxiesStyle.layout, builder: (_, ref, __) {
builder: (_, proxiesLayout, __) { final layout = ref.watch(proxiesStyleSettingProvider.select(
final config = globalState.appController.config; (state) => state.layout,
));
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
for (final item in ProxiesLayout.values) for (final item in ProxiesLayout.values)
SettingTextCard( SettingTextCard(
getTextForProxiesLayout(item), getTextForProxiesLayout(item),
isSelected: item == proxiesLayout, isSelected: item == layout,
onPressed: () { onPressed: () {
config.proxiesStyle = config.proxiesStyle.copyWith( ref
layout: item, .watch(proxiesStyleSettingProvider.notifier)
); .updateState((state) {
return state.copyWith(
layout: item,
);
});
}, },
) )
], ],
@@ -196,9 +215,11 @@ class ProxiesSetting extends StatelessWidget {
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Selector<Config, ProxiesIconStyle>( child: Consumer(
selector: (_, config) => config.proxiesStyle.iconStyle, builder: (_, ref, __) {
builder: (_, iconStyle, __) { final iconStyle = ref.watch(proxiesStyleSettingProvider.select(
(state) => state.iconStyle,
));
return Wrap( return Wrap(
spacing: 16, spacing: 16,
children: [ children: [
@@ -207,10 +228,13 @@ class ProxiesSetting extends StatelessWidget {
_getTextWithProxiesIconStyle(item), _getTextWithProxiesIconStyle(item),
isSelected: iconStyle == item, isSelected: iconStyle == item,
onPressed: () { onPressed: () {
final config = globalState.appController.config; ref
config.proxiesStyle = config.proxiesStyle.copyWith( .read(proxiesStyleSettingProvider.notifier)
iconStyle: item, .updateState((state) {
); return state.copyWith(
iconStyle: item,
);
});
}, },
), ),
], ],
@@ -234,11 +258,11 @@ class ProxiesSetting extends StatelessWidget {
..._buildSortSetting(), ..._buildSortSetting(),
..._buildLayoutSetting(), ..._buildLayoutSetting(),
..._buildSizeSetting(), ..._buildSizeSetting(),
Selector<Config, bool>( Consumer(
selector: (_, config) => builder: (_, ref, child) {
config.proxiesStyle.type == ProxiesType.list, final isList = ref.watch(proxiesStyleSettingProvider
builder: (_, value, child) { .select((state) => state.type == ProxiesType.list));
if (value) { if (isList) {
return child!; return child!;
} }
return Container(); return Container();

View File

@@ -1,34 +1,40 @@
import 'dart:math'; import 'dart:math';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'card.dart'; import 'card.dart';
import 'common.dart'; import 'common.dart';
List<Proxy> currentProxies = []; List<Proxy> currentTabProxies = [];
String? currentTestUrl; String? currentTabTestUrl;
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>; typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
class ProxiesTabFragment extends StatefulWidget { class ProxiesTabFragment extends ConsumerStatefulWidget {
const ProxiesTabFragment({super.key}); const ProxiesTabFragment({super.key});
@override @override
State<ProxiesTabFragment> createState() => ProxiesTabFragmentState(); ConsumerState<ProxiesTabFragment> createState() => ProxiesTabFragmentState();
} }
class ProxiesTabFragmentState extends State<ProxiesTabFragment> class ProxiesTabFragmentState extends ConsumerState<ProxiesTabFragment>
with TickerProviderStateMixin { with TickerProviderStateMixin {
TabController? _tabController; TabController? _tabController;
final _hasMoreButtonNotifier = ValueNotifier<bool>(false); final _hasMoreButtonNotifier = ValueNotifier<bool>(false);
GroupNameKeyMap _keyMap = {}; GroupNameKeyMap _keyMap = {};
@override
void initState() {
super.initState();
_handleTabListen();
}
@override @override
void dispose() { void dispose() {
_destroyTabController(); _destroyTabController();
@@ -36,17 +42,17 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
} }
scrollToGroupSelected() { scrollToGroupSelected() {
final currentGroupName = globalState.appController.config.currentGroupName; final currentGroupName = globalState.appController.getCurrentGroupName();
_keyMap[currentGroupName]?.currentState?.scrollToSelected(); _keyMap[currentGroupName]?.currentState?.scrollToSelected();
} }
_buildMoreButton() { _buildMoreButton() {
return Selector<AppState, bool>( return Consumer(
selector: (_, appState) => appState.viewMode == ViewMode.mobile, builder: (_, ref, ___) {
builder: (_, value, ___) { final isMobileView = ref.watch(viewWidthProvider.notifier).isMobileView;
return IconButton( return IconButton(
onPressed: _showMoreMenu, onPressed: _showMoreMenu,
icon: value icon: isMobileView
? const Icon( ? const Icon(
Icons.expand_more, Icons.expand_more,
) )
@@ -65,16 +71,9 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
isScrollControlled: false, isScrollControlled: false,
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Selector2<AppState, Config, ProxiesSelectorState>( child: Consumer(
selector: (_, appState, config) { builder: (_, ref, __) {
final currentGroups = appState.currentGroups; final state = ref.watch(proxiesSelectorStateProvider);
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
builder: (_, state, __) {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: Wrap( child: Wrap(
@@ -86,13 +85,13 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
SettingTextCard( SettingTextCard(
groupName, groupName,
onPressed: () { onPressed: () {
final index = state.groupNames final index = state.groupNames.indexWhere(
.indexWhere((item) => item == groupName); (item) => item == groupName,
);
if (index == -1) return; if (index == -1) return;
_tabController?.animateTo(index); _tabController?.animateTo(index);
globalState.appController.config.updateCurrentGroupName( globalState.appController
groupName, .updateCurrentGroupName(groupName);
);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
isSelected: groupName == state.currentGroupName, isSelected: groupName == state.currentGroupName,
@@ -108,16 +107,24 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
} }
_tabControllerListener([int? index]) { _tabControllerListener([int? index]) {
final appController = globalState.appController; int? groupIndex = index;
final currentGroups = appController.appState.currentGroups; if(groupIndex == -1){
if (_tabController?.index == null) {
return; return;
} }
final currentGroup = currentGroups[index ?? _tabController!.index]; final appController = globalState.appController;
currentProxies = currentGroup.all; if (groupIndex == null) {
currentTestUrl = currentGroup.testUrl; final currentIndex = _tabController?.index;
groupIndex = currentIndex;
}
final currentGroups = appController.getCurrentGroups();
if (groupIndex == null || groupIndex > currentGroups.length) {
return;
}
final currentGroup = currentGroups[groupIndex];
currentTabProxies = currentGroup.all;
currentTabTestUrl = currentGroup.testUrl;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
appController.config.updateCurrentGroupName( globalState.appController.updateCurrentGroupName(
currentGroup.name, currentGroup.name,
); );
}); });
@@ -144,122 +151,117 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
_tabController?.addListener(_tabControllerListener); _tabController?.addListener(_tabControllerListener);
} }
_handleTabListen() {
ref.listenManual(
proxiesSelectorStateProvider,
(prev, next) {
if (prev == next) {
return;
}
if (prev?.groupNames.length != next.groupNames.length) {
_destroyTabController();
final index = next.groupNames.indexWhere(
(item) => item == next.currentGroupName,
);
_updateTabController(next.groupNames.length, index);
}
},
fireImmediately: true,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>( final state = ref.watch(groupNamesStateProvider);
selector: (_, appState, config) { final groupNames = state.groupNames;
final currentGroups = appState.currentGroups; if (groupNames.isEmpty) {
final groupNames = currentGroups.map((e) => e.name).toList(); return NullStatus(
return ProxiesSelectorState( label: appLocalizations.nullProxies,
groupNames: groupNames, );
currentGroupName: config.currentGroupName, }
); final GroupNameKeyMap keyMap = {};
}, final children = groupNames.map((groupName) {
shouldRebuild: (prev, next) { keyMap[groupName] = GlobalObjectKey(groupName);
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) { return KeepScope(
_destroyTabController(); child: ProxyGroupView(
return true; key: keyMap[groupName],
} groupName: groupName,
return false; ),
}, );
builder: (_, state, __) { }).toList();
if (state.groupNames.isEmpty) { _keyMap = keyMap;
return NullStatus( return Column(
label: appLocalizations.nullProxies, mainAxisAlignment: MainAxisAlignment.start,
); crossAxisAlignment: CrossAxisAlignment.start,
} children: [
final index = state.groupNames.indexWhere( NotificationListener<ScrollMetricsNotification>(
(item) => item == state.currentGroupName, onNotification: (scrollNotification) {
); _hasMoreButtonNotifier.value =
_updateTabController(state.groupNames.length, index); scrollNotification.metrics.maxScrollExtent > 0;
if (state.groupNames.isEmpty) { return true;
return Container(); },
} child: ValueListenableBuilder(
final GroupNameKeyMap keyMap = {}; valueListenable: _hasMoreButtonNotifier,
final children = state.groupNames.map((groupName) { builder: (_, value, child) {
keyMap[groupName] = GlobalObjectKey(groupName); return Stack(
return KeepScope( alignment: AlignmentDirectional.centerStart,
child: ProxyGroupView( children: [
key: keyMap[groupName], TabBar(
groupName: groupName, controller: _tabController,
), padding: EdgeInsets.only(
); left: 16,
}).toList(); right: 16 + (value ? 16 : 0),
_keyMap = keyMap; ),
return Column( dividerColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.start, isScrollable: true,
crossAxisAlignment: CrossAxisAlignment.start, tabAlignment: TabAlignment.start,
children: [ overlayColor:
NotificationListener<ScrollMetricsNotification>( const WidgetStatePropertyAll(Colors.transparent),
onNotification: (scrollNotification) { tabs: [
_hasMoreButtonNotifier.value = for (final groupName in groupNames)
scrollNotification.metrics.maxScrollExtent > 0; Tab(
return true; text: groupName,
},
child: ValueListenableBuilder(
valueListenable: _hasMoreButtonNotifier,
builder: (_, value, child) {
return Stack(
alignment: AlignmentDirectional.centerStart,
children: [
TabBar(
controller: _tabController,
padding: EdgeInsets.only(
left: 16,
right: 16 + (value ? 16 : 0),
),
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
overlayColor:
const WidgetStatePropertyAll(Colors.transparent),
tabs: [
for (final groupName in state.groupNames)
Tab(
text: groupName,
),
],
),
if (value)
Positioned(
right: 0,
child: child!,
), ),
], ],
);
},
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
context.colorScheme.surface.withOpacity(0.1),
context.colorScheme.surface,
],
stops: const [
0.0,
0.1
]),
), ),
child: _buildMoreButton(), if (value)
), Positioned(
right: 0,
child: child!,
),
],
);
},
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
context.colorScheme.surface.withOpacity(0.1),
context.colorScheme.surface,
],
stops: const [
0.0,
0.1
]),
), ),
child: _buildMoreButton(),
), ),
Expanded( ),
child: TabBarView( ),
controller: _tabController, Expanded(
children: children, child: TabBarView(
), controller: _tabController,
) children: children,
], ),
); )
}, ],
); );
} }
} }
class ProxyGroupView extends StatefulWidget { class ProxyGroupView extends ConsumerStatefulWidget {
final String groupName; final String groupName;
const ProxyGroupView({ const ProxyGroupView({
@@ -268,25 +270,14 @@ class ProxyGroupView extends StatefulWidget {
}); });
@override @override
State<ProxyGroupView> createState() => ProxyGroupViewState(); ConsumerState<ProxyGroupView> createState() => ProxyGroupViewState();
} }
class ProxyGroupViewState extends State<ProxyGroupView> { class ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
var isLock = false;
final _controller = ScrollController(); final _controller = ScrollController();
String get groupName => widget.groupName; String get groupName => widget.groupName;
_delayTest() async {
if (isLock) return;
isLock = true;
await delayTest(
currentProxies,
currentTestUrl,
);
isLock = false;
}
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
@@ -297,9 +288,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
if (_controller.position.maxScrollExtent == 0) { if (_controller.position.maxScrollExtent == 0) {
return; return;
} }
final sortedProxies = globalState.appController.getSortProxies( final sortedProxies = globalState.appController.getSortProxies(
currentProxies, currentTabProxies,
currentTestUrl, currentTabTestUrl,
); );
_controller.animateTo( _controller.animateTo(
min( min(
@@ -315,85 +307,45 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
); );
} }
initFab(bool isCurrent) {
if (!isCurrent) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DelayTestButton(
onClick: () async {
await _delayTest();
},
);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxyGroupSelectorState>( final state = ref.watch(proxyGroupSelectorStateProvider(groupName));
selector: (_, appState, config) { final proxies = state.proxies;
final group = appState.getGroupWithName(groupName)!; final columns = state.columns;
return ProxyGroupSelectorState( final proxyCardType = state.proxyCardType;
proxyCardType: config.proxiesStyle.cardType, final sortedProxies = globalState.appController.getSortProxies(
proxiesSortType: config.proxiesStyle.sortType, proxies,
columns: other.getProxiesColumns( state.testUrl,
appState.viewWidth, );
config.proxiesStyle.layout, return Align(
), alignment: Alignment.topCenter,
sortNum: appState.sortNum, child: GridView.builder(
proxies: group.all, controller: _controller,
groupType: group.type, padding: const EdgeInsets.only(
testUrl: group.testUrl, top: 16,
); left: 16,
}, right: 16,
builder: (_, state, __) { bottom: 96,
final proxies = state.proxies; ),
final columns = state.columns; gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
final proxyCardType = state.proxyCardType; crossAxisCount: columns,
final sortedProxies = globalState.appController.getSortProxies( mainAxisSpacing: 8,
proxies, crossAxisSpacing: 8,
state.testUrl, mainAxisExtent: getItemHeight(proxyCardType),
); ),
return ActiveBuilder( itemCount: sortedProxies.length,
label: "proxies", itemBuilder: (_, index) {
builder: (isCurrent, child) { final proxy = sortedProxies[index];
initFab(isCurrent); return ProxyCard(
return child!; testUrl: state.testUrl,
}, groupType: state.groupType,
child: Align( type: proxyCardType,
alignment: Alignment.topCenter, key: ValueKey('$groupName.${proxy.name}'),
child: GridView.builder( proxy: proxy,
controller: _controller, groupName: groupName,
padding: const EdgeInsets.only( );
top: 16, },
left: 16, ),
right: 16,
bottom: 96,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return ProxyCard(
testUrl: state.testUrl,
groupType: state.groupType,
type: proxyCardType,
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
},
),
),
);
},
); );
} }
} }
@@ -416,6 +368,9 @@ class _DelayTestButtonState extends State<DelayTestButton>
late Animation<double> _scale; late Animation<double> _scale;
_healthcheck() async { _healthcheck() async {
if (_controller.isAnimating) {
return;
}
_controller.forward(); _controller.forward();
await widget.onClick(); await widget.onClick();
if (mounted) { if (mounted) {

View File

@@ -1,317 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class RequestsFragment extends StatefulWidget {
const RequestsFragment({super.key});
@override
State<RequestsFragment> createState() => _RequestsFragmentState();
}
class _RequestsFragmentState extends State<RequestsFragment> {
final requestsNotifier =
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
final ScrollController _scrollController = ScrollController(
keepScrollOffset: false,
);
Timer? timer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appState = globalState.appController.appState;
requestsNotifier.value =
requestsNotifier.value.copyWith(connections: appState.requests);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
final maxLength = Platform.isAndroid ? 1000 : 60;
final requests = appState.requests.safeSublist(
appState.requests.length - maxLength,
);
if (!connectionListEquality.equals(
requestsNotifier.value.connections,
requests,
)) {
requestsNotifier.value =
requestsNotifier.value.copyWith(connections: requests);
}
});
});
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
showSearch(
context: context,
delegate: RequestsSearchDelegate(
state: requestsNotifier.value,
),
);
},
icon: const Icon(Icons.search),
),
];
},
);
}
_addKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..add(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..remove(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
@override
void dispose() {
timer?.cancel();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'requests' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: ValueListenableBuilder<ConnectionsAndKeywords>(
valueListenable: requestsNotifier,
builder: (_, state, __) {
var connections = state.filteredConnections;
if (connections.isEmpty) {
return NullStatus(
label: appLocalizations.nullRequestsDesc,
);
}
connections = connections.reversed.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.keywords.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: _addKeyword,
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
)
],
);
},
),
);
}
}
class RequestsSearchDelegate extends SearchDelegate {
ValueNotifier<ConnectionsAndKeywords> requestsNotifier;
RequestsSearchDelegate({
required ConnectionsAndKeywords state,
}) : requestsNotifier = ValueNotifier<ConnectionsAndKeywords>(state);
get state => requestsNotifier.value;
List<Connection> get _results {
final lowerQuery = query.toLowerCase().trim();
return requestsNotifier.value.filteredConnections.where((request) {
final lowerNetwork = request.metadata.network.toLowerCase();
final lowerHost = request.metadata.host.toLowerCase();
final lowerDestinationIP = request.metadata.destinationIP.toLowerCase();
final lowerProcess = request.metadata.process.toLowerCase();
final lowerChains = request.chains.join("").toLowerCase();
return lowerNetwork.contains(lowerQuery) ||
lowerHost.contains(lowerQuery) ||
lowerDestinationIP.contains(lowerQuery) ||
lowerProcess.contains(lowerQuery) ||
lowerChains.contains(lowerQuery);
}).toList();
}
_addKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..add(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
_deleteKeyword(String keyword) {
final isContains = requestsNotifier.value.keywords.contains(keyword);
if (!isContains) return;
final keywords = List<String>.from(requestsNotifier.value.keywords)
..remove(keyword);
requestsNotifier.value = requestsNotifier.value.copyWith(
keywords: keywords,
);
}
@override
List<Widget>? buildActions(BuildContext context) {
return [
IconButton(
onPressed: () {
if (query.isEmpty) {
close(context, null);
return;
}
query = '';
},
icon: const Icon(Icons.clear),
),
const SizedBox(
width: 8,
)
];
}
@override
Widget? buildLeading(BuildContext context) {
return IconButton(
onPressed: () {
close(context, null);
},
icon: const Icon(Icons.arrow_back),
);
}
@override
Widget buildResults(BuildContext context) {
return buildSuggestions(context);
}
@override
void dispose() {
requestsNotifier.dispose();
super.dispose();
}
@override
Widget buildSuggestions(BuildContext context) {
return ValueListenableBuilder(
valueListenable: requestsNotifier,
builder: (_, __, ___) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.keywords.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: Wrap(
runSpacing: 6,
spacing: 6,
children: [
for (final keyword in state.keywords)
CommonChip(
label: keyword,
type: ChipType.delete,
onPressed: () {
_deleteKeyword(keyword);
},
),
],
),
),
Expanded(
child: ListView.separated(
itemBuilder: (_, index) {
final connection = _results[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
_addKeyword(value);
},
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: _results.length,
),
)
],
);
},
);
}
}

View File

@@ -3,11 +3,12 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart' hide context; import 'package:path/path.dart' hide context;
import 'package:provider/provider.dart';
@immutable @immutable
class GeoItem { class GeoItem {
@@ -84,12 +85,13 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
GeoItem get geoItem => widget.geoItem; GeoItem get geoItem => widget.geoItem;
_updateUrl(String url) async { _updateUrl(String url, WidgetRef ref) async {
final defaultMap = defaultGeoXUrl.toJson();
final newUrl = await globalState.showCommonDialog<String>( final newUrl = await globalState.showCommonDialog<String>(
child: UpdateGeoUrlFormDialog( child: UpdateGeoUrlFormDialog(
title: geoItem.label, title: geoItem.label,
url: url, url: url,
defaultValue: defaultGeoXMap[geoItem.key], defaultValue: defaultMap[geoItem.key],
), ),
); );
if (newUrl != null && newUrl != url && mounted) { if (newUrl != null && newUrl != url && mounted) {
@@ -97,9 +99,13 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
if (!newUrl.isUrl) { if (!newUrl.isUrl) {
throw "Invalid url"; throw "Invalid url";
} }
final appController = globalState.appController; ref.read(patchClashConfigProvider.notifier).updateState((state) {
appController.clashConfig.geoXUrl = final map = state.geoXUrl.toJson();
Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl; map[geoItem.key] = newUrl;
return state.copyWith(
geoXUrl: GeoXUrl.fromJson(map),
);
});
} catch (e) { } catch (e) {
globalState.showMessage( globalState.showMessage(
title: geoItem.label, title: geoItem.label,
@@ -112,7 +118,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
} }
Future<FileInfo> _getGeoFileLastModified(String fileName) async { Future<FileInfo> _getGeoFileLastModified(String fileName) async {
final homePath = await appPath.getHomeDirPath(); final homePath = await appPath.homeDirPath;
final file = File(join(homePath, fileName)); final file = File(join(homePath, fileName));
final lastModified = await file.lastModified(); final lastModified = await file.lastModified();
final size = await file.length(); final size = await file.length();
@@ -122,68 +128,84 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
); );
} }
Widget _buildSubtitle(String url) { Widget _buildSubtitle() {
return Column( return Consumer(
crossAxisAlignment: CrossAxisAlignment.start, builder: (_, ref, __) {
children: [ final url = ref.watch(
const SizedBox( patchClashConfigProvider
height: 4, .select((state) => state.geoXUrl.toJson()[geoItem.key]),
), );
FutureBuilder<FileInfo>( if (url == null) {
future: _getGeoFileLastModified(geoItem.fileName), return SizedBox();
builder: (_, snapshot) { }
return SizedBox( return Column(
height: 24, crossAxisAlignment: CrossAxisAlignment.start,
child: FadeBox(
key: Key("fade_box_${geoItem.label}"),
child: snapshot.data == null
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
snapshot.data!.desc,
),
),
);
},
),
Text(
url,
style: context.textTheme.bodyMedium?.toLight,
),
const SizedBox(
height: 8,
),
Wrap(
runSpacing: 6,
spacing: 12,
children: [ children: [
CommonChip( const SizedBox(
avatar: const Icon(Icons.edit), height: 4,
label: appLocalizations.edit, ),
onPressed: () { FutureBuilder<FileInfo>(
_updateUrl(url); future: _getGeoFileLastModified(geoItem.fileName),
builder: (_, snapshot) {
return SizedBox(
height: 24,
child: FadeBox(
key: Key("fade_box_${geoItem.label}"),
child: snapshot.data == null
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
snapshot.data!.desc,
),
),
);
}, },
), ),
CommonChip( Text(
avatar: const Icon(Icons.sync), url,
label: appLocalizations.sync, style: context.textTheme.bodyMedium?.toLight,
onPressed: () { ),
_handleUpdateGeoDataItem(); const SizedBox(
}, height: 8,
),
Wrap(
runSpacing: 6,
spacing: 12,
children: [
CommonChip(
avatar: const Icon(Icons.edit),
label: appLocalizations.edit,
onPressed: () {
_updateUrl(url, ref);
},
),
CommonChip(
avatar: const Icon(Icons.sync),
label: appLocalizations.sync,
onPressed: () {
_handleUpdateGeoDataItem();
},
),
],
), ),
], ],
), );
], },
); );
} }
_handleUpdateGeoDataItem() async { _handleUpdateGeoDataItem() async {
await globalState.safeRun<void>(updateGeoDateItem); await globalState.safeRun<void>(
() async {
await updateGeoDateItem();
},
silence: false,
);
setState(() {}); setState(() {});
} }
@@ -219,12 +241,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
vertical: 4, vertical: 4,
), ),
title: Text(geoItem.label), title: Text(geoItem.label),
subtitle: Selector<ClashConfig, String>( subtitle: _buildSubtitle(),
selector: (_, clashConfig) => clashConfig.geoXUrl[geoItem.key]!,
builder: (_, value, __) {
return _buildSubtitle(value);
},
),
trailing: SizedBox( trailing: SizedBox(
height: 48, height: 48,
width: 48, width: 48,

View File

@@ -1,11 +1,9 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../widgets/widgets.dart';
class ThemeModeItem { class ThemeModeItem {
final ThemeMode themeMode; final ThemeMode themeMode;
@@ -88,99 +86,106 @@ class ItemCard extends StatelessWidget {
} }
} }
class ThemeColorsBox extends StatefulWidget { class ThemeColorsBox extends ConsumerStatefulWidget {
const ThemeColorsBox({super.key}); const ThemeColorsBox({super.key});
@override @override
State<ThemeColorsBox> createState() => _ThemeColorsBoxState(); ConsumerState<ThemeColorsBox> createState() => _ThemeColorsBoxState();
} }
class _ThemeColorsBoxState extends State<ThemeColorsBox> { class _ThemeColorsBoxState extends ConsumerState<ThemeColorsBox> {
Widget _themeModeCheckBox({
bool? isSelected,
required ThemeModeItem themeModeItem,
}) {
return CommonCard(
isSelected: isSelected,
onPressed: () {
final appController = globalState.appController;
appController.config.themeProps =
appController.config.themeProps.copyWith(
themeMode: themeModeItem.themeMode,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Icon(themeModeItem.iconData),
),
const SizedBox(
width: 8,
),
Flexible(
child: Text(
themeModeItem.label,
),
),
],
),
),
);
}
Widget _primaryColorCheckBox({
bool? isSelected,
Color? color,
}) {
return ColorSchemeBox(
isSelected: isSelected,
primaryColor: color,
onPressed: () {
final appController = globalState.appController;
appController.config.themeProps =
appController.config.themeProps.copyWith(
primaryColor: color?.value,
);
},
);
}
Widget _fontFamilyCheckBox({
bool? isSelected,
required FontFamilyItem fontFamilyItem,
}) {
return CommonCard(
isSelected: isSelected,
onPressed: () {
final appController = globalState.appController;
appController.config.themeProps =
appController.config.themeProps.copyWith(
fontFamily: fontFamilyItem.fontFamily,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Text(
fontFamilyItem.label,
),
),
],
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column(
children: [
_FontFamilyItem(),
_ThemeModeItem(),
_PrimaryColorItem(),
_PrueBlackItem(),
const SizedBox(
height: 64,
),
],
);
}
}
class _FontFamilyItem extends ConsumerWidget {
const _FontFamilyItem();
@override
Widget build(BuildContext context, WidgetRef ref) {
final fontFamily =
ref.watch(themeSettingProvider.select((state) => state.fontFamily));
List<FontFamilyItem> fontFamilyItems = [
FontFamilyItem(
label: appLocalizations.systemFont,
fontFamily: FontFamily.system,
),
const FontFamilyItem(
label: "MiSans",
fontFamily: FontFamily.miSans,
),
];
return ItemCard(
info: Info(
label: appLocalizations.fontFamily,
iconData: Icons.text_fields,
),
child: Container(
margin: const EdgeInsets.only(
left: 16,
right: 16,
),
height: 48,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemBuilder: (_, index) {
final fontFamilyItem = fontFamilyItems[index];
return CommonCard(
isSelected: fontFamilyItem.fontFamily == fontFamily,
onPressed: () {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith(
fontFamily: fontFamilyItem.fontFamily,
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Text(
fontFamilyItem.label,
),
),
],
),
),
);
},
separatorBuilder: (_, __) {
return const SizedBox(
width: 16,
);
},
itemCount: fontFamilyItems.length,
),
),
);
}
}
class _ThemeModeItem extends ConsumerWidget {
const _ThemeModeItem();
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode =
ref.watch(themeSettingProvider.select((state) => state.themeMode));
List<ThemeModeItem> themeModeItems = [ List<ThemeModeItem> themeModeItems = [
ThemeModeItem( ThemeModeItem(
iconData: Icons.auto_mode, iconData: Icons.auto_mode,
@@ -198,6 +203,68 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
themeMode: ThemeMode.dark, themeMode: ThemeMode.dark,
), ),
]; ];
return ItemCard(
info: Info(
label: appLocalizations.themeMode,
iconData: Icons.brightness_high,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
height: 64,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: themeModeItems.length,
itemBuilder: (_, index) {
final themeModeItem = themeModeItems[index];
return CommonCard(
isSelected: themeModeItem.themeMode == themeMode,
onPressed: () {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith(
themeMode: themeModeItem.themeMode,
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Icon(themeModeItem.iconData),
),
const SizedBox(
width: 8,
),
Flexible(
child: Text(
themeModeItem.label,
),
),
],
),
),
);
},
separatorBuilder: (_, __) {
return const SizedBox(
width: 16,
);
},
),
),
);
}
}
class _PrimaryColorItem extends ConsumerWidget {
const _PrimaryColorItem();
@override
Widget build(BuildContext context, WidgetRef ref) {
final primaryColor =
ref.watch(themeSettingProvider.select((state) => state.primaryColor));
List<Color?> primaryColors = [ List<Color?> primaryColors = [
null, null,
defaultPrimaryColor, defaultPrimaryColor,
@@ -207,147 +274,72 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
Colors.yellowAccent, Colors.yellowAccent,
Colors.purple, Colors.purple,
]; ];
List<FontFamilyItem> fontFamilyItems = [ return ItemCard(
FontFamilyItem( info: Info(
label: appLocalizations.systemFont, label: appLocalizations.themeColor,
fontFamily: FontFamily.system, iconData: Icons.palette,
), ),
const FontFamilyItem( child: Container(
label: "MiSans", margin: const EdgeInsets.only(
fontFamily: FontFamily.miSans, left: 16,
right: 16,
bottom: 16,
),
height: 88,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemBuilder: (_, index) {
final color = primaryColors[index];
return ColorSchemeBox(
isSelected: color?.value == primaryColor,
primaryColor: color,
onPressed: () {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith(
primaryColor: color?.value,
),
);
},
);
},
separatorBuilder: (_, __) {
return const SizedBox(
width: 16,
);
},
itemCount: primaryColors.length,
),
),
);
}
}
class _PrueBlackItem extends ConsumerWidget {
const _PrueBlackItem();
@override
Widget build(BuildContext context, WidgetRef ref) {
final prueBlack =
ref.watch(themeSettingProvider.select((state) => state.prueBlack));
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: ListItem.switchItem(
leading: Icon(
Icons.contrast,
color: context.colorScheme.primary,
),
title: Text(appLocalizations.prueBlackMode),
delegate: SwitchDelegate(
value: prueBlack,
onChanged: (value) {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith(
prueBlack: value,
),
);
},
),
), ),
];
return Column(
children: [
ItemCard(
info: Info(
label: appLocalizations.fontFamily,
iconData: Icons.text_fields,
),
child: Container(
margin: const EdgeInsets.only(
left: 16,
right: 16,
),
height: 48,
child: Selector<Config, FontFamily>(
selector: (_, config) => config.themeProps.fontFamily,
builder: (_, fontFamily, __) {
return ListView.separated(
scrollDirection: Axis.horizontal,
itemBuilder: (_, index) {
final fontFamilyItem = fontFamilyItems[index];
return _fontFamilyCheckBox(
isSelected: fontFamily == fontFamilyItem.fontFamily,
fontFamilyItem: fontFamilyItem,
);
},
separatorBuilder: (_, __) {
return const SizedBox(
width: 16,
);
},
itemCount: fontFamilyItems.length,
);
},
),
),
),
ItemCard(
info: Info(
label: appLocalizations.themeMode,
iconData: Icons.brightness_high,
),
child: Selector<Config, ThemeMode>(
selector: (_, config) => config.themeProps.themeMode,
builder: (_, themeMode, __) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
height: 64,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: themeModeItems.length,
itemBuilder: (_, index) {
final themeModeItem = themeModeItems[index];
return _themeModeCheckBox(
isSelected: themeMode == themeModeItem.themeMode,
themeModeItem: themeModeItem,
);
},
separatorBuilder: (_, __) {
return const SizedBox(
width: 16,
);
},
),
);
},
),
),
ItemCard(
info: Info(
label: appLocalizations.themeColor,
iconData: Icons.palette,
),
child: Container(
margin: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
),
height: 88,
child: Selector<Config, int?>(
selector: (_, config) => config.themeProps.primaryColor,
builder: (_, currentPrimaryColor, __) {
return ListView.separated(
scrollDirection: Axis.horizontal,
itemBuilder: (_, index) {
final primaryColor = primaryColors[index];
return _primaryColorCheckBox(
isSelected: currentPrimaryColor == primaryColor?.value,
color: primaryColor,
);
},
separatorBuilder: (_, __) {
return const SizedBox(
width: 16,
);
},
itemCount: primaryColors.length,
);
},
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Selector<Config, bool>(
selector: (_, config) => config.themeProps.prueBlack,
builder: (_, value, ___) {
return ListItem.switchItem(
leading: Icon(
Icons.contrast,
color: context.colorScheme.primary,
),
title: Text(appLocalizations.prueBlackMode),
delegate: SwitchDelegate(
value: value,
onChanged: (value) {
final appController = globalState.appController;
appController.config.themeProps =
appController.config.themeProps.copyWith(
prueBlack: value,
);
},
),
);
},
),
),
const SizedBox(
height: 64,
),
],
); );
} }
} }

View File

@@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/about.dart'; import 'package:fl_clash/fragments/about.dart';
import 'package:fl_clash/fragments/access.dart'; import 'package:fl_clash/fragments/access.dart';
import 'package:fl_clash/fragments/application_setting.dart'; import 'package:fl_clash/fragments/application_setting.dart';
@@ -8,33 +7,35 @@ import 'package:fl_clash/fragments/config/config.dart';
import 'package:fl_clash/fragments/hotkey.dart'; import 'package:fl_clash/fragments/hotkey.dart';
import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'backup_and_recovery.dart'; import 'backup_and_recovery.dart';
import 'theme.dart'; import 'theme.dart';
import 'package:path/path.dart' show dirname, join; import 'package:path/path.dart' show dirname, join;
class ToolsFragment extends StatefulWidget { class ToolsFragment extends ConsumerStatefulWidget {
const ToolsFragment({super.key}); const ToolsFragment({super.key});
@override @override
State<ToolsFragment> createState() => _ToolboxFragmentState(); ConsumerState<ToolsFragment> createState() => _ToolboxFragmentState();
} }
class _ToolboxFragmentState extends State<ToolsFragment> { class _ToolboxFragmentState extends ConsumerState<ToolsFragment> {
_buildNavigationMenuItem(NavigationItem navigationItem) { _buildNavigationMenuItem(NavigationItem navigationItem) {
return ListItem.open( return ListItem.open(
leading: navigationItem.icon, leading: navigationItem.icon,
title: Text(Intl.message(navigationItem.label)), title: Text(Intl.message(navigationItem.label.name)),
subtitle: navigationItem.description != null subtitle: navigationItem.description != null
? Text(Intl.message(navigationItem.description!)) ? Text(Intl.message(navigationItem.description!))
: null, : null,
delegate: OpenDelegate( delegate: OpenDelegate(
title: Intl.message(navigationItem.label), title: Intl.message(navigationItem.label.name),
widget: navigationItem.fragment, widget: navigationItem.fragment,
extendPageWidth: 360,
), ),
); );
} }
@@ -54,34 +55,12 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
); );
} }
String _getLocaleString(Locale? locale) {
if (locale == null) return appLocalizations.defaultText;
return Intl.message(locale.toString());
}
List<Widget> _getOtherList() { List<Widget> _getOtherList() {
return generateSection( return generateSection(
title: appLocalizations.other, title: appLocalizations.other,
items: [ items: [
ListItem( _DisclaimerItem(),
leading: const Icon(Icons.gavel), _InfoItem(),
title: Text(appLocalizations.disclaimer),
onTap: () async {
final isDisclaimerAccepted =
await globalState.appController.showDisclaimer();
if (!isDisclaimerAccepted) {
globalState.appController.handleExit();
}
},
),
ListItem.open(
leading: const Icon(Icons.info),
title: Text(appLocalizations.about),
delegate: OpenDelegate(
title: appLocalizations.about,
widget: const AboutFragment(),
),
),
], ],
); );
} }
@@ -90,142 +69,233 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
return generateSection( return generateSection(
title: appLocalizations.settings, title: appLocalizations.settings,
items: [ items: [
Selector<Config, String?>( _LocaleItem(),
selector: (_, config) => config.appSetting.locale, _ThemeItem(),
builder: (_, localeString, __) { _BackupItem(),
final subTitle = localeString ?? appLocalizations.defaultText; if (system.isDesktop) _HotkeyItem(),
final currentLocale = other.getLocaleForString(localeString); if (Platform.isWindows) _LoopbackItem(),
return ListItem<Locale?>.options( if (Platform.isAndroid) _AccessItem(),
leading: const Icon(Icons.language_outlined), _OverrideItem(),
title: Text(appLocalizations.language), _SettingItem(),
subtitle: Text(Intl.message(subTitle)),
delegate: OptionsDelegate(
title: appLocalizations.language,
options: [null, ...AppLocalizations.delegate.supportedLocales],
onChanged: (Locale? value) {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
locale: value?.toString(),
);
},
textBuilder: (locale) => _getLocaleString(locale),
value: currentLocale,
),
);
},
),
ListItem.open(
leading: const Icon(Icons.style),
title: Text(appLocalizations.theme),
subtitle: Text(appLocalizations.themeDesc),
delegate: OpenDelegate(
title: appLocalizations.theme,
widget: const ThemeFragment(),
extendPageWidth: 360,
),
),
ListItem.open(
leading: const Icon(Icons.cloud_sync),
title: Text(appLocalizations.backupAndRecovery),
subtitle: Text(appLocalizations.backupAndRecoveryDesc),
delegate: OpenDelegate(
title: appLocalizations.backupAndRecovery,
widget: const BackupAndRecovery(),
),
),
if (system.isDesktop)
ListItem.open(
leading: const Icon(Icons.keyboard),
title: Text(appLocalizations.hotkeyManagement),
subtitle: Text(appLocalizations.hotkeyManagementDesc),
delegate: OpenDelegate(
title: appLocalizations.hotkeyManagement,
widget: const HotKeyFragment(),
),
),
if (Platform.isWindows)
ListItem(
leading: const Icon(Icons.lock),
title: Text(appLocalizations.loopback),
subtitle: Text(appLocalizations.loopbackDesc),
onTap: () {
windows?.runas(
'"${join(dirname(Platform.resolvedExecutable), "EnableLoopback.exe")}"',
"",
);
},
),
if (Platform.isAndroid)
ListItem.open(
leading: const Icon(Icons.view_list),
title: Text(appLocalizations.accessControl),
subtitle: Text(appLocalizations.accessControlDesc),
delegate: OpenDelegate(
title: appLocalizations.appAccessControl,
widget: const AccessFragment(),
),
),
ListItem.open(
leading: const Icon(Icons.edit),
title: Text(appLocalizations.override),
subtitle: Text(appLocalizations.overrideDesc),
delegate: OpenDelegate(
title: appLocalizations.override,
widget: const ConfigFragment(),
),
),
ListItem.open(
leading: const Icon(Icons.settings),
title: Text(appLocalizations.application),
subtitle: Text(appLocalizations.applicationDesc),
delegate: OpenDelegate(
title: appLocalizations.application,
widget: const ApplicationSettingFragment(),
),
),
], ],
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LocaleBuilder( ref.watch(appSettingProvider.select((state) => state.locale));
builder: (_) { final items = [
final items = [ Consumer(
Selector<AppState, MoreToolsSelectorState>( builder: (_, ref, __) {
selector: (_, appState) { final state = ref.watch(moreToolsSelectorStateProvider);
return MoreToolsSelectorState( if (state.navigationItems.isEmpty) {
navigationItems: appState.viewMode == ViewMode.mobile return Container();
? appState.navigationItems.where( }
(element) { return Column(
return element.modes children: [
.contains(NavigationItemMode.more); ListHeader(title: appLocalizations.more),
}, _buildNavigationMenu(state.navigationItems)
).toList() ],
: [], );
},
),
..._getSettingList(),
..._getOtherList(),
];
return ListView.builder(
itemCount: items.length,
itemBuilder: (_, index) => items[index],
padding: const EdgeInsets.only(bottom: 20),
);
}
}
class _LocaleItem extends ConsumerWidget {
const _LocaleItem();
String _getLocaleString(Locale? locale) {
if (locale == null) return appLocalizations.defaultText;
return Intl.message(locale.toString());
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale =
ref.watch(appSettingProvider.select((state) => state.locale));
final subTitle = locale ?? appLocalizations.defaultText;
final currentLocale = other.getLocaleForString(locale);
return ListItem<Locale?>.options(
leading: const Icon(Icons.language_outlined),
title: Text(appLocalizations.language),
subtitle: Text(Intl.message(subTitle)),
delegate: OptionsDelegate(
title: appLocalizations.language,
options: [null, ...AppLocalizations.delegate.supportedLocales],
onChanged: (Locale? locale) {
ref.read(appSettingProvider.notifier).updateState(
(state) => state.copyWith(locale: locale?.toString()),
); );
}, },
builder: (_, state, __) { textBuilder: (locale) => _getLocaleString(locale),
if (state.navigationItems.isEmpty) { value: currentLocale,
return Container(); ),
} );
return Column( }
children: [ }
ListHeader(title: appLocalizations.more),
_buildNavigationMenu(state.navigationItems) class _ThemeItem extends StatelessWidget {
], const _ThemeItem();
);
}, @override
), Widget build(BuildContext context) {
..._getSettingList(), return ListItem.open(
..._getOtherList(), leading: const Icon(Icons.style),
]; title: Text(appLocalizations.theme),
return ListView.builder( subtitle: Text(appLocalizations.themeDesc),
itemCount: items.length, delegate: OpenDelegate(
itemBuilder: (_, index) => items[index], title: appLocalizations.theme,
padding: const EdgeInsets.only(bottom: 20), widget: const ThemeFragment(),
extendPageWidth: 360,
),
);
}
}
class _BackupItem extends StatelessWidget {
const _BackupItem();
@override
Widget build(BuildContext context) {
return ListItem.open(
leading: const Icon(Icons.cloud_sync),
title: Text(appLocalizations.backupAndRecovery),
subtitle: Text(appLocalizations.backupAndRecoveryDesc),
delegate: OpenDelegate(
title: appLocalizations.backupAndRecovery,
widget: const BackupAndRecovery(),
),
);
}
}
class _HotkeyItem extends StatelessWidget {
const _HotkeyItem();
@override
Widget build(BuildContext context) {
return ListItem.open(
leading: const Icon(Icons.keyboard),
title: Text(appLocalizations.hotkeyManagement),
subtitle: Text(appLocalizations.hotkeyManagementDesc),
delegate: OpenDelegate(
title: appLocalizations.hotkeyManagement,
widget: const HotKeyFragment(),
),
);
}
}
class _LoopbackItem extends StatelessWidget {
const _LoopbackItem();
@override
Widget build(BuildContext context) {
return ListItem(
leading: const Icon(Icons.lock),
title: Text(appLocalizations.loopback),
subtitle: Text(appLocalizations.loopbackDesc),
onTap: () {
windows?.runas(
'"${join(dirname(Platform.resolvedExecutable), "EnableLoopback.exe")}"',
"",
); );
}, },
); );
} }
} }
class _AccessItem extends StatelessWidget {
const _AccessItem();
@override
Widget build(BuildContext context) {
return ListItem.open(
leading: const Icon(Icons.view_list),
title: Text(appLocalizations.accessControl),
subtitle: Text(appLocalizations.accessControlDesc),
delegate: OpenDelegate(
title: appLocalizations.appAccessControl,
widget: const AccessFragment(),
),
);
}
}
class _OverrideItem extends StatelessWidget {
const _OverrideItem();
@override
Widget build(BuildContext context) {
return ListItem.open(
leading: const Icon(Icons.edit),
title: Text(appLocalizations.override),
subtitle: Text(appLocalizations.overrideDesc),
delegate: OpenDelegate(
title: appLocalizations.override,
widget: const ConfigFragment(),
),
);
}
}
class _SettingItem extends StatelessWidget {
const _SettingItem();
@override
Widget build(BuildContext context) {
return ListItem.open(
leading: const Icon(Icons.settings),
title: Text(appLocalizations.application),
subtitle: Text(appLocalizations.applicationDesc),
delegate: OpenDelegate(
title: appLocalizations.application,
widget: const ApplicationSettingFragment(),
),
);
}
}
class _DisclaimerItem extends StatelessWidget {
const _DisclaimerItem();
@override
Widget build(BuildContext context) {
return ListItem(
leading: const Icon(Icons.gavel),
title: Text(appLocalizations.disclaimer),
onTap: () async {
final isDisclaimerAccepted =
await globalState.appController.showDisclaimer();
if (!isDisclaimerAccepted) {
globalState.appController.handleExit();
}
},
);
}
}
class _InfoItem extends StatelessWidget {
const _InfoItem();
@override
Widget build(BuildContext context) {
return ListItem.open(
leading: const Icon(Icons.info),
title: Text(appLocalizations.about),
delegate: OpenDelegate(
title: appLocalizations.about,
widget: const AboutFragment(),
),
);
}
}

View File

@@ -341,5 +341,7 @@
"nullProxies": "No proxies", "nullProxies": "No proxies",
"copySuccess": "Copy success", "copySuccess": "Copy success",
"copyLink": "Copy link", "copyLink": "Copy link",
"exportFile": "Export file" "exportFile": "Export file",
"cacheCorrupt": "The cache is corrupt. Do you want to clear it?",
"detectionTip": "Relying on third-party api is for reference only"
} }

View File

@@ -341,5 +341,7 @@
"nullProxies": "暂无代理", "nullProxies": "暂无代理",
"copySuccess": "复制成功", "copySuccess": "复制成功",
"copyLink": "复制链接", "copyLink": "复制链接",
"exportFile": "导出文件" "exportFile": "导出文件",
"cacheCorrupt": "缓存已损坏,是否清空?",
"detectionTip": "依赖第三方api仅供参考"
} }

View File

@@ -39,8 +39,10 @@ MessageLookupByLibrary? _findExact(String localeName) {
/// User programs should call this before using [localeName] for messages. /// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) { Future<bool> initializeMessages(String localeName) {
var availableLocale = Intl.verifiedLocale( var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null, localeName,
onFailure: (_) => null); (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null,
);
if (availableLocale == null) { if (availableLocale == null) {
return new SynchronousFuture(false); return new SynchronousFuture(false);
} }
@@ -60,8 +62,11 @@ bool _messagesExistFor(String locale) {
} }
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale = var actualLocale = Intl.verifiedLocale(
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); locale,
_messagesExistFor,
onFailure: (_) => null,
);
if (actualLocale == null) return null; if (actualLocale == null) return null;
return _findExact(actualLocale); return _findExact(actualLocale);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -22,390 +22,392 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"about": MessageLookupByLibrary.simpleMessage("关于"), "about": MessageLookupByLibrary.simpleMessage("关于"),
"accessControl": MessageLookupByLibrary.simpleMessage("访问控制"), "accessControl": MessageLookupByLibrary.simpleMessage("访问控制"),
"accessControlAllowDesc": "accessControlAllowDesc": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage("只允许选中应用进入VPN"), "只允许选中应用进入VPN",
"accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"), ),
"accessControlNotAllowDesc": "accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"),
MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"), "accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage(
"account": MessageLookupByLibrary.simpleMessage("账号"), "选中应用将会被排除在VPN之外",
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"), ),
"action": MessageLookupByLibrary.simpleMessage("操作"), "account": MessageLookupByLibrary.simpleMessage("账号"),
"action_mode": MessageLookupByLibrary.simpleMessage("切换模式"), "accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
"action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"), "action": MessageLookupByLibrary.simpleMessage("操作"),
"action_start": MessageLookupByLibrary.simpleMessage("启动/停止"), "action_mode": MessageLookupByLibrary.simpleMessage("切换模式"),
"action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"), "action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"), "action_start": MessageLookupByLibrary.simpleMessage("启动/停止"),
"add": MessageLookupByLibrary.simpleMessage("添加"), "action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"address": MessageLookupByLibrary.simpleMessage("地址"), "action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"),
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), "add": MessageLookupByLibrary.simpleMessage("添加"),
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), "address": MessageLookupByLibrary.simpleMessage("地址"),
"adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理员自启动"), "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
"adminAutoLaunchDesc": "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
MessageLookupByLibrary.simpleMessage("使用管理员模式开机自启动"), "adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理员自启动"),
"ago": MessageLookupByLibrary.simpleMessage(""), "adminAutoLaunchDesc": MessageLookupByLibrary.simpleMessage("使用管理员模式开机自启动"),
"agree": MessageLookupByLibrary.simpleMessage("同意"), "ago": MessageLookupByLibrary.simpleMessage(""),
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"), "agree": MessageLookupByLibrary.simpleMessage("同意"),
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"), "allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
"allowBypassDesc": "allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"), "allowBypassDesc": MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"), "allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"), "allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
"app": MessageLookupByLibrary.simpleMessage("应用"), "app": MessageLookupByLibrary.simpleMessage("应用"),
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"), "appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
"appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"), "appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"),
"application": MessageLookupByLibrary.simpleMessage("应用程序"), "application": MessageLookupByLibrary.simpleMessage("应用程序"),
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"), "applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
"auto": MessageLookupByLibrary.simpleMessage("自动"), "auto": MessageLookupByLibrary.simpleMessage("自动"),
"autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"), "autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"),
"autoCheckUpdateDesc": "autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"),
MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"), "autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"),
"autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"), "autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
"autoCloseConnectionsDesc": "切换节点后自动关闭连接",
MessageLookupByLibrary.simpleMessage("切换节点后自动关闭连接"), ),
"autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"), "autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"),
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"), "autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"),
"autoRun": MessageLookupByLibrary.simpleMessage("自动运行"), "autoRun": MessageLookupByLibrary.simpleMessage("自动运行"),
"autoRunDesc": MessageLookupByLibrary.simpleMessage("应用打开时自动运行"), "autoRunDesc": MessageLookupByLibrary.simpleMessage("应用打开时自动运行"),
"autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"), "autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"),
"autoUpdateInterval": "autoUpdateInterval": MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"),
MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"), "backup": MessageLookupByLibrary.simpleMessage("备份"),
"backup": MessageLookupByLibrary.simpleMessage("备份"), "backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"),
"backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"), "backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage(
"backupAndRecoveryDesc": "通过WebDAV或者文件同步数据",
MessageLookupByLibrary.simpleMessage("通过WebDAV或者文件同步数据"), ),
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"), "backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
"bind": MessageLookupByLibrary.simpleMessage("绑定"), "bind": MessageLookupByLibrary.simpleMessage("绑定"),
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"), "blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
"bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"), "bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"),
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"), "bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"),
"cancel": MessageLookupByLibrary.simpleMessage("取消"), "cacheCorrupt": MessageLookupByLibrary.simpleMessage("缓存已损坏,是否清空?"),
"cancelFilterSystemApp": "cancel": MessageLookupByLibrary.simpleMessage("取消"),
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"), "cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
"checkError": MessageLookupByLibrary.simpleMessage("检测失败"), "checkError": MessageLookupByLibrary.simpleMessage("检测失败"),
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
"checking": MessageLookupByLibrary.simpleMessage("检测中..."), "checking": MessageLookupByLibrary.simpleMessage("检测中..."),
"clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"), "clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"),
"clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"), "clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"),
"columns": MessageLookupByLibrary.simpleMessage("列数"), "columns": MessageLookupByLibrary.simpleMessage("列数"),
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"), "compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
"compatibleDesc": "compatibleDesc": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"), "开启将失去部分应用能力获得全量的Clash的支持",
"confirm": MessageLookupByLibrary.simpleMessage("确定"), ),
"connections": MessageLookupByLibrary.simpleMessage("连接"), "confirm": MessageLookupByLibrary.simpleMessage("确定"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"), "connections": MessageLookupByLibrary.simpleMessage("连接"),
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"), "connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
"copy": MessageLookupByLibrary.simpleMessage("复制"), "connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"), "copy": MessageLookupByLibrary.simpleMessage("复制"),
"copyLink": MessageLookupByLibrary.simpleMessage("复制链接"), "copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
"copySuccess": MessageLookupByLibrary.simpleMessage("复制成功"), "copyLink": MessageLookupByLibrary.simpleMessage("复制链接"),
"core": MessageLookupByLibrary.simpleMessage("内核"), "copySuccess": MessageLookupByLibrary.simpleMessage("复制成功"),
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"), "core": MessageLookupByLibrary.simpleMessage("内核"),
"country": MessageLookupByLibrary.simpleMessage("区域"), "coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
"create": MessageLookupByLibrary.simpleMessage("创建"), "country": MessageLookupByLibrary.simpleMessage("区域"),
"cut": MessageLookupByLibrary.simpleMessage("剪切"), "create": MessageLookupByLibrary.simpleMessage("创建"),
"dark": MessageLookupByLibrary.simpleMessage("深色"), "cut": MessageLookupByLibrary.simpleMessage("剪切"),
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"), "dark": MessageLookupByLibrary.simpleMessage("深色"),
"days": MessageLookupByLibrary.simpleMessage(""), "dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
"defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"), "days": MessageLookupByLibrary.simpleMessage(""),
"defaultNameserverDesc": "defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"),
MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"), "defaultNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"),
"defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"), "defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"),
"defaultText": MessageLookupByLibrary.simpleMessage("默认"), "defaultText": MessageLookupByLibrary.simpleMessage("默认"),
"delay": MessageLookupByLibrary.simpleMessage("延迟"), "delay": MessageLookupByLibrary.simpleMessage("延迟"),
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"), "delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
"delete": MessageLookupByLibrary.simpleMessage("删除"), "delete": MessageLookupByLibrary.simpleMessage("删除"),
"deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"), "deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"),
"desc": MessageLookupByLibrary.simpleMessage( "desc": MessageLookupByLibrary.simpleMessage(
"基于ClashMeta的多平台代理客户端简单易用开源无广告。"), "基于ClashMeta的多平台代理客户端简单易用开源无广告。",
"direct": MessageLookupByLibrary.simpleMessage("直连"), ),
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"), "detectionTip": MessageLookupByLibrary.simpleMessage("依赖第三方api仅供参考"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage( "direct": MessageLookupByLibrary.simpleMessage("直连"),
"本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。"), "disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"), "disclaimerDesc": MessageLookupByLibrary.simpleMessage(
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"), "本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。",
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"), ),
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"), "discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"),
"doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"), "discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
"domain": MessageLookupByLibrary.simpleMessage("域名"), "dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
"download": MessageLookupByLibrary.simpleMessage("下载"), "dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"),
"edit": MessageLookupByLibrary.simpleMessage("编辑"), "doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"),
"en": MessageLookupByLibrary.simpleMessage("英语"), "domain": MessageLookupByLibrary.simpleMessage("域名"),
"entries": MessageLookupByLibrary.simpleMessage("个条目"), "download": MessageLookupByLibrary.simpleMessage("下载"),
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"), "edit": MessageLookupByLibrary.simpleMessage("编辑"),
"excludeDesc": "en": MessageLookupByLibrary.simpleMessage("英语"),
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"), "entries": MessageLookupByLibrary.simpleMessage("个条目"),
"exit": MessageLookupByLibrary.simpleMessage("退出"), "exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
"expand": MessageLookupByLibrary.simpleMessage("标准"), "excludeDesc": MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"), "exit": MessageLookupByLibrary.simpleMessage("退出"),
"exportFile": MessageLookupByLibrary.simpleMessage("导出文件"), "expand": MessageLookupByLibrary.simpleMessage("标准"),
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"), "expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"), "exportFile": MessageLookupByLibrary.simpleMessage("导出文件"),
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"), "exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
"externalControllerDesc": "exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"), "externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalLink": MessageLookupByLibrary.simpleMessage("外部链接"), "externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"), "开启后将可以通过9090端口控制Clash内核",
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"), ),
"fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"), "externalLink": MessageLookupByLibrary.simpleMessage("外部链接"),
"fallback": MessageLookupByLibrary.simpleMessage("Fallback"), "externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"), "fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"),
"fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"), "fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"),
"file": MessageLookupByLibrary.simpleMessage("文件"), "fallback": MessageLookupByLibrary.simpleMessage("Fallback"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), "fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"),
"fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"), "fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"),
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"), "file": MessageLookupByLibrary.simpleMessage("文件"),
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"), "fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"findProcessModeDesc": "fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"),
MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"), "filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
"fontFamily": MessageLookupByLibrary.simpleMessage("字体"), "findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"), "findProcessModeDesc": MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
"general": MessageLookupByLibrary.simpleMessage("基础"), "fontFamily": MessageLookupByLibrary.simpleMessage("字体"),
"generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"), "fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"), "general": MessageLookupByLibrary.simpleMessage("基础"),
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"), "generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"),
"geodataLoaderDesc": "geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"), "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
"geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
"global": MessageLookupByLibrary.simpleMessage("全局"), "geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"),
"go": MessageLookupByLibrary.simpleMessage("前往"), "global": MessageLookupByLibrary.simpleMessage("全局"),
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"), "go": MessageLookupByLibrary.simpleMessage("前往"),
"hasCacheChange": MessageLookupByLibrary.simpleMessage("是否缓存修改"), "goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"), "hasCacheChange": MessageLookupByLibrary.simpleMessage("是否缓存修改"),
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"), "hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"), "hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"),
"hotkeyManagementDesc": "hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"),
MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"), "hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"),
"hours": MessageLookupByLibrary.simpleMessage("小时"), "hours": MessageLookupByLibrary.simpleMessage("小时"),
"icon": MessageLookupByLibrary.simpleMessage("图片"), "icon": MessageLookupByLibrary.simpleMessage("图片"),
"iconConfiguration": MessageLookupByLibrary.simpleMessage("图片配置"), "iconConfiguration": MessageLookupByLibrary.simpleMessage("图片配置"),
"iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"), "iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"), "importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"), "infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
"init": MessageLookupByLibrary.simpleMessage("初始化"), "init": MessageLookupByLibrary.simpleMessage("初始化"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"), "inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"),
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"), "intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"), "ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"), "ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"), "just": MessageLookupByLibrary.simpleMessage("刚刚"),
"keepAliveIntervalDesc": "keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"), "key": MessageLookupByLibrary.simpleMessage(""),
"key": MessageLookupByLibrary.simpleMessage(""), "language": MessageLookupByLibrary.simpleMessage("语言"),
"language": MessageLookupByLibrary.simpleMessage("语言"), "layout": MessageLookupByLibrary.simpleMessage("布局"),
"layout": MessageLookupByLibrary.simpleMessage("布局"), "light": MessageLookupByLibrary.simpleMessage("浅色"),
"light": MessageLookupByLibrary.simpleMessage("浅色"), "list": MessageLookupByLibrary.simpleMessage("列表"),
"list": MessageLookupByLibrary.simpleMessage("列表"), "local": MessageLookupByLibrary.simpleMessage("本地"),
"local": MessageLookupByLibrary.simpleMessage("本地"), "localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"),
"localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"), "localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"),
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"), "logLevel": MessageLookupByLibrary.simpleMessage("日志等级"),
"logLevel": MessageLookupByLibrary.simpleMessage("日志等级"), "logcat": MessageLookupByLibrary.simpleMessage("日志捕获"),
"logcat": MessageLookupByLibrary.simpleMessage("日志捕获"), "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), "logs": MessageLookupByLibrary.simpleMessage("日志"),
"logs": MessageLookupByLibrary.simpleMessage("日志"), "logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"), "loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"), "loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"), "loose": MessageLookupByLibrary.simpleMessage("宽松"),
"loose": MessageLookupByLibrary.simpleMessage("宽松"), "memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"),
"memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"), "min": MessageLookupByLibrary.simpleMessage("最小"),
"min": MessageLookupByLibrary.simpleMessage("最小"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), "minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
"minimizeOnExitDesc": "minutes": MessageLookupByLibrary.simpleMessage("分钟"),
MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"), "mode": MessageLookupByLibrary.simpleMessage("模式"),
"minutes": MessageLookupByLibrary.simpleMessage("分钟"), "months": MessageLookupByLibrary.simpleMessage(""),
"mode": MessageLookupByLibrary.simpleMessage("模式"), "more": MessageLookupByLibrary.simpleMessage("更多"),
"months": MessageLookupByLibrary.simpleMessage(""), "name": MessageLookupByLibrary.simpleMessage("名称"),
"more": MessageLookupByLibrary.simpleMessage("更多"), "nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"),
"name": MessageLookupByLibrary.simpleMessage("名称"), "nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"),
"nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"), "nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域名"),
"nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"), "nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"),
"nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域名"), "nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"),
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"), "network": MessageLookupByLibrary.simpleMessage("网络"),
"nameserverPolicyDesc": "networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"),
MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"), "networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"),
"network": MessageLookupByLibrary.simpleMessage("网络"), "networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"),
"networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"), "noData": MessageLookupByLibrary.simpleMessage("暂无数据"),
"networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"), "noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
"networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"), "noIcon": MessageLookupByLibrary.simpleMessage("无图标"),
"noData": MessageLookupByLibrary.simpleMessage("暂无数据"), "noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"), "noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
"noIcon": MessageLookupByLibrary.simpleMessage("图标"), "noNetwork": MessageLookupByLibrary.simpleMessage("网络"),
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"), "noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"), "noProxyDesc": MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"), "notEmpty": MessageLookupByLibrary.simpleMessage("不能为空"),
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"), "notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"),
"noProxyDesc": "nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"),
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"), "nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"),
"notEmpty": MessageLookupByLibrary.simpleMessage("不能为空"), "nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
"notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"), "nullProfileDesc": MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"), "nullProxies": MessageLookupByLibrary.simpleMessage("暂无代理"),
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"), "nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"), "oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
"nullProfileDesc": "onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"),
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"), "onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
"nullProxies": MessageLookupByLibrary.simpleMessage("暂无代理"), "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"), "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage(
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"), "开启后,将只统计代理流量",
"onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"), ),
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"), "options": MessageLookupByLibrary.simpleMessage("选项"),
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"), "other": MessageLookupByLibrary.simpleMessage("其他"),
"onlyStatisticsProxyDesc": "otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"),
MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"), "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
"options": MessageLookupByLibrary.simpleMessage("选项"), "override": MessageLookupByLibrary.simpleMessage("覆写"),
"other": MessageLookupByLibrary.simpleMessage("其他"), "overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
"otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"), "overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"),
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "overrideDnsDesc": MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"),
"override": MessageLookupByLibrary.simpleMessage("覆写"), "password": MessageLookupByLibrary.simpleMessage("密码"),
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"), "passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
"overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"), "paste": MessageLookupByLibrary.simpleMessage("粘贴"),
"overrideDnsDesc": "pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"), "pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage(
"password": MessageLookupByLibrary.simpleMessage("密码"), "请输入管理员密码",
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"), ),
"paste": MessageLookupByLibrary.simpleMessage("粘贴"), "pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"), "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(
"pleaseInputAdminPassword": "请上传有效的二维码",
MessageLookupByLibrary.simpleMessage("请输入管理员密码"), ),
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"), "port": MessageLookupByLibrary.simpleMessage("端口"),
"pleaseUploadValidQrcode": "preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
MessageLookupByLibrary.simpleMessage("上传有效的二维码"), "pressKeyboard": MessageLookupByLibrary.simpleMessage("按下按键"),
"port": MessageLookupByLibrary.simpleMessage("端口"), "preview": MessageLookupByLibrary.simpleMessage("预览"),
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"), "profile": MessageLookupByLibrary.simpleMessage("配置"),
"pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"), "profileAutoUpdateIntervalInvalidValidationDesc":
"preview": MessageLookupByLibrary.simpleMessage("预览"), MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"),
"profile": MessageLookupByLibrary.simpleMessage("配置"), "profileAutoUpdateIntervalNullValidationDesc":
"profileAutoUpdateIntervalInvalidValidationDesc": MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"),
MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"), "profileHasUpdate": MessageLookupByLibrary.simpleMessage(
"profileAutoUpdateIntervalNullValidationDesc": "配置文件已经修改,是否关闭自动更新 ",
MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"), ),
"profileHasUpdate": "profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage("配置文件已经修改,是否关闭自动更新 "), "请输入配置名称",
"profileNameNullValidationDesc": ),
MessageLookupByLibrary.simpleMessage("请输入配置名称"), "profileParseErrorDesc": MessageLookupByLibrary.simpleMessage("配置文件解析错误"),
"profileParseErrorDesc": "profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage("配置文件解析错误"), "请输入有效配置URL",
"profileUrlInvalidValidationDesc": ),
MessageLookupByLibrary.simpleMessage("请输入有效配置URL"), "profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"profileUrlNullValidationDesc": "请输入配置URL",
MessageLookupByLibrary.simpleMessage("请输入配置URL"), ),
"profiles": MessageLookupByLibrary.simpleMessage("配置"), "profiles": MessageLookupByLibrary.simpleMessage("配置"),
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"), "profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
"project": MessageLookupByLibrary.simpleMessage("项目"), "project": MessageLookupByLibrary.simpleMessage("项目"),
"providers": MessageLookupByLibrary.simpleMessage("提供者"), "providers": MessageLookupByLibrary.simpleMessage("提供者"),
"proxies": MessageLookupByLibrary.simpleMessage("代理"), "proxies": MessageLookupByLibrary.simpleMessage("代理"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"), "proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"), "proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"),
"proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"), "proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"),
"proxyNameserverDesc": "proxyNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"),
MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"), "proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"),
"proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"), "prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"),
"prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"), "recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"), "recoveryProfiles": MessageLookupByLibrary.simpleMessage("恢复配置文件"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("恢复配置文件"), "recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"), "regExp": MessageLookupByLibrary.simpleMessage("正则"),
"regExp": MessageLookupByLibrary.simpleMessage("正则"), "remote": MessageLookupByLibrary.simpleMessage("远程"),
"remote": MessageLookupByLibrary.simpleMessage("远程"), "remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"), "remoteRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
"remoteRecoveryDesc": "remove": MessageLookupByLibrary.simpleMessage("移除"),
MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"), "requests": MessageLookupByLibrary.simpleMessage("请求"),
"remove": MessageLookupByLibrary.simpleMessage("移除"), "requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
"requests": MessageLookupByLibrary.simpleMessage("请求"), "reset": MessageLookupByLibrary.simpleMessage("重置"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"), "resetTip": MessageLookupByLibrary.simpleMessage("确定要重置吗?"),
"reset": MessageLookupByLibrary.simpleMessage("重置"), "resources": MessageLookupByLibrary.simpleMessage("资源"),
"resetTip": MessageLookupByLibrary.simpleMessage("确定要重置吗?"), "resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"resources": MessageLookupByLibrary.simpleMessage("资源"), "respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"), "respectRulesDesc": MessageLookupByLibrary.simpleMessage(
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"), "DNS连接跟随rules,需配置proxy-server-nameserver",
"respectRulesDesc": MessageLookupByLibrary.simpleMessage( ),
"DNS连接跟随rules,需配置proxy-server-nameserver"), "routeAddress": MessageLookupByLibrary.simpleMessage("路由地址"),
"routeAddress": MessageLookupByLibrary.simpleMessage("路由地址"), "routeAddressDesc": MessageLookupByLibrary.simpleMessage("配置监听路由地址"),
"routeAddressDesc": MessageLookupByLibrary.simpleMessage("配置监听路由地址"), "routeMode": MessageLookupByLibrary.simpleMessage("路由模式"),
"routeMode": MessageLookupByLibrary.simpleMessage("路由模式"), "routeMode_bypassPrivate": MessageLookupByLibrary.simpleMessage("绕过私有路由地址"),
"routeMode_bypassPrivate": "routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"),
MessageLookupByLibrary.simpleMessage("绕过私有路由地址"), "rule": MessageLookupByLibrary.simpleMessage("规则"),
"routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"), "ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"),
"rule": MessageLookupByLibrary.simpleMessage("规则"), "save": MessageLookupByLibrary.simpleMessage("保存"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"), "search": MessageLookupByLibrary.simpleMessage("搜索"),
"save": MessageLookupByLibrary.simpleMessage("保存"), "seconds": MessageLookupByLibrary.simpleMessage(""),
"search": MessageLookupByLibrary.simpleMessage("搜索"), "selectAll": MessageLookupByLibrary.simpleMessage("全选"),
"seconds": MessageLookupByLibrary.simpleMessage(""), "selected": MessageLookupByLibrary.simpleMessage("已选择"),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"), "settings": MessageLookupByLibrary.simpleMessage("设置"),
"selected": MessageLookupByLibrary.simpleMessage("已选择"), "show": MessageLookupByLibrary.simpleMessage("显示"),
"settings": MessageLookupByLibrary.simpleMessage("设置"), "shrink": MessageLookupByLibrary.simpleMessage("紧凑"),
"show": MessageLookupByLibrary.simpleMessage("显示"), "silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"),
"shrink": MessageLookupByLibrary.simpleMessage("紧凑"), "silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
"silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"), "size": MessageLookupByLibrary.simpleMessage("尺寸"),
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"), "sort": MessageLookupByLibrary.simpleMessage("排序"),
"size": MessageLookupByLibrary.simpleMessage("尺寸"), "source": MessageLookupByLibrary.simpleMessage("来源"),
"sort": MessageLookupByLibrary.simpleMessage("排序"), "stackMode": MessageLookupByLibrary.simpleMessage("栈模式"),
"source": MessageLookupByLibrary.simpleMessage("来源"), "standard": MessageLookupByLibrary.simpleMessage("标准"),
"stackMode": MessageLookupByLibrary.simpleMessage("栈模式"), "start": MessageLookupByLibrary.simpleMessage("启动"),
"standard": MessageLookupByLibrary.simpleMessage("标准"), "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
"start": MessageLookupByLibrary.simpleMessage("启动"), "status": MessageLookupByLibrary.simpleMessage("状态"),
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), "statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"),
"status": MessageLookupByLibrary.simpleMessage("状态"), "stop": MessageLookupByLibrary.simpleMessage("暂停"),
"statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
"stop": MessageLookupByLibrary.simpleMessage("暂停"), "style": MessageLookupByLibrary.simpleMessage("风格"),
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), "submit": MessageLookupByLibrary.simpleMessage("提交"),
"style": MessageLookupByLibrary.simpleMessage("风格"), "sync": MessageLookupByLibrary.simpleMessage("同步"),
"submit": MessageLookupByLibrary.simpleMessage("提交"), "system": MessageLookupByLibrary.simpleMessage("系统"),
"sync": MessageLookupByLibrary.simpleMessage("同步"), "systemFont": MessageLookupByLibrary.simpleMessage("系统字体"),
"system": MessageLookupByLibrary.simpleMessage("系统"), "systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
"systemFont": MessageLookupByLibrary.simpleMessage("系统字体"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"),
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"), "tab": MessageLookupByLibrary.simpleMessage("标签页"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"), "tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
"tab": MessageLookupByLibrary.simpleMessage("标签页"), "tabAnimationDesc": MessageLookupByLibrary.simpleMessage(
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"), "开启后,主页选项卡将添加切换动画",
"tabAnimationDesc": ),
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"), "tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"), "tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"), "testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"), "theme": MessageLookupByLibrary.simpleMessage("主题"),
"theme": MessageLookupByLibrary.simpleMessage("主题"), "themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"), "themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"), "themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"), "threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"), "tight": MessageLookupByLibrary.simpleMessage("紧凑"),
"tight": MessageLookupByLibrary.simpleMessage("紧凑"), "time": MessageLookupByLibrary.simpleMessage("时间"),
"time": MessageLookupByLibrary.simpleMessage("时间"), "tip": MessageLookupByLibrary.simpleMessage("提示"),
"tip": MessageLookupByLibrary.simpleMessage("提示"), "toggle": MessageLookupByLibrary.simpleMessage("切换"),
"toggle": MessageLookupByLibrary.simpleMessage("切换"), "tools": MessageLookupByLibrary.simpleMessage("工具"),
"tools": MessageLookupByLibrary.simpleMessage("工具"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), "tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
"tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"), "tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"), "twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"), "unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage(
"unableToUpdateCurrentProfileDesc": "无法更新当前配置文件",
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"), ),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"),
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"), "unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"), "unknown": MessageLookupByLibrary.simpleMessage("未知"),
"update": MessageLookupByLibrary.simpleMessage("更新"), "update": MessageLookupByLibrary.simpleMessage("更新"),
"upload": MessageLookupByLibrary.simpleMessage("上传"), "upload": MessageLookupByLibrary.simpleMessage("上传"),
"url": MessageLookupByLibrary.simpleMessage("URL"), "url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"), "urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"),
"useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"), "useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"),
"useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"),
"value": MessageLookupByLibrary.simpleMessage(""), "value": MessageLookupByLibrary.simpleMessage(""),
"view": MessageLookupByLibrary.simpleMessage("查看"), "view": MessageLookupByLibrary.simpleMessage("查看"),
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"), "vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
"vpnEnableDesc": "vpnEnableDesc": MessageLookupByLibrary.simpleMessage(
MessageLookupByLibrary.simpleMessage("通过VpnService自动路由系统所有流量"), "通过VpnService自动路由系统所有流量",
"vpnSystemProxyDesc": ),
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"), "vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage(
"vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"), "为VpnService附加HTTP代理",
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"), ),
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"), "vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"),
"years": MessageLookupByLibrary.simpleMessage(""), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
"zh_CN": MessageLookupByLibrary.simpleMessage("中文简体") "whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),
}; "years": MessageLookupByLibrary.simpleMessage(""),
"zh_CN": MessageLookupByLibrary.simpleMessage("中文简体"),
};
} }

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More