Optimize performance

Update core

Optimize core stability

Fix linux tun authority check error

Fix some issues
This commit is contained in:
chen08209
2025-02-09 18:39:38 +08:00
parent 96328f66e9
commit 4e679f776e
138 changed files with 13013 additions and 8723 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,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

@@ -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

@@ -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) result.success(true)
} catch (e: RuntimeException) {
result.success(false)
}
} else { } else {
result.success(false) result.success(false)
} }

View File

@@ -68,7 +68,8 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} }
addDnsServer(options.dnsServerAddress) addDnsServer(options.dnsServerAddress)
setMtu(9000) setMtu(9000)
options.accessControl?.let { accessControl -> options.accessControl.let { accessControl ->
if (accessControl.enable) {
when (accessControl.mode) { when (accessControl.mode) {
AccessControlMode.acceptSelected -> { AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach { (accessControl.acceptList + packageName).forEach {
@@ -83,6 +84,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} }
} }
} }
}
setSession("FlClash") setSession("FlClash")
setBlocking(false) setBlocking(false)
if (Build.VERSION.SDK_INT >= 29) { if (Build.VERSION.SDK_INT >= 29) {

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

@@ -13,7 +13,6 @@ type ConfigExtendedParams struct {
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"
@@ -27,9 +28,7 @@ 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

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

@@ -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.homeDirPath; 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),
); );
} }
@@ -318,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>(

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,6 +30,7 @@ class ClashService extends ClashHandlerInterface {
} }
_initServer() async { _initServer() async {
runZonedGuarded(() async {
final address = !Platform.isWindows final address = !Platform.isWindows
? InternetAddress( ? InternetAddress(
unixSocketPath, unixSocketPath,
@@ -65,10 +69,22 @@ class ClashService extends ClashHandlerInterface {
}, },
); );
} }
}, (error, stack) {
commonPrint.log(error.toString());
if(error is SocketException){
globalState.showNotifier(error.toString());
globalState.appController.restartCore();
}
});
} }
@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

@@ -35,4 +35,5 @@ export 'tray.dart';
export 'window.dart'; export 'window.dart';
export 'windows.dart'; export 'windows.dart';
export 'render.dart'; export 'render.dart';
export 'view.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

@@ -63,6 +63,22 @@ class Throttler {
} }
} }
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();

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

@@ -2,9 +2,9 @@ import 'dart:collection';
class FixedList<T> { class FixedList<T> {
final int maxLength; final int maxLength;
final List<T> _list = []; final List<T> _list;
FixedList(this.maxLength); FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
add(T item) { add(T item) {
if (_list.length == maxLength) { if (_list.length == maxLength) {
@@ -13,11 +13,22 @@ class FixedList<T> {
_list.add(item); _list.add(item);
} }
clear() {
_list.clear();
}
List<T> get list => List.unmodifiable(_list); List<T> get list => List.unmodifiable(_list);
int get length => _list.length; int get length => _list.length;
T operator [](int index) => _list[index]; T operator [](int index) => _list[index];
FixedList<T> copyWith() {
return FixedList(
maxLength,
list: _list,
);
}
} }
class FixedMap<K, V> { class FixedMap<K, V> {

View File

@@ -4,19 +4,26 @@ 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( Size computeTextSize(
Text text, { Text text, {
double maxWidth = double.infinity, 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,

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(
key: GlobalObjectKey(
PageLabel.resources,
),
),
modes: [NavigationItemMode.more], 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

@@ -4,17 +4,18 @@ import 'dart:convert';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.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; Future<bool> get isInit async =>
await sharedPreferencesCompleter.future != null;
Preferences._internal() { Preferences._internal() {
SharedPreferences.getInstance().then((value) => sharedPreferencesCompleter.complete(value)) SharedPreferences.getInstance()
.then((value) => sharedPreferencesCompleter.complete(value))
.onError((_, __) => sharedPreferencesCompleter.complete(null)); .onError((_, __) => sharedPreferencesCompleter.complete(null));
} }
@@ -23,7 +24,6 @@ class Preferences {
return _instance!; return _instance!;
} }
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);
@@ -32,21 +32,12 @@ class Preferences {
return ClashConfig.fromJson(clashConfigMap); return ClashConfig.fromJson(clashConfigMap);
} }
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.setString(
clashConfigKey,
json.encode(clashConfig),
);
return true;
}
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);
return Config.fromJson(configMap); return Config.compatibleFromJson(configMap);
} }
Future<bool> saveConfig(Config config) async { Future<bool> saveConfig(Config config) async {
@@ -54,7 +45,13 @@ class Preferences {
return await preferences?.setString( return await preferences?.setString(
configKey, configKey,
json.encode(config), json.encode(config),
) ?? false; ) ??
false;
}
clearClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.remove(clashConfigKey);
} }
clearPreferences() async { clearPreferences() async {

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() {
@@ -50,7 +50,7 @@ class Render {
_dispatcher.onBeginFrame = _beginFrame; _dispatcher.onBeginFrame = _beginFrame;
_dispatcher.onDrawFrame = _drawFrame; _dispatcher.onDrawFrame = _drawFrame;
_dispatcher.scheduleFrame(); _dispatcher.scheduleFrame();
debugPrint("[App] resume"); 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(
.timeout(httpTimeoutDuration); responseType: ResponseType.json,
),
);
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,
), ),
) )

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,20 +0,0 @@
import 'package:flutter/material.dart';
import 'context.dart';
mixin ViewMixin<T extends StatefulWidget> on State<T> {
List<Widget> get actions => [];
Widget? get floatingActionButton => null;
initViewState() {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onSearch = onSearch;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
}
Function(String)? get onSearch => null;
Function(List<String>)? get onKeywordsUpdate => null;
}

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;
@@ -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,9 +322,9 @@ 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(),
@@ -230,8 +334,17 @@ class AppController {
} }
} }
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);
} }
@@ -346,7 +458,7 @@ class AppController {
title: appLocalizations.tip, title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.cacheCorrupt), message: TextSpan(text: appLocalizations.cacheCorrupt),
); );
if (res) { if (res == true) {
final file = File(await appPath.sharedPreferencesPath); final file = File(await appPath.sharedPreferencesPath);
final isExists = await file.exists(); final isExists = await file.exists();
if (isExists) { if (isExists) {
@@ -356,33 +468,43 @@ class AppController {
await handleExit(); 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 { init() async {
await _handlePreference(); await _handlePreference();
await _handlerDisclaimer(); await _handlerDisclaimer();
await globalState.initCore( await initCore();
appState: appState,
clashConfig: clashConfig,
config: config,
);
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) {
@@ -391,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,
@@ -414,8 +541,8 @@ 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);
@@ -476,8 +603,8 @@ 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);
}, },
@@ -490,7 +617,7 @@ class AppController {
} }
_handlerDisclaimer() async { _handlerDisclaimer() async {
if (config.appSetting.disclaimerAccepted) { if (_ref.read(appSettingProvider).disclaimerAccepted) {
return; return;
} }
final isDisclaimerAccepted = await showDisclaimer(); final isDisclaimerAccepted = await showDisclaimer();
@@ -551,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) {
@@ -577,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;
} }
@@ -598,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);
@@ -625,37 +745,66 @@ 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,
),
); );
} }
@@ -669,16 +818,22 @@ class AppController {
} }
updateMode() { updateMode() {
final index = Mode.values.indexWhere((item) => item == clashConfig.mode); _ref.read(patchClashConfigProvider.notifier).updateState(
(state) {
final index = Mode.values.indexWhere((item) => item == state.mode);
if (index == -1) { if (index == -1) {
return; return null;
} }
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1; final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
clashConfig.mode = Mode.values[nextIndex]; 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 {
@@ -695,12 +850,10 @@ class AppController {
Future<List<int>> backupData() async { Future<List<int>> backupData() async {
final homeDirPath = await appPath.homeDirPath; final homeDirPath = await appPath.homeDirPath;
final profilesPath = await appPath.profilesPath; 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) ?? [];
@@ -709,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,
); );
} }
@@ -732,32 +881,64 @@ class AppController {
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,20 +4,21 @@ 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; late ScrollController _controller;
@@ -28,10 +29,11 @@ class _AccessFragmentState extends State<AccessFragment> {
_updateInitList(); _updateInitList();
_controller = ScrollController(); _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() ?? [];
}); });
} }
}); });
@@ -44,9 +46,8 @@ class _AccessFragmentState extends State<AccessFragment> {
} }
_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() {
@@ -59,9 +60,13 @@ class _AccessFragmentState extends State<AccessFragment> {
acceptList: acceptList, acceptList: acceptList,
rejectList: rejectList, rejectList: rejectList,
), ),
).then((_) => setState(() { ).then(
(_) => setState(
() {
_updateInitList(); _updateInitList();
})); },
),
);
}, },
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
); );
@@ -77,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)
@@ -106,26 +112,91 @@ 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;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
);
final currentList = accessControl.currentList;
final packageNameList = packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe = accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return Column( return Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
@@ -134,10 +205,13 @@ class _AccessFragmentState extends State<AccessFragment> {
child: ListItem.switchItem( child: ListItem.switchItem(
title: Text(appLocalizations.appAccessControl), title: Text(appLocalizations.appAccessControl),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: isAccessControl, value: accessControl.enable,
onChanged: (isAccessControl) { onChanged: (enable) {
final config = context.read<Config>(); ref.read(vpnSettingProvider.notifier).updateState(
config.isAccessControl = isAccessControl; (state) => state.copyWith.accessControl(
enable: enable,
),
);
}, },
), ),
), ),
@@ -149,43 +223,12 @@ class _AccessFragmentState extends State<AccessFragment> {
), ),
), ),
Flexible( Flexible(
child: child!, child: DisabledMask(
), status: !accessControl.enable,
],
);
},
child: Selector<AppState, List<Package>>(
selector: (_, appState) => appState.packages,
builder: (_, packages, ___) {
return Selector2<AppState, Config, PackageListSelectorState>(
selector: (_, appState, config) => PackageListSelectorState(
accessControl: config.accessControl,
isAccessControl: config.isAccessControl,
packages: appState.packages,
),
builder: (context, state, __) {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final accessControlMode = accessControl.mode;
final packages = state.getList(
accessControlMode == AccessControlMode.acceptSelected
? acceptList
: rejectList,
);
final currentList = accessControl.currentList;
final packageNameList =
packages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe =
accessControlMode == AccessControlMode.acceptSelected
? appLocalizations.accessControlAllowDesc
: appLocalizations.accessControlNotAllowDesc;
return DisabledMask(
status: !isAccessControl,
child: Column( child: Column(
children: [ children: [
ActivateBox( ActivateBox(
active: isAccessControl, active: accessControl.enable,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 4, top: 4,
@@ -256,8 +299,8 @@ class _AccessFragmentState extends State<AccessFragment> {
), ),
Flexible( Flexible(
child: _buildSelectedAllButton( child: _buildSelectedAllButton(
isSelectedAll: valueList.length == isSelectedAll:
packageNameList.length, valueList.length == packageNameList.length,
allValueList: packageNameList, allValueList: packageNameList,
), ),
), ),
@@ -287,29 +330,10 @@ class _AccessFragmentState extends State<AccessFragment> {
return PackageListItem( return PackageListItem(
key: Key(package.packageName), key: Key(package.packageName),
package: package, package: package,
value: value: valueList.contains(package.packageName),
valueList.contains(package.packageName), isActive: accessControl.enable,
isActive: isAccessControl,
onChanged: (value) { onChanged: (value) {
if (value == true) { _handleSelected(valueList, package, value);
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,
);
}
}, },
); );
}, },
@@ -318,11 +342,9 @@ class _AccessFragmentState extends State<AccessFragment> {
), ),
], ],
), ),
);
},
);
},
), ),
),
],
); );
} }
} }
@@ -343,7 +365,8 @@ class PackageListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActivateBox( return FadeScaleEnterBox(
child: ActivateBox(
active: isActive, active: isActive,
child: ListItem.checkbox( child: ListItem.checkbox(
leading: SizedBox( leading: SizedBox(
@@ -384,6 +407,7 @@ class PackageListItem extends StatelessWidget {
onChanged: onChanged, onChanged: onChanged,
), ),
), ),
),
); );
} }
} }
@@ -426,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(
@@ -449,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);
@@ -465,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,
);
}
}, },
); );
}, },
@@ -500,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,
@@ -552,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: [
@@ -566,9 +601,10 @@ 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,
),
); );
}, },
) )
@@ -588,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: [
@@ -602,9 +640,10 @@ 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,
),
); );
}, },
), ),
@@ -624,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: [
@@ -635,9 +677,10 @@ 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,
),
); );
}, },
) )
@@ -650,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(
(state) => state.copyWith(
accessControl: AccessControl.fromJson(
json.decode(text), json.decode(text),
),
),
); );
}); },
if (!context.mounted) return; );
if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
@@ -725,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,11 +122,20 @@ class BackupAndRecovery extends StatelessWidget {
_recoveryOnLocal(context, recoveryOption); _recoveryOnLocal(context, recoveryOption);
} }
_handleChange(String? value, WidgetRef ref) {
if (value == null) {
return;
}
ref.read(appDAVSettingProvider.notifier).updateState(
(state) => state?.copyWith(
fileName: value,
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Selector<Config, DAV?>( final dav = ref.watch(appDAVSettingProvider);
selector: (_, config) => config.dav,
builder: (_, dav, __) {
final client = dav != null ? DAVClient(dav) : null; final client = dav != null ? DAVClient(dav) : null;
return ListView( return ListView(
children: [ children: [
@@ -210,14 +220,8 @@ class BackupAndRecovery extends StatelessWidget {
title: appLocalizations.file, title: appLocalizations.file,
value: dav.fileName, value: dav.fileName,
resetValue: defaultDavFileName, resetValue: defaultDavFileName,
onChanged: (String? value) { onChanged: (value) {
if (value == null) { _handleChange(value, ref);
return;
}
globalState.appController.config.dav =
globalState.appController.config.dav?.copyWith(
fileName: value,
);
}, },
), ),
), ),
@@ -253,8 +257,6 @@ class BackupAndRecovery extends StatelessWidget {
), ),
], ],
); );
},
);
} }
} }
@@ -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,151 +1,128 @@
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,
builder: (_, override, __) {
return ListItem.switchItem( return ListItem.switchItem(
title: Text(appLocalizations.overrideDns), title: Text(appLocalizations.overrideDns),
subtitle: Text(appLocalizations.overrideDnsDesc), subtitle: Text(appLocalizations.overrideDnsDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: override, value: override,
onChanged: (bool value) async { onChanged: (bool value) async {
final config = globalState.appController.config; ref.read(overrideDnsProvider.notifier).value = value;
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 {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(enable: value));
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 {
final clashConfig = globalState.appController.clashConfig; ref
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(preferH3: value));
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),
@@ -156,27 +133,25 @@ class DnsModeItem extends StatelessWidget {
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),
@@ -184,55 +159,41 @@ class FakeIpRangeItem extends StatelessWidget {
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(
fakeIpRange: value,
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.fakeipRange,
message: TextSpan(
text: e.toString(),
),
);
}
} }
ref
.read(patchClashConfigProvider.notifier)
.updateState((state) => state.copyWith.dns(fakeIpRange: value));
}, },
), ),
); );
},
);
} }
} }
class FakeIpFilterItem extends StatelessWidget { class FakeIpFilterItem extends ConsumerWidget {
const FakeIpFilterItem({super.key}); const FakeIpFilterItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final fakeIpFilter = ref.watch(
patchClashConfigProvider.select((state) => state.dns.fakeIpFilter),
);
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.fakeipFilter), title: Text(appLocalizations.fakeipFilter),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.fakeipFilter, title: appLocalizations.fakeipFilter,
widget: Selector<ClashConfig, List<String>>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, fakeIpFilter, __) {
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),
); ));
},
);
}, },
), ),
extendPageWidth: 360, extendPageWidth: 360,
@@ -241,33 +202,30 @@ class FakeIpFilterItem extends StatelessWidget {
} }
} }
class DefaultNameserverItem extends StatelessWidget { class DefaultNameserverItem extends ConsumerWidget {
const DefaultNameserverItem({super.key}); const DefaultNameserverItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final defaultNameserver = ref.watch(
patchClashConfigProvider.select((state) => state.dns.defaultNameserver),
);
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.defaultNameserver), title: Text(appLocalizations.defaultNameserver),
subtitle: Text(appLocalizations.defaultNameserverDesc), subtitle: Text(appLocalizations.defaultNameserverDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.defaultNameserver, title: appLocalizations.defaultNameserver,
widget: Selector<ClashConfig, List<String>>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, defaultNameserver, __) {
return ListPage(
title: appLocalizations.defaultNameserver, title: appLocalizations.defaultNameserver,
items: defaultNameserver, items: defaultNameserver,
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(
defaultNameserver: List.from(items), defaultNameserver: List.from(items),
); ));
},
);
}, },
), ),
extendPageWidth: 360, extendPageWidth: 360,
@@ -276,33 +234,30 @@ class DefaultNameserverItem extends StatelessWidget {
} }
} }
class NameserverItem extends StatelessWidget { class NameserverItem extends ConsumerWidget {
const NameserverItem({super.key}); const NameserverItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final nameserver = ref.watch(
patchClashConfigProvider.select((state) => state.dns.nameserver),
);
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.nameserver), title: Text(appLocalizations.nameserver),
subtitle: Text(appLocalizations.nameserverDesc), subtitle: Text(appLocalizations.nameserverDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
title: appLocalizations.nameserver, title: appLocalizations.nameserver,
isBlur: false, isBlur: false,
widget: Selector<ClashConfig, List<String>>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.dns.nameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, nameserver, __) {
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
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(
nameserver: List.from(items), nameserver: List.from(items),
); ));
},
);
}, },
), ),
extendPageWidth: 360, extendPageWidth: 360,
@@ -311,87 +266,77 @@ class NameserverItem extends StatelessWidget {
} }
} }
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,
); ));
}, },
), ),
); );
},
);
} }
} }
class NameserverPolicyItem extends StatelessWidget { class NameserverPolicyItem extends ConsumerWidget {
const NameserverPolicyItem({super.key}); const NameserverPolicyItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final nameserverPolicy = ref.watch(
patchClashConfigProvider.select((state) => state.dns.nameserverPolicy),
);
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.nameserverPolicy), title: Text(appLocalizations.nameserverPolicy),
subtitle: Text(appLocalizations.nameserverPolicyDesc), subtitle: Text(appLocalizations.nameserverPolicyDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.nameserverPolicy, title: appLocalizations.nameserverPolicy,
widget: Selector<ClashConfig, Map<String, String>>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
shouldRebuild: (prev, next) =>
!const MapEquality<String, String>().equals(prev, next),
builder: (_, nameserverPolicy, __) {
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
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(
nameserverPolicy: Map.fromEntries(items), nameserverPolicy: Map.fromEntries(items),
); ));
},
);
}, },
), ),
extendPageWidth: 360, extendPageWidth: 360,
@@ -400,33 +345,31 @@ class NameserverPolicyItem extends StatelessWidget {
} }
} }
class ProxyServerNameserverItem extends StatelessWidget { class ProxyServerNameserverItem extends ConsumerWidget {
const ProxyServerNameserverItem({super.key}); const ProxyServerNameserverItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final proxyServerNameserver = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.proxyServerNameserver),
);
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.proxyNameserver), title: Text(appLocalizations.proxyNameserver),
subtitle: Text(appLocalizations.proxyNameserverDesc), subtitle: Text(appLocalizations.proxyNameserverDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.proxyNameserver, title: appLocalizations.proxyNameserver,
widget: Selector<ClashConfig, List<String>>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, proxyServerNameserver, __) {
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
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(
proxyServerNameserver: List.from(items), proxyServerNameserver: List.from(items),
); ));
},
);
}, },
), ),
extendPageWidth: 360, extendPageWidth: 360,
@@ -435,33 +378,30 @@ class ProxyServerNameserverItem extends StatelessWidget {
} }
} }
class FallbackItem extends StatelessWidget { class FallbackItem extends ConsumerWidget {
const FallbackItem({super.key}); const FallbackItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final fallback = ref.watch(
patchClashConfigProvider.select((state) => state.dns.fallback),
);
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.fallback), title: Text(appLocalizations.fallback),
subtitle: Text(appLocalizations.fallbackDesc), subtitle: Text(appLocalizations.fallbackDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.fallback, title: appLocalizations.fallback,
widget: Selector<ClashConfig, List<String>>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.dns.fallback,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, fallback, __) {
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
final dns = clashConfig.dns; .read(patchClashConfigProvider.notifier)
clashConfig.dns = dns.copyWith( .updateState((state) => state.copyWith.dns(
fallback: List.from(items), fallback: List.from(items),
); ));
},
);
}, },
), ),
extendPageWidth: 360, extendPageWidth: 360,
@@ -470,40 +410,40 @@ class FallbackItem extends StatelessWidget {
} }
} }
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( return ListItem.switchItem(
title: const Text("Geoip"), title: const Text("Geoip"),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: geoip, value: geoip,
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.fallbackFilter(
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value), 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( return ListItem.input(
title: Text(appLocalizations.geoipCode), title: Text(appLocalizations.geoipCode),
subtitle: Text(geoipCode), subtitle: Text(geoipCode),
@@ -511,60 +451,44 @@ class GeoipCodeItem extends StatelessWidget {
title: appLocalizations.geoipCode, title: appLocalizations.geoipCode,
value: geoipCode, value: geoipCode,
onChanged: (String? value) { onChanged: (String? value) {
if (value != null) { if (value == null) {
try { return;
final clashConfig = globalState.appController.clashConfig; }
final dns = clashConfig.dns; ref
clashConfig.dns = dns.copyWith( .read(patchClashConfigProvider.notifier)
fallbackFilter: dns.fallbackFilter.copyWith( .updateState((state) => state.copyWith.dns.fallbackFilter(
geoipCode: value, geoipCode: value,
), ));
);
} catch (e) {
globalState.showMessage(
title: appLocalizations.geoipCode,
message: TextSpan(
text: e.toString(),
),
);
}
}
}, },
), ),
); );
},
);
} }
} }
class GeositeItem extends StatelessWidget { class GeositeItem extends ConsumerWidget {
const GeositeItem({super.key}); const GeositeItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final geosite = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.geosite),
);
return ListItem.open( return ListItem.open(
title: const Text("Geosite"), title: const Text("Geosite"),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: "Geosite", title: "Geosite",
widget: Selector<ClashConfig, List<String>>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, geosite, __) {
return ListPage(
title: "Geosite", title: "Geosite",
items: geosite, items: geosite,
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.fallbackFilter(
fallbackFilter: dns.fallbackFilter.copyWith(
geosite: List.from(items), geosite: List.from(items),
), ));
);
},
);
}, },
), ),
extendPageWidth: 360, extendPageWidth: 360,
@@ -573,34 +497,30 @@ class GeositeItem extends StatelessWidget {
} }
} }
class IpcidrItem extends StatelessWidget { class IpcidrItem extends ConsumerWidget {
const IpcidrItem({super.key}); const IpcidrItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final ipcidr = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.ipcidr),
);
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.ipcidr), title: Text(appLocalizations.ipcidr),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.ipcidr, title: appLocalizations.ipcidr,
widget: Selector<ClashConfig, List<String>>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, ipcidr, __) {
return ListPage(
title: appLocalizations.ipcidr, title: appLocalizations.ipcidr,
items: ipcidr, items: ipcidr,
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.fallbackFilter(
fallbackFilter: dns.fallbackFilter.copyWith(
ipcidr: List.from(items), ipcidr: List.from(items),
), ));
);
},
);
}, },
), ),
extendPageWidth: 360, extendPageWidth: 360,
@@ -609,34 +529,30 @@ class IpcidrItem extends StatelessWidget {
} }
} }
class DomainItem extends StatelessWidget { class DomainItem extends ConsumerWidget {
const DomainItem({super.key}); const DomainItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final domain = ref.watch(
patchClashConfigProvider
.select((state) => state.dns.fallbackFilter.domain),
);
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.domain), title: Text(appLocalizations.domain),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: appLocalizations.domain, title: appLocalizations.domain,
widget: Selector<ClashConfig, List<String>>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (_, domain, __) {
return ListPage(
title: appLocalizations.domain, title: appLocalizations.domain,
items: domain, items: domain,
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.fallbackFilter(
fallbackFilter: dns.fallbackFilter.copyWith(
domain: List.from(items), domain: List.from(items),
), ));
);
},
);
}, },
), ),
extendPageWidth: 360, extendPageWidth: 360,
@@ -700,10 +616,10 @@ 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) {
context.commonScaffoldState?.actions = [ context.commonScaffoldState?.actions = [
IconButton( IconButton(
@@ -717,7 +633,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(
@@ -729,8 +650,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,24 +1,23 @@
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(value.name), subtitle: Text(logLevel.name),
delegate: OptionsDelegate<LogLevel>( delegate: OptionsDelegate<LogLevel>(
title: appLocalizations.logLevel, title: appLocalizations.logLevel,
options: LogLevel.values, options: LogLevel.values,
@@ -26,30 +25,30 @@ class LogLevelItem extends StatelessWidget {
if (value == null) { if (value == null) {
return; return;
} }
final appController = globalState.appController; ref.read(patchClashConfigProvider.notifier).updateState(
appController.clashConfig.logLevel = value; (state) => state.copyWith(
}, 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(value ?? appLocalizations.defaultText), subtitle: Text(globalUa ?? appLocalizations.defaultText),
delegate: OptionsDelegate<String?>( delegate: OptionsDelegate<String?>(
title: "UA", title: "UA",
options: [ options: [
@@ -57,151 +56,146 @@ class UaItem extends StatelessWidget {
"clash-verge/v1.6.6", "clash-verge/v1.6.6",
"ClashforWindows/0.19.23", "ClashforWindows/0.19.23",
], ],
value: value, value: globalUa,
onChanged: (ua) { onChanged: (value) {
final appController = globalState.appController; ref.read(patchClashConfigProvider.notifier).updateState(
appController.clashConfig.globalRealUa = ua; (state) => state.copyWith(
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("$value ${appLocalizations.seconds}"), subtitle: Text("$keepAliveInterval ${appLocalizations.seconds}"),
delegate: InputDelegate( delegate: InputDelegate(
title: appLocalizations.keepAliveIntervalDesc, title: appLocalizations.keepAliveIntervalDesc,
suffixText: appLocalizations.seconds, suffixText: appLocalizations.seconds,
resetValue: "$defaultKeepAliveInterval", resetValue: "$defaultKeepAliveInterval",
value: "$value", value: "$keepAliveInterval",
onChanged: (String? value) { onChanged: (String? value) {
if (value != null) { if (value == null) {
try { return;
}
globalState.safeRun(
() {
final intValue = int.parse(value); final intValue = int.parse(value);
if (intValue <= 0) { if (intValue <= 0) {
throw "Invalid keepAliveInterval"; throw "Invalid keepAliveInterval";
} }
globalState.appController.clashConfig.keepAliveInterval = ref.read(patchClashConfigProvider.notifier).updateState(
intValue; (state) => state.copyWith(
} catch (e) { keepAliveInterval: intValue,
globalState.showMessage( ),
);
},
silence: false,
title: appLocalizations.keepAliveIntervalDesc, title: appLocalizations.keepAliveIntervalDesc,
message: TextSpan(
text: e.toString(),
),
);
}
}
},
),
); );
}, },
),
); );
} }
} }
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(value), subtitle: Text(testUrl),
delegate: InputDelegate( delegate: InputDelegate(
resetValue: defaultTestUrl, resetValue: defaultTestUrl,
title: appLocalizations.testUrl, title: appLocalizations.testUrl,
value: value, value: testUrl,
onChanged: (String? value) { onChanged: (String? value) {
if (value != null) { if (value == null) {
try { return;
}
globalState.safeRun(
() {
if (!value.isUrl) { if (!value.isUrl) {
throw "Invalid url"; throw "Invalid url";
} }
final config = globalState.appController.config; ref.read(appSettingProvider.notifier).updateState(
config.appSetting = config.appSetting.copyWith( (state) => state.copyWith(
testUrl: value, testUrl: value,
),
); );
} catch (e) { },
globalState.showMessage( silence: false,
title: appLocalizations.testUrl, title: appLocalizations.testUrl,
message: TextSpan(
text: e.toString(),
),
); );
} }),
}
},
),
);
},
); );
} }
} }
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("$value"), subtitle: Text("$mixedPort"),
delegate: InputDelegate( delegate: InputDelegate(
title: appLocalizations.proxyPort, title: appLocalizations.proxyPort,
value: "$value", value: "$mixedPort",
onChanged: (String? value) { onChanged: (String? value) {
if (value != null) { if (value == null) {
try { return;
}
globalState.safeRun(
() {
final mixedPort = int.parse(value); final mixedPort = int.parse(value);
if (mixedPort < 1024 || mixedPort > 49151) { if (mixedPort < 1024 || mixedPort > 49151) {
throw "Invalid port"; throw "Invalid port";
} }
globalState.appController.clashConfig.mixedPort = mixedPort; ref.read(patchClashConfigProvider.notifier).updateState(
} catch (e) { (state) => state.copyWith(
globalState.showMessage( mixedPort: mixedPort,
title: appLocalizations.proxyPort,
message: TextSpan(
text: e.toString(),
), ),
); );
} },
} silence: false,
title: appLocalizations.proxyPort,
);
}, },
resetValue: "$defaultMixedPort", resetValue: "$defaultMixedPort",
), ),
); );
},
);
} }
} }
class HostsItem extends StatelessWidget { class HostsItem extends ConsumerWidget {
const HostsItem({super.key}); const HostsItem({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final hosts =
ref.watch(patchClashConfigProvider.select((state) => state.hosts));
return ListItem.open( return ListItem.open(
leading: const Icon(Icons.view_list_outlined), leading: const Icon(Icons.view_list_outlined),
title: const Text("Hosts"), title: const Text("Hosts"),
@@ -209,21 +203,16 @@ class HostsItem extends StatelessWidget {
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
title: "Hosts", title: "Hosts",
widget: Selector<ClashConfig, HostsMap>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.hosts,
shouldRebuild: (prev, next) =>
!const MapEquality<String, String>().equals(prev, next),
builder: (_, hosts, ___) {
final entries = hosts.entries;
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,14 +222,13 @@ 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"),
@@ -248,24 +236,24 @@ class Ipv6Item extends StatelessWidget {
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: ipv6, value: ipv6,
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; ref.read(patchClashConfigProvider.notifier).updateState(
appController.clashConfig.ipv6 = value; (state) => state.copyWith(
}, 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),
@@ -273,24 +261,25 @@ class AllowLanItem extends StatelessWidget {
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: allowLan, value: allowLan,
onChanged: (bool value) async { onChanged: (bool value) async {
final clashConfig = context.read<ClashConfig>(); ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.allowLan = value; (state) => state.copyWith(
}, 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),
@@ -298,25 +287,25 @@ class UnifiedDelayItem extends StatelessWidget {
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),
@@ -324,77 +313,77 @@ class FindProcessItem extends StatelessWidget {
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: findProcess, value: findProcess,
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; ref.read(patchClashConfigProvider.notifier).updateState(
appController.clashConfig.findProcessMode = (state) => state.copyWith(
value ? FindProcessMode.always : FindProcessMode.off; findProcessMode:
}, 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: (bool value) async { onChanged: (value) async {
final appController = globalState.appController; ref.read(patchClashConfigProvider.notifier).updateState(
appController.clashConfig.tcpConcurrent = value; (state) => state.copyWith(
}, 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,
builder: (_, memconservative, __) {
return ListItem.switchItem( return ListItem.switchItem(
leading: const Icon(Icons.memory), leading: const Icon(Icons.memory),
title: Text(appLocalizations.geodataLoader), title: Text(appLocalizations.geodataLoader),
subtitle: Text(appLocalizations.geodataLoaderDesc), subtitle: Text(appLocalizations.geodataLoaderDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: memconservative, value: isMemconservative,
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; ref.read(patchClashConfigProvider.notifier).updateState(
appController.clashConfig.geodataLoader = (state) => state.copyWith(
value ? geodataLoaderMemconservative : geodataLoaderStandard; geodataLoader: value
}, ? GeodataLoader.memconservative
: 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),
@@ -402,21 +391,23 @@ class ExternalControllerItem extends StatelessWidget {
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: hasExternalController, value: hasExternalController,
onChanged: (bool value) async { onChanged: (bool value) async {
final appController = globalState.appController; ref.read(patchClashConfigProvider.notifier).updateState(
appController.clashConfig.externalController = (state) => state.copyWith(
value ? defaultExternalController : ''; externalController: value
}, ? 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,180 +3,166 @@ 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 {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState(
config.vpnProps = config.vpnProps.copyWith( (state) => state.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 {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState(
final vpnProps = config.vpnProps; (state) => state.copyWith(
config.vpnProps = vpnProps.copyWith(
allowBypass: value, 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 {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState(
final vpnProps = config.vpnProps; (state) => state.copyWith(
config.vpnProps = vpnProps.copyWith(
systemProxy: value, 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,
builder: (_, ipv6, __) {
return ListItem.switchItem( return ListItem.switchItem(
title: const Text("IPv6"), title: const Text("IPv6"),
subtitle: Text(appLocalizations.ipv6InboundDesc), subtitle: Text(appLocalizations.ipv6InboundDesc),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: ipv6, value: ipv6,
onChanged: (bool value) async { onChanged: (bool value) async {
final config = globalState.appController.config; ref.read(vpnSettingProvider.notifier).updateState(
final vpnProps = config.vpnProps; (state) => state.copyWith(
config.vpnProps = vpnProps.copyWith(
ipv6: value, 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),
@@ -188,23 +174,22 @@ class TunStackItem extends StatelessWidget {
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,
), ),
); );
},
);
} }
} }
class BypassDomainItem extends StatelessWidget { class BypassDomainItem extends ConsumerWidget {
const BypassDomainItem({super.key}); const BypassDomainItem({super.key});
_initActions(BuildContext context) { _initActions(BuildContext context, WidgetRef ref) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context.commonScaffoldState?.actions = [ context.commonScaffoldState?.actions = [
IconButton( IconButton(
@@ -218,9 +203,10 @@ 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,
@@ -233,7 +219,10 @@ class BypassDomainItem extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final bypassDomain =
ref.watch(networkSettingProvider.select((state) => state.bypassDomain));
return ListItem.open( return ListItem.open(
title: Text(appLocalizations.bypassDomain), title: Text(appLocalizations.bypassDomain),
subtitle: Text(appLocalizations.bypassDomainDesc), subtitle: Text(appLocalizations.bypassDomainDesc),
@@ -241,41 +230,37 @@ class BypassDomainItem extends StatelessWidget {
isBlur: false, isBlur: false,
isScaffold: true, isScaffold: true,
title: appLocalizations.bypassDomain, title: appLocalizations.bypassDomain,
widget: Selector<Config, List<String>>( widget: Builder(builder: (context) {
selector: (_, config) => config.networkProps.bypassDomain, _initActions(context, ref);
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
builder: (context, bypassDomain, __) {
_initActions(context);
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),
);
},
);
},
), ),
);
},
);
}),
extendPageWidth: 360, extendPageWidth: 360,
), ),
); );
} }
} }
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_${value.name}")), subtitle: Text(Intl.message("routeMode_${routeMode.name}")),
delegate: OptionsDelegate<RouteMode>( delegate: OptionsDelegate<RouteMode>(
title: appLocalizations.routeMode, title: appLocalizations.routeMode,
options: RouteMode.values, options: RouteMode.values,
@@ -283,60 +268,54 @@ class RouteModeItem extends StatelessWidget {
if (value == null) { if (value == null) {
return; return;
} }
final appController = globalState.appController; ref.read(networkSettingProvider.notifier).updateState(
appController.clashConfig.routeMode = value; (state) => state.copyWith(
routeMode: value,
),
);
}, },
textBuilder: (routeMode) => Intl.message( textBuilder: (routeMode) => Intl.message(
"routeMode_${routeMode.name}", "routeMode_${routeMode.name}",
), ),
value: value, 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 child!;
}
return Container(); return Container();
}, }
child: ListItem.open( final routeAddress = ref.watch(
patchClashConfigProvider.select((state) => state.tun.routeAddress));
return ListItem.open(
title: Text(appLocalizations.routeAddress), title: Text(appLocalizations.routeAddress),
subtitle: Text(appLocalizations.routeAddressDesc), subtitle: Text(appLocalizations.routeAddressDesc),
delegate: OpenDelegate( delegate: OpenDelegate(
isBlur: false, isBlur: false,
isScaffold: true, isScaffold: true,
title: appLocalizations.routeAddress, title: appLocalizations.routeAddress,
widget: Selector<ClashConfig, List<String>>( widget: ListPage(
selector: (_, clashConfig) => clashConfig.includeRouteAddress,
shouldRebuild: (prev, next) =>
!stringListEquality.equals(prev, next),
builder: (context, routeAddress, __) {
return ListPage(
title: appLocalizations.routeAddress, title: appLocalizations.routeAddress,
items: routeAddress, items: routeAddress,
titleBuilder: (item) => Text(item), titleBuilder: (item) => Text(item),
onChange: (items) { onChange: (items) {
final clashConfig = globalState.appController.clashConfig; ref.read(patchClashConfigProvider.notifier).updateState(
clashConfig.includeRouteAddress = (state) => state.copyWith.tun(
Set<String>.from(items).toList(); routeAddress: List.from(items),
}, ),
); );
}, },
), ),
extendPageWidth: 360, extendPageWidth: 360,
), ),
),
); );
} }
} }
@@ -347,7 +326,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(),
], ],
@@ -371,10 +351,10 @@ 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) {
context.commonScaffoldState?.actions = [ context.commonScaffoldState?.actions = [
IconButton( IconButton(
@@ -388,9 +368,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(
@@ -402,8 +387,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

@@ -4,21 +4,23 @@ 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/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/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 'item.dart'; import 'item.dart';
class ConnectionsFragment extends StatefulWidget { class ConnectionsFragment extends ConsumerStatefulWidget {
const ConnectionsFragment({super.key}); const ConnectionsFragment({super.key});
@override @override
State<ConnectionsFragment> createState() => _ConnectionsFragmentState(); ConsumerState<ConnectionsFragment> createState() =>
_ConnectionsFragmentState();
} }
class _ConnectionsFragmentState extends State<ConnectionsFragment> class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
with ViewMixin { with PageMixin {
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>( final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
const ConnectionsState(), const ConnectionsState(),
); );
@@ -71,15 +73,20 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_updateConnections(); ref.listenManual(
isCurrentPageProvider(
PageLabel.connections,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
} }
_initActions() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
initViewState();
}, },
fireImmediately: true,
); );
_updateConnections();
} }
_handleBlockConnection(String id) async { _handleBlockConnection(String id) async {
@@ -100,18 +107,7 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<AppState, bool?>( return ValueListenableBuilder<ConnectionsState>(
selector: (_, appState) =>
appState.currentLabel == 'connections' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: ValueListenableBuilder<ConnectionsState>(
valueListenable: _connectionsStateNotifier, valueListenable: _connectionsStateNotifier,
builder: (_, state, __) { builder: (_, state, __) {
final connections = state.list; final connections = state.list;
@@ -149,7 +145,6 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
), ),
); );
}, },
),
); );
} }
} }

View File

@@ -4,9 +4,10 @@ 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/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/config.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 FindProcessBuilder extends StatelessWidget { class FindProcessBuilder extends StatelessWidget {
final Widget Function(bool value) builder; final Widget Function(bool value) builder;
@@ -18,11 +19,15 @@ class FindProcessBuilder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<ClashConfig, bool>( return Consumer(
selector: (_, clashConfig) => builder: (_, ref, __) {
clashConfig.findProcessMode == FindProcessMode.always && final value = ref.watch(
patchClashConfigProvider.select(
(state) =>
state.findProcessMode == FindProcessMode.always &&
Platform.isAndroid, Platform.isAndroid,
builder: (_, value, __) { ),
);
return builder(value); return builder(value);
}, },
); );

View File

@@ -1,28 +1,25 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
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';
import 'item.dart'; import 'item.dart';
double _preOffset = 0; double _preOffset = 0;
class RequestsFragment extends StatefulWidget { class RequestsFragment extends ConsumerStatefulWidget {
const RequestsFragment({super.key}); const RequestsFragment({super.key});
@override @override
State<RequestsFragment> createState() => _RequestsFragmentState(); ConsumerState<RequestsFragment> createState() => _RequestsFragmentState();
} }
class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin { class _RequestsFragmentState extends ConsumerState<RequestsFragment>
with PageMixin {
final _requestsStateNotifier = final _requestsStateNotifier =
ValueNotifier<ConnectionsState>(const ConnectionsState()); ValueNotifier<ConnectionsState>(const ConnectionsState());
List<Connection> _requests = []; List<Connection> _requests = [];
@@ -51,18 +48,32 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final appController = globalState.appController;
final appState = appController.appState;
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith( _requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: appState.requests, connections: globalState.appState.requests.list,
); );
}
_initActions() { ref.listenManual(
WidgetsBinding.instance.addPostFrameCallback( isCurrentPageProvider(
(_) { PageLabel.requests,
initViewState(); 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,
); );
} }
@@ -111,22 +122,6 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
super.dispose(); super.dispose();
} }
Widget _wrapPage(Widget child) {
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: child,
);
}
updateRequestsThrottler() { updateRequestsThrottler() {
throttler.call("request", () { throttler.call("request", () {
final isEquality = connectionListEquality.equals( final isEquality = connectionListEquality.equals(
@@ -144,33 +139,13 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
}, duration: commonDuration); }, duration: commonDuration);
} }
Widget _wrapRequestsUpdate(Widget child) {
return Selector<AppState, List<Connection>>(
selector: (_, appState) => appState.requests,
shouldRebuild: (prev, next) {
final isEquality = connectionListEquality.equals(prev, next);
if (!isEquality) {
_requests = next;
updateRequestsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (_, constraints) { builder: (_, constraints) {
return FindProcessBuilder(builder: (value) { return FindProcessBuilder(builder: (value) {
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0)); _handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return _wrapPage( return ValueListenableBuilder<ConnectionsState>(
_wrapRequestsUpdate(
ValueListenableBuilder<ConnectionsState>(
valueListenable: _requestsStateNotifier, valueListenable: _requestsStateNotifier,
builder: (_, state, __) { builder: (_, state, __) {
final connections = state.list; final connections = state.list;
@@ -229,8 +204,6 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
), ),
); );
}, },
),
),
); );
}); });
}, },

View File

@@ -2,31 +2,42 @@ 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/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),
(prev, next) {
if (prev != next && next == true) {
initPageState();
} }
WidgetsBinding.instance.addPostFrameCallback((_) { },
final commonScaffoldState = context.commonScaffoldState; fireImmediately: true,
commonScaffoldState?.floatingActionButton = const StartButton(); );
commonScaffoldState?.actions = [ 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,37 +78,35 @@ 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);
return child!;
},
child: Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(16).copyWith( padding: const EdgeInsets.all(16).copyWith(
bottom: 88, bottom: 88,
), ),
child: Selector2<AppState, Config, DashboardState>( child: SuperGrid(
selector: (_, appState, config) => DashboardState(
dashboardWidgets: config.appSetting.dashboardWidgets,
viewWidth: appState.viewWidth,
),
builder: (_, state, ___) {
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8);
return SuperGrid(
key: key, key: key,
crossAxisCount: columns, crossAxisCount: columns,
crossAxisSpacing: 16, crossAxisSpacing: 16,
mainAxisSpacing: 16, mainAxisSpacing: 16,
children: [ children: [
...state.dashboardWidgets ...dashboardState.dashboardWidgets
.where( .where(
(item) => item.platforms.contains( (item) => item.platforms.contains(
SupportPlatform.currentPlatform, SupportPlatform.currentPlatform,
@@ -108,15 +117,7 @@ class _DashboardFragmentState extends State<DashboardFragment> {
), ),
], ],
onSave: (girdItems) { onSave: (girdItems) {
final dashboardWidgets = girdItems _handleSave(girdItems, ref);
.map(
(item) => DashboardWidget.getDashboardWidget(item),
)
.toList();
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
dashboardWidgets: dashboardWidgets,
);
}, },
addedItemsBuilder: (girdItems) { addedItemsBuilder: (girdItems) {
return DashboardWidget.values return DashboardWidget.values
@@ -130,9 +131,6 @@ class _DashboardFragmentState extends State<DashboardFragment> {
.map((item) => item.widget) .map((item) => item.widget)
.toList(); .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,8 +130,7 @@ 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;
@@ -198,6 +186,27 @@ class _NetworkDetectionState extends State<NetworkDetection> {
), ),
), ),
), ),
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,
),
),
)
], ],
), ),
), ),
@@ -232,7 +241,9 @@ class _NetworkDetectionState extends State<NetworkDetection> {
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
child: const AspectRatio( child: const AspectRatio(
aspectRatio: 1, aspectRatio: 1,
child: CircularProgressIndicator(), child: CircularProgressIndicator(
strokeWidth: 2,
),
), ),
), ),
), ),
@@ -244,7 +255,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
); );
}, },
), ),
),
); );
} }
} }

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,18 +1,16 @@
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: () {
@@ -56,16 +54,17 @@ class TUNButton extends StatelessWidget {
), ),
), ),
), ),
Selector<ClashConfig, bool>( Consumer(
selector: (_, clashConfig) => clashConfig.tun.enable, builder: (_, ref, __) {
builder: (_, enable, __) { final enable = ref.watch(patchClashConfigProvider
.select((state) => state.tun.enable));
return Switch( return Switch(
value: enable, value: enable,
onChanged: (value) { onChanged: (value) {
final clashConfig = ref.read(patchClashConfigProvider.notifier).updateState(
globalState.appController.clashConfig; (state) => state.copyWith.tun(
clashConfig.tun = clashConfig.tun.copyWith(
enable: value, enable: value,
),
); );
}, },
); );
@@ -75,7 +74,6 @@ class TUNButton extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
} }
@@ -87,8 +85,7 @@ 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,
@@ -132,16 +129,19 @@ class SystemProxyButton extends StatelessWidget {
), ),
), ),
), ),
Selector<Config, bool>( Consumer(
selector: (_, config) => config.networkProps.systemProxy, builder: (_, ref, __) {
builder: (_, systemProxy, __) { final systemProxy = ref.watch(networkSettingProvider
.select((state) => state.systemProxy));
return Switch( return Switch(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: systemProxy, value: systemProxy,
onChanged: (value) { onChanged: (value) {
final config = globalState.appController.config; ref.read(networkSettingProvider.notifier).updateState(
config.networkProps = (state) => state.copyWith(
config.networkProps.copyWith(systemProxy: value); systemProxy: value,
),
);
}, },
); );
}, },
@@ -150,7 +150,6 @@ class SystemProxyButton extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
} }

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,8 +79,7 @@ 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(
@@ -126,13 +118,12 @@ class _StartButtonState extends State<StartButton>
); );
}, },
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

@@ -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,24 +1,23 @@
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';
double _preOffset = 0; double _preOffset = 0;
class LogsFragment extends StatefulWidget { 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> with ViewMixin { class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState()); final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
final _scrollController = ScrollController( final _scrollController = ScrollController(
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite, initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
@@ -31,10 +30,34 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final appController = globalState.appController;
final appFlowingState = appController.appFlowingState;
_logsStateNotifier.value = _logsStateNotifier.value.copyWith( _logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: appFlowingState.logs, logs: globalState.appState.logs.list,
);
ref.listenManual(
logsProvider.select((state) => state.list),
(prev, next) {
if (prev != next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
}
},
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,
); );
} }
@@ -93,12 +116,6 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
); );
} }
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
super.initViewState();
});
}
double _calcCacheHeight(String text) { double _calcCacheHeight(String text) {
final cacheHeight = _cacheDynamicHeightMap.get(text); final cacheHeight = _cacheDynamicHeightMap.get(text);
if (cacheHeight != null) { if (cacheHeight != null) {
@@ -140,42 +157,12 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
}, duration: commonDuration); }, duration: commonDuration);
} }
Widget _wrapLogsUpdate(Widget child) {
return Selector<AppFlowingState, List<Log>>(
selector: (_, appFlowingState) => appFlowingState.logs,
shouldRebuild: (prev, next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (_, constraints) { builder: (_, constraints) {
_handleTryClearCache(constraints.maxWidth - 40); _handleTryClearCache(constraints.maxWidth - 40);
return Selector<AppState, bool?>( return Align(
selector: (_, appState) =>
appState.currentLabel == 'logs' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: _wrapLogsUpdate(
Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: ValueListenableBuilder<LogsState>( child: ValueListenableBuilder<LogsState>(
valueListenable: _logsStateNotifier, valueListenable: _logsStateNotifier,
@@ -231,8 +218,6 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
); );
}, },
), ),
),
),
); );
}, },
); );

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,12 +68,8 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
} }
} }
_initScaffold() { @override
WidgetsBinding.instance.addPostFrameCallback( List<Widget> get actions => [
(_) {
if (!mounted) return;
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = [
IconButton( IconButton(
onPressed: () { onPressed: () {
_updateProfiles(); _updateProfiles();
@@ -84,7 +78,7 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
final profiles = globalState.appController.config.profiles; final profiles = globalState.config.profiles;
showSheet( showSheet(
title: appLocalizations.profilesSort, title: appLocalizations.profilesSort,
context: context, context: context,
@@ -98,35 +92,31 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
iconSize: 26, iconSize: 26,
), ),
]; ];
commonScaffoldState?.floatingActionButton = FloatingActionButton(
@override
Widget? get floatingActionButton => FloatingActionButton(
heroTag: null, heroTag: null,
onPressed: _handleShowAddExtendPage, onPressed: _handleShowAddExtendPage,
child: const Icon( child: const Icon(
Icons.add, 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) {
initPageState();
} }
return child!;
}, },
child: Selector2<AppState, Config, ProfilesSelectorState>( fireImmediately: true,
selector: (_, appState, config) => ProfilesSelectorState( );
profiles: config.profiles, final profilesSelectorState = ref.watch(profilesSelectorStateProvider);
currentProfileId: config.currentProfileId, if (profilesSelectorState.profiles.isEmpty) {
columns: other.getProfilesColumns(appState.viewWidth),
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus( return NullStatus(
label: appLocalizations.nullProfileDesc, label: appLocalizations.nullProfileDesc,
); );
@@ -143,15 +133,18 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
child: Grid( child: Grid(
mainAxisSpacing: 16, mainAxisSpacing: 16,
crossAxisSpacing: 16, crossAxisSpacing: 16,
crossAxisCount: state.columns, crossAxisCount: profilesSelectorState.columns,
children: [ children: [
for (int i = 0; i < state.profiles.length; i++) for (int i = 0; i < profilesSelectorState.profiles.length; i++)
GridItem( GridItem(
child: ProfileItem( child: ProfileItem(
key: Key(state.profiles[i].id), key: Key(profilesSelectorState.profiles[i].id),
profile: state.profiles[i], profile: profilesSelectorState.profiles[i],
groupValue: state.currentProfileId, groupValue: profilesSelectorState.currentProfileId,
onChanged: globalState.appController.changeProfile, onChanged: (profileId) {
ref.read(currentProfileIdProvider.notifier).value =
profileId;
},
), ),
), ),
], ],
@@ -159,7 +152,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
), ),
); );
}, },
),
); );
} }
} }
@@ -195,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,
), ),
@@ -256,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;
@@ -286,6 +277,15 @@ 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>(); final key = GlobalKey<CommonPopupBoxState>();
@@ -326,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,
@@ -499,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,15 +36,14 @@ 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
@@ -56,9 +56,8 @@ class ProxyCard extends StatelessWidget {
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",
@@ -73,9 +72,6 @@ class ProxyCard extends StatelessWidget {
}, },
), ),
); );
},
),
);
} }
Widget _buildProxyNameText(BuildContext context) { Widget _buildProxyNameText(BuildContext context) {
@@ -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,19 +125,25 @@ 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(
groupName: groupName,
builder: (currentGroupName) {
return Stack( return Stack(
children: [ children: [
CommonCard( Consumer(
builder: (_, ref, child) {
final selectedProxyName =
ref.watch(getSelectedProxyNameProvider(groupName));
return CommonCard(
key: key, key: key,
enterAnimated: true,
onPressed: () { onPressed: () {
_changeProxy(context); _changeProxy(ref);
},
isSelected: selectedProxyName == proxy.name,
child: child!,
);
}, },
isSelected: currentGroupName == proxy.name,
child: Container( child: Container(
padding: const EdgeInsets.all(12), alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -154,25 +155,12 @@ class ProxyCard extends StatelessWidget {
if (type == ProxyCardType.expand) ...[ if (type == ProxyCardType.expand) ...[
SizedBox( SizedBox(
height: measure.bodySmallHeight, height: measure.bodySmallHeight,
child: Selector<AppState, String>( child: _ProxyDesc(
selector: (context, appState) => appState.getDesc( proxy: proxy,
proxy.type,
proxy.name,
),
builder: (_, desc, __) {
return EmojiText(
desc,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color:
context.textTheme.bodySmall?.color?.toLight,
),
);
},
), ),
), ),
const SizedBox( const SizedBox(
height: 8, height: 6,
), ),
delayText, delayText,
] else ] else
@@ -189,8 +177,8 @@ class ProxyCard extends StatelessWidget {
proxy.type, proxy.type,
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
color: context color:
.textTheme.bodySmall?.color?.toLight, context.textTheme.bodySmall?.color?.toLight,
), ),
), ),
), ),
@@ -203,19 +191,60 @@ class ProxyCard extends StatelessWidget {
), ),
), ),
), ),
if (groupType.isURLTestOrFallback) if (groupType.isComputedSelected)
Selector<Config, String>( Positioned(
selector: (_, config) { top: 0,
final selectedProxyName = right: 0,
config.currentSelectedMap[groupName]; child: _ProxyComputedMark(
return selectedProxyName ?? ''; groupName: groupName,
}, proxy: proxy,
builder: (_, value, child) { ),
if (value != proxy.name) return Container(); )
return child!; ],
}, );
child: Positioned.fill( }
child: Container( }
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, alignment: Alignment.topRight,
margin: const EdgeInsets.all(8), margin: const EdgeInsets.all(8),
child: Container( child: Container(
@@ -226,12 +255,6 @@ class ProxyCard extends StatelessWidget {
), ),
child: const SelectIcon(), 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 + 4; 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,38 +247,16 @@ 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,
@@ -318,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,
), ),
@@ -343,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,
@@ -417,13 +411,12 @@ 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);
@@ -434,9 +427,8 @@ class _ListHeaderState extends State<ListHeader>
if (index != -1) { if (index != -1) {
return iconMapEntryList[index].value; return iconMapEntryList[index].value;
} }
return icon; return this.icon;
}, }));
builder: (_, icon, __) {
return switch (iconStyle) { return switch (iconStyle) {
ProxiesIconStyle.standard => Container( ProxiesIconStyle.standard => Container(
height: 48, height: 48,
@@ -468,20 +460,20 @@ 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,
@@ -518,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:
@@ -528,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,25 +3,25 @@ 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();
@@ -43,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(
@@ -57,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),
); );
}, },
@@ -83,17 +83,13 @@ 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, ___) {
final proxyProviders =
providers.where((item) => item.type == "Proxy").map(
(item) => ProviderItem( (item) => ProviderItem(
provider: item, provider: item,
), ),
); );
final ruleProviders = final ruleProviders = providers.where((item) => item.type == "Rule").map(
providers.where((item) => item.type == "Rule").map(
(item) => ProviderItem( (item) => ProviderItem(
provider: item, provider: item,
), ),
@@ -110,8 +106,6 @@ class _ProvidersState extends State<Providers> {
...proxySection, ...proxySection,
...ruleSection, ...ruleSection,
]); ]);
},
);
} }
} }
@@ -124,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,
), ),
@@ -140,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();
@@ -149,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);
@@ -160,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,36 +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((_) { get actions => [
context.commonScaffoldState?.actions = [ if (_hasProviders)
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,
); );
}, },
@@ -38,32 +40,94 @@ 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(
IconButton(
onPressed: () { onPressed: () {
showExtendPage( showExtendPage(
context, context,
extendPageWidth: 360, extendPageWidth: 360,
title: appLocalizations.iconConfiguration, title: appLocalizations.iconConfiguration,
body: Selector<Config, Map<String, String>>( body: _IconConfigView(),
selector: (_, config) => config.proxiesStyle.iconMap,
shouldRebuild: (prev, next) {
return !stringAndStringMapEntryIterableEquality.equals(
prev.entries,
next.entries,
); );
}, },
builder: (_, iconMap, __) { icon: const Icon(
Icons.style_outlined,
),
),
IconButton(
onPressed: () {
showSheet(
title: appLocalizations.proxiesSetting,
context: context,
body: const ProxiesSetting(),
);
},
icon: const Icon(
Icons.tune,
),
)
];
@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
Widget build(BuildContext context) {
final proxiesType =
ref.watch(proxiesStyleSettingProvider.select((state) => state.type));
return switch (proxiesType) {
ProxiesType.tab => ProxiesTabFragment(
key: _proxiesTabKey,
),
ProxiesType.list => const ProxiesListFragment(),
};
}
}
class _IconConfigView extends ConsumerWidget {
const _IconConfigView();
@override
Widget build(BuildContext context, WidgetRef ref) {
final iconMap =
ref.watch(proxiesStyleSettingProvider.select((state) => state.iconMap));
final entries = iconMap.entries.toList(); final entries = iconMap.entries.toList();
return ListPage( return ListPage(
title: appLocalizations.iconConfiguration, title: appLocalizations.iconConfiguration,
@@ -88,57 +152,12 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
onChange: (entries) { onChange: (entries) {
final config = globalState.appController.config; ref.read(proxiesStyleSettingProvider.notifier).updateState(
config.proxiesStyle = config.proxiesStyle.copyWith( (state) => state.copyWith(
iconMap: Map.fromEntries(entries), iconMap: Map.fromEntries(entries),
);
},
);
},
), ),
); );
}, },
icon: const Icon(
Icons.style_outlined,
),
),
],
IconButton(
onPressed: () {
showSheet(
title: appLocalizations.proxiesSetting,
context: context,
body: const ProxiesSetting(),
);
},
icon: const Icon(
Icons.tune,
),
)
];
});
}
@override
Widget build(BuildContext context) {
return Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesStyle.type,
builder: (_, proxiesType, __) {
return ProxiesActionsBuilder(
builder: (state, child) {
if (state.isCurrent) {
_initActions(proxiesType, state.hasProvider);
}
return child!;
},
child: switch (proxiesType) {
ProxiesType.tab => ProxiesTabFragment(
key: _proxiesTabKey,
),
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
.read(proxiesStyleSettingProvider.notifier)
.updateState((state) {
return state.copyWith(
type: item, 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
.read(proxiesStyleSettingProvider.notifier)
.updateState((state) {
return state.copyWith(
sortType: item, 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
.read(proxiesStyleSettingProvider.notifier)
.updateState((state) {
return state.copyWith(
cardType: item, 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
.watch(proxiesStyleSettingProvider.notifier)
.updateState((state) {
return state.copyWith(
layout: item, 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)
.updateState((state) {
return state.copyWith(
iconStyle: item, 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,39 +151,36 @@ 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 ProxiesSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
);
},
shouldRebuild: (prev, next) {
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
_destroyTabController();
return true;
}
return false;
},
builder: (_, state, __) {
if (state.groupNames.isEmpty) {
return NullStatus( return NullStatus(
label: appLocalizations.nullProxies, label: appLocalizations.nullProxies,
); );
} }
final index = state.groupNames.indexWhere(
(item) => item == state.currentGroupName,
);
_updateTabController(state.groupNames.length, index);
if (state.groupNames.isEmpty) {
return Container();
}
final GroupNameKeyMap keyMap = {}; final GroupNameKeyMap keyMap = {};
final children = state.groupNames.map((groupName) { final children = groupNames.map((groupName) {
keyMap[groupName] = GlobalObjectKey(groupName); keyMap[groupName] = GlobalObjectKey(groupName);
return KeepScope( return KeepScope(
child: ProxyGroupView( child: ProxyGroupView(
@@ -214,7 +218,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
overlayColor: overlayColor:
const WidgetStatePropertyAll(Colors.transparent), const WidgetStatePropertyAll(Colors.transparent),
tabs: [ tabs: [
for (final groupName in state.groupNames) for (final groupName in groupNames)
Tab( Tab(
text: groupName, text: groupName,
), ),
@@ -254,12 +258,10 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
) )
], ],
); );
},
);
} }
} }
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,38 +307,9 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
); );
} }
initFab(bool isCurrent) {
if (!isCurrent) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
context.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 group = appState.getGroupWithName(groupName)!;
return ProxyGroupSelectorState(
proxyCardType: config.proxiesStyle.cardType,
proxiesSortType: config.proxiesStyle.sortType,
columns: other.getProxiesColumns(
appState.viewWidth,
config.proxiesStyle.layout,
),
sortNum: appState.sortNum,
proxies: group.all,
groupType: group.type,
testUrl: group.testUrl,
);
},
builder: (_, state, __) {
final proxies = state.proxies; final proxies = state.proxies;
final columns = state.columns; final columns = state.columns;
final proxyCardType = state.proxyCardType; final proxyCardType = state.proxyCardType;
@@ -354,13 +317,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
proxies, proxies,
state.testUrl, state.testUrl,
); );
return ActiveBuilder( return Align(
label: "proxies",
builder: (isCurrent, child) {
initFab(isCurrent);
return child!;
},
child: Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: GridView.builder( child: GridView.builder(
controller: _controller, controller: _controller,
@@ -389,9 +346,6 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
); );
}, },
), ),
),
);
},
); );
} }
} }
@@ -414,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

@@ -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,
@@ -122,7 +128,16 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
); );
} }
Widget _buildSubtitle(String url) { Widget _buildSubtitle() {
return Consumer(
builder: (_, ref, __) {
final url = ref.watch(
patchClashConfigProvider
.select((state) => state.geoXUrl.toJson()[geoItem.key]),
);
if (url == null) {
return SizedBox();
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -166,7 +181,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
avatar: const Icon(Icons.edit), avatar: const Icon(Icons.edit),
label: appLocalizations.edit, label: appLocalizations.edit,
onPressed: () { onPressed: () {
_updateUrl(url); _updateUrl(url, ref);
}, },
), ),
CommonChip( CommonChip(
@@ -180,6 +195,8 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
), ),
], ],
); );
},
);
} }
_handleUpdateGeoDataItem() async { _handleUpdateGeoDataItem() async {
@@ -201,7 +218,6 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
geoType: geoItem.label, geoType: geoItem.label,
), ),
); );
print(message);
if (message.isNotEmpty) throw message; if (message.isNotEmpty) throw message;
} catch (e) { } catch (e) {
isUpdating.value = false; isUpdating.value = false;
@@ -225,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,25 +86,143 @@ 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({ @override
bool? isSelected, Widget build(BuildContext context) {
required ThemeModeItem themeModeItem, 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( return CommonCard(
isSelected: isSelected, isSelected: fontFamilyItem.fontFamily == fontFamily,
onPressed: () { onPressed: () {
final appController = globalState.appController; ref.read(themeSettingProvider.notifier).updateState(
appController.config.themeProps = (state) => state.copyWith(
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,
),
),
],
),
),
);
},
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 = [
ThemeModeItem(
iconData: Icons.auto_mode,
label: appLocalizations.auto,
themeMode: ThemeMode.system,
),
ThemeModeItem(
iconData: Icons.light_mode,
label: appLocalizations.light,
themeMode: ThemeMode.light,
),
ThemeModeItem(
iconData: Icons.dark_mode,
label: appLocalizations.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, themeMode: themeModeItem.themeMode,
),
); );
}, },
child: Padding( child: Padding(
@@ -130,74 +246,25 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
), ),
), ),
); );
} },
separatorBuilder: (_, __) {
Widget _primaryColorCheckBox({ return const SizedBox(
bool? isSelected, width: 16,
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({ class _PrimaryColorItem extends ConsumerWidget {
bool? isSelected, const _PrimaryColorItem();
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, WidgetRef ref) {
List<ThemeModeItem> themeModeItems = [ final primaryColor =
ThemeModeItem( ref.watch(themeSettingProvider.select((state) => state.primaryColor));
iconData: Icons.auto_mode,
label: appLocalizations.auto,
themeMode: ThemeMode.system,
),
ThemeModeItem(
iconData: Icons.light_mode,
label: appLocalizations.light,
themeMode: ThemeMode.light,
),
ThemeModeItem(
iconData: Icons.dark_mode,
label: appLocalizations.dark,
themeMode: ThemeMode.dark,
),
];
List<Color?> primaryColors = [ List<Color?> primaryColors = [
null, null,
defaultPrimaryColor, defaultPrimaryColor,
@@ -207,84 +274,7 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
Colors.yellowAccent, Colors.yellowAccent,
Colors.purple, Colors.purple,
]; ];
List<FontFamilyItem> fontFamilyItems = [ return ItemCard(
FontFamilyItem(
label: appLocalizations.systemFont,
fontFamily: FontFamily.system,
),
const FontFamilyItem(
label: "MiSans",
fontFamily: FontFamily.miSans,
),
];
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( info: Info(
label: appLocalizations.themeColor, label: appLocalizations.themeColor,
iconData: Icons.palette, iconData: Icons.palette,
@@ -296,16 +286,20 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
bottom: 16, bottom: 16,
), ),
height: 88, height: 88,
child: Selector<Config, int?>( child: ListView.separated(
selector: (_, config) => config.themeProps.primaryColor,
builder: (_, currentPrimaryColor, __) {
return ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemBuilder: (_, index) { itemBuilder: (_, index) {
final primaryColor = primaryColors[index]; final color = primaryColors[index];
return _primaryColorCheckBox( return ColorSchemeBox(
isSelected: currentPrimaryColor == primaryColor?.value, isSelected: color?.value == primaryColor,
color: primaryColor, primaryColor: color,
onPressed: () {
ref.read(themeSettingProvider.notifier).updateState(
(state) => state.copyWith(
primaryColor: color?.value,
),
);
},
); );
}, },
separatorBuilder: (_, __) { separatorBuilder: (_, __) {
@@ -314,40 +308,38 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
); );
}, },
itemCount: primaryColors.length, itemCount: primaryColors.length,
),
),
); );
}, }
), }
),
), class _PrueBlackItem extends ConsumerWidget {
Padding( 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), padding: const EdgeInsets.symmetric(vertical: 16),
child: Selector<Config, bool>( child: ListItem.switchItem(
selector: (_, config) => config.themeProps.prueBlack,
builder: (_, value, ___) {
return ListItem.switchItem(
leading: Icon( leading: Icon(
Icons.contrast, Icons.contrast,
color: context.colorScheme.primary, color: context.colorScheme.primary,
), ),
title: Text(appLocalizations.prueBlackMode), title: Text(appLocalizations.prueBlackMode),
delegate: SwitchDelegate( delegate: SwitchDelegate(
value: value, value: prueBlack,
onChanged: (value) { onChanged: (value) {
final appController = globalState.appController; ref.read(themeSettingProvider.notifier).updateState(
appController.config.themeProps = (state) => state.copyWith(
appController.config.themeProps.copyWith(
prueBlack: value, 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,32 +7,33 @@ 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, extendPageWidth: 360,
), ),
@@ -55,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(),
),
),
], ],
); );
} }
@@ -91,125 +69,25 @@ 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 = [
Selector<AppState, MoreToolsSelectorState>( Consumer(
selector: (_, appState) { builder: (_, ref, __) {
return MoreToolsSelectorState( final state = ref.watch(moreToolsSelectorStateProvider);
navigationItems: appState.navigationItems.where((element) {
final isMore =
element.modes.contains(NavigationItemMode.more);
final isDesktop =
element.modes.contains(NavigationItemMode.desktop);
if (isMore && !isDesktop) return true;
if (appState.viewMode != ViewMode.mobile || !isMore) {
return false;
}
return true;
}).toList(),
);
},
builder: (_, state, __) {
if (state.navigationItems.isEmpty) { if (state.navigationItems.isEmpty) {
return Container(); return Container();
} }
@@ -229,7 +107,195 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
itemBuilder: (_, index) => items[index], itemBuilder: (_, index) => items[index],
padding: const EdgeInsets.only(bottom: 20), 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()),
);
},
textBuilder: (locale) => _getLocaleString(locale),
value: currentLocale,
),
);
}
}
class _ThemeItem extends StatelessWidget {
const _ThemeItem();
@override
Widget build(BuildContext context) {
return 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,
),
);
}
}
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

@@ -342,5 +342,6 @@
"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?" "cacheCorrupt": "The cache is corrupt. Do you want to clear it?",
"detectionTip": "Relying on third-party APIs is for reference only"
} }

View File

@@ -342,5 +342,6 @@
"copySuccess": "复制成功", "copySuccess": "复制成功",
"copyLink": "复制链接", "copyLink": "复制链接",
"exportFile": "导出文件", "exportFile": "导出文件",
"cacheCorrupt": "缓存已损坏,是否清空?" "cacheCorrupt": "缓存已损坏,是否清空?",
"detectionTip": "依赖第三方程序仅供参考"
} }

View File

@@ -180,6 +180,9 @@ class MessageLookup extends MessageLookupByLibrary {
"desc": MessageLookupByLibrary.simpleMessage( "desc": MessageLookupByLibrary.simpleMessage(
"A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.", "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.",
), ),
"detectionTip": MessageLookupByLibrary.simpleMessage(
"Relying on third-party APIs is for reference only",
),
"direct": MessageLookupByLibrary.simpleMessage("Direct"), "direct": MessageLookupByLibrary.simpleMessage("Direct"),
"disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"), "disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage( "disclaimerDesc": MessageLookupByLibrary.simpleMessage(

View File

@@ -122,6 +122,7 @@ class MessageLookup extends MessageLookupByLibrary {
"desc": MessageLookupByLibrary.simpleMessage( "desc": MessageLookupByLibrary.simpleMessage(
"基于ClashMeta的多平台代理客户端简单易用开源无广告。", "基于ClashMeta的多平台代理客户端简单易用开源无广告。",
), ),
"detectionTip": MessageLookupByLibrary.simpleMessage("依赖第三方程序仅供参考"),
"direct": MessageLookupByLibrary.simpleMessage("直连"), "direct": MessageLookupByLibrary.simpleMessage("直连"),
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"), "disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage( "disclaimerDesc": MessageLookupByLibrary.simpleMessage(

View File

@@ -2669,6 +2669,16 @@ class AppLocalizations {
args: [], args: [],
); );
} }
/// `Relying on third-party APIs is for reference only`
String get detectionTip {
return Intl.message(
'Relying on third-party APIs is for reference only',
name: 'detectionTip',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -11,53 +11,26 @@ import 'package:fl_clash/plugins/tile.dart';
import 'package:fl_clash/plugins/vpn.dart'; import 'package:fl_clash/plugins/vpn.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:package_info_plus/package_info_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'application.dart'; import 'application.dart';
import 'clash/core.dart'; import 'clash/core.dart';
import 'clash/lib.dart'; import 'clash/lib.dart';
import 'common/common.dart'; import 'common/common.dart';
import 'l10n/l10n.dart';
import 'models/models.dart'; import 'models/models.dart';
Future<void> main() async { Future<void> main() async {
globalState.isService = false; globalState.isService = false;
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await clashCore.preload();
globalState.packageInfo = await PackageInfo.fromPlatform();
final version = await system.version; final version = await system.version;
final config = await preferences.getConfig() ?? Config(); await clashCore.preload();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); await globalState.initApp(version);
await AppLocalizations.load(
other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
await android?.init(); await android?.init();
await window?.init(config.windowProps, version); await window?.init(version);
final appState = AppState(
mode: clashConfig.mode,
version: version,
selectedMap: config.currentSelectedMap,
);
final appFlowingState = AppFlowingState();
appState.navigationItems = navigation.getItems(
openLogs: config.appSetting.openLogs,
hasProxies: false,
);
tray.update(
appState: appState,
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
);
HttpOverrides.global = FlClashHttpOverrides(); HttpOverrides.global = FlClashHttpOverrides();
runAppWithPreferences( runApp(ProviderScope(
const Application(), child: const Application(),
appState: appState, ));
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
);
} }
@pragma('vm:entry-point') @pragma('vm:entry-point')
@@ -66,11 +39,7 @@ Future<void> _service(List<String> flags) async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
final quickStart = flags.contains("quick"); final quickStart = flags.contains("quick");
final clashLibHandler = ClashLibHandler(); final clashLibHandler = ClashLibHandler();
final config = await preferences.getConfig() ?? Config(); await globalState.init();
await AppLocalizations.load(
other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
tile?.addListener( tile?.addListener(
_TileListenerWithService( _TileListenerWithService(
@@ -83,33 +52,6 @@ Future<void> _service(List<String> flags) async {
}, },
), ),
); );
if (!quickStart) {
_handleMainIpc(clashLibHandler);
} else {
await ClashCore.initGeo();
globalState.packageInfo = await PackageInfo.fromPlatform();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
final homeDirPath = await appPath.homeDirPath;
await app?.tip(appLocalizations.startVpn);
clashLibHandler
.quickStart(
homeDirPath,
globalState.getUpdateConfigParams(config, clashConfig, false),
globalState.getCoreState(config, clashConfig),
)
.then(
(res) async {
if (res.isNotEmpty) {
await vpn?.stop();
exit(0);
}
await vpn?.start(
clashLibHandler.getAndroidVpnOptions(),
);
clashLibHandler.startListener();
},
);
}
vpn?.handleGetStartForegroundParams = () { vpn?.handleGetStartForegroundParams = () {
final traffic = clashLibHandler.getTraffic(); final traffic = clashLibHandler.getTraffic();
@@ -122,17 +64,22 @@ Future<void> _service(List<String> flags) async {
vpn?.addListener( vpn?.addListener(
_VpnListenerWithService( _VpnListenerWithService(
onStarted: (int fd) { onStarted: (int fd) {
clashLibHandler.startTun(fd); commonPrint.log("vpn started fd: $fd");
final time = clashLibHandler.startTun(fd);
commonPrint.log("vpn start tun time: $time");
}, },
onDnsChanged: (String dns) { onDnsChanged: (String dns) {
clashLibHandler.updateDns(dns); clashLibHandler.updateDns(dns);
}, },
), ),
); );
final invokeReceiverPort = ReceivePort(); final invokeReceiverPort = ReceivePort();
clashLibHandler.attachInvokePort( clashLibHandler.attachInvokePort(
invokeReceiverPort.sendPort.nativePort, invokeReceiverPort.sendPort.nativePort,
); );
invokeReceiverPort.listen( invokeReceiverPort.listen(
(message) async { (message) async {
final invokeMessage = InvokeMessage.fromJson(json.decode(message)); final invokeMessage = InvokeMessage.fromJson(json.decode(message));
@@ -153,6 +100,32 @@ Future<void> _service(List<String> flags) async {
} }
}, },
); );
if (!quickStart) {
_handleMainIpc(clashLibHandler);
} else {
commonPrint.log("quick start");
await ClashCore.initGeo();
app?.tip(appLocalizations.startVpn);
final homeDirPath = await appPath.homeDirPath;
clashLibHandler
.quickStart(
homeDirPath,
globalState.getUpdateConfigParams(),
globalState.getCoreState(),
)
.then(
(res) async {
if (res.isNotEmpty) {
await vpn?.stop();
exit(0);
}
await vpn?.start(
clashLibHandler.getAndroidVpnOptions(),
);
clashLibHandler.startListener();
},
);
}
} }
_handleMainIpc(ClashLibHandler clashLibHandler) { _handleMainIpc(ClashLibHandler clashLibHandler) {

View File

@@ -1,12 +1,10 @@
import 'package:fl_clash/clash/clash.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/state.dart'; import 'package:fl_clash/providers/config.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 AndroidManager extends StatefulWidget { class AndroidManager extends ConsumerStatefulWidget {
final Widget child; final Widget child;
const AndroidManager({ const AndroidManager({
@@ -15,47 +13,25 @@ class AndroidManager extends StatefulWidget {
}); });
@override @override
State<AndroidManager> createState() => _AndroidContainerState(); ConsumerState<AndroidManager> createState() => _AndroidContainerState();
} }
class _AndroidContainerState extends State<AndroidManager> { class _AndroidContainerState extends ConsumerState<AndroidManager> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} ref.listenManual(
appSettingProvider.select((state) => state.hidden),
Widget _updateCoreState(Widget child) { (prev, next) {
return Selector2<Config, ClashConfig, CoreState>( app?.updateExcludeFromRecents(next);
selector: (_, config, clashConfig) => globalState.getCoreState(
config,
clashConfig,
),
builder: (__, state, child) {
clashLib?.setState(state);
return child!;
}, },
child: child, fireImmediately: true
);
}
Widget _excludeContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.hidden,
builder: (_, hidden, child) {
app?.updateExcludeFromRecents(hidden);
return child!;
},
child: child,
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _updateCoreState( return widget.child;
_excludeContainer(
widget.child,
),
);
} }
} }

View File

@@ -1,8 +1,6 @@
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/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AppStateManager extends StatefulWidget { class AppStateManager extends StatefulWidget {
final Widget child; final Widget child;
@@ -18,48 +16,6 @@ class AppStateManager extends StatefulWidget {
class _AppStateManagerState extends State<AppStateManager> class _AppStateManagerState extends State<AppStateManager>
with WidgetsBindingObserver { with WidgetsBindingObserver {
_updateNavigationsContainer(Widget child) {
return Selector2<AppState, Config, UpdateNavigationsSelector>(
selector: (_, appState, config) {
final group = appState.currentGroups;
final hasProfile = config.profiles.isNotEmpty;
return UpdateNavigationsSelector(
openLogs: config.appSetting.openLogs,
hasProxies: group.isNotEmpty && hasProfile,
);
},
builder: (context, state, child) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
globalState.appController.appState.navigationItems =
navigation.getItems(
openLogs: state.openLogs,
hasProxies: state.hasProxies,
);
},
);
return child!;
},
child: child,
);
}
_cacheStateChange(Widget child) {
return Selector2<Config, ClashConfig, String>(
selector: (_, config, clashConfig) => "$clashConfig $config",
shouldRebuild: (prev, next) {
if (prev != next) {
globalState.appController.savePreferencesDebounce();
}
return prev != next;
},
builder: (context, state, child) {
return child!;
},
child: child,
);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -76,7 +32,7 @@ class _AppStateManagerState extends State<AppStateManager>
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async { Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
if (state == AppLifecycleState.paused || if (state == AppLifecycleState.paused ||
state == AppLifecycleState.inactive) { state == AppLifecycleState.inactive) {
globalState.appController.savePreferencesDebounce(); globalState.appController.savePreferences();
render?.pause(); render?.pause();
} else { } else {
render?.resume(); render?.resume();
@@ -85,8 +41,9 @@ class _AppStateManagerState extends State<AppStateManager>
@override @override
void didChangePlatformBrightness() { void didChangePlatformBrightness() {
globalState.appController.appState.brightness = globalState.appController.updateBrightness(
WidgetsBinding.instance.platformDispatcher.platformBrightness; WidgetsBinding.instance.platformDispatcher.platformBrightness,
);
} }
@override @override
@@ -95,11 +52,7 @@ class _AppStateManagerState extends State<AppStateManager>
onPointerHover: (_) { onPointerHover: (_) {
render?.resume(); render?.resume();
}, },
child: _cacheStateChange( child: widget.child,
_updateNavigationsContainer(
widget.child,
),
),
); );
} }
} }

View File

@@ -2,13 +2,14 @@ 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/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:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../common/function.dart'; class ClashManager extends ConsumerStatefulWidget {
class ClashManager extends StatefulWidget {
final Widget child; final Widget child;
const ClashManager({ const ClashManager({
@@ -17,78 +18,35 @@ class ClashManager extends StatefulWidget {
}); });
@override @override
State<ClashManager> createState() => _ClashContainerState(); ConsumerState<ClashManager> createState() => _ClashContainerState();
}
class _ClashContainerState extends State<ClashManager> with AppMessageListener {
Widget _updateContainer(Widget child) {
return Selector2<Config, ClashConfig, ClashConfigState>(
selector: (_, config, clashConfig) => ClashConfigState(
overrideDns: config.overrideDns,
mixedPort: clashConfig.mixedPort,
allowLan: clashConfig.allowLan,
ipv6: clashConfig.ipv6,
logLevel: clashConfig.logLevel,
geodataLoader: clashConfig.geodataLoader,
externalController: clashConfig.externalController,
mode: clashConfig.mode,
findProcessMode: clashConfig.findProcessMode,
keepAliveInterval: clashConfig.keepAliveInterval,
unifiedDelay: clashConfig.unifiedDelay,
tcpConcurrent: clashConfig.tcpConcurrent,
tun: clashConfig.tun,
dns: clashConfig.dns,
hosts: clashConfig.hosts,
geoXUrl: clashConfig.geoXUrl,
rules: clashConfig.rules,
globalRealUa: clashConfig.globalRealUa,
),
shouldRebuild: (prev, next) {
if (prev != next) {
globalState.appController.updateClashConfigDebounce();
}
return prev != next;
},
builder: (__, state, child) {
return child!;
},
child: child,
);
}
Widget _changeProfileContainer(Widget child) {
return Selector<Config, String?>(
selector: (_, config) => config.currentProfileId,
shouldRebuild: (prev, next) {
if (prev != next) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final appController = globalState.appController;
appController.appState.delayMap = {};
appController.applyProfile();
});
}
return prev != next;
},
builder: (__, state, child) {
return child!;
},
child: child,
);
} }
class _ClashContainerState extends ConsumerState<ClashManager>
with AppMessageListener {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _changeProfileContainer( return widget.child;
_updateContainer(
widget.child,
),
);
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
clashMessage.addListener(this); clashMessage.addListener(this);
ref.listenManual(currentProfileIdProvider, (prev, next) {
if (prev != next) {
globalState.appController.handleChangeProfile();
}
});
ref.listenManual(coreStateProvider, (prev, next) async {
if (prev != next) {
await clashCore.setState(next);
}
});
ref.listenManual(clashConfigStateProvider, (prev, next) {
if (prev != next) {
globalState.appController.updateClashConfigDebounce();
}
});
} }
@override @override
@@ -113,7 +71,7 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
@override @override
void onLog(Log log) { void onLog(Log log) {
globalState.appController.appFlowingState.addLog(log); ref.watch(logsProvider.notifier).addLog(log);
if (log.logLevel == LogLevel.error) { if (log.logLevel == LogLevel.error) {
globalState.showNotifier(log.payload ?? ''); globalState.showNotifier(log.payload ?? '');
} }
@@ -122,19 +80,18 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
@override @override
void onRequest(Connection connection) async { void onRequest(Connection connection) async {
globalState.appController.appState.addRequest(connection); ref.watch(requestsProvider.notifier).addRequest(connection);
super.onRequest(connection); super.onRequest(connection);
} }
@override @override
Future<void> onLoaded(String providerName) async { Future<void> onLoaded(String providerName) async {
final appController = globalState.appController; ref.watch(providersProvider.notifier).setProvider(
appController.appState.setProvider(
await clashCore.getExternalProvider( await clashCore.getExternalProvider(
providerName, providerName,
), ),
); );
await appController.updateGroupsDebounce(); await globalState.appController.updateGroupsDebounce();
super.onLoaded(providerName); super.onLoaded(providerName);
} }
} }

View File

@@ -1,12 +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/enum/enum.dart';
import 'package:fl_clash/models/common.dart'; import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/providers/config.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.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:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:provider/provider.dart';
class HotKeyManager extends StatelessWidget { class HotKeyManager extends StatelessWidget {
final Widget child; final Widget child;
@@ -61,14 +61,18 @@ class HotKeyManager extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<Config, List<HotKeyAction>>( return Consumer(
selector: (_, config) => config.hotKeyActions, builder: (_, ref, child) {
shouldRebuild: (prev, next) { ref.listenManual(
return !hotKeyActionListEquality.equals(prev, next); hotKeyActionsProvider,
(prev, next) {
if (!hotKeyActionListEquality.equals(prev, next)) {
_updateHotKeys(hotKeyActions: next);
}
}, },
builder: (_, hotKeyActions, __) { fireImmediately: true,
_updateHotKeys(hotKeyActions: hotKeyActions); );
return child; return child!;
}, },
child: child, child: child,
); );

View File

@@ -5,7 +5,6 @@ export 'clash_manager.dart';
export 'tile_manager.dart'; export 'tile_manager.dart';
export 'app_state_manager.dart'; export 'app_state_manager.dart';
export 'vpn_manager.dart'; export 'vpn_manager.dart';
export 'media_manager.dart';
export 'proxy_manager.dart'; export 'proxy_manager.dart';
export 'connectivity_manager.dart'; export 'connectivity_manager.dart';
export 'message_manager.dart'; export 'message_manager.dart';

View File

@@ -1,18 +0,0 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
class MediaManager extends StatelessWidget {
final Widget child;
const MediaManager({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
globalState.measure = Measure.of(context);
return child;
}
}

View File

@@ -1,13 +1,19 @@
import 'package:fl_clash/common/proxy.dart'; import 'package:fl_clash/common/proxy.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/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 ProxyManager extends StatelessWidget { class ProxyManager extends ConsumerStatefulWidget {
final Widget child; final Widget child;
const ProxyManager({super.key, required this.child}); const ProxyManager({super.key, required this.child});
@override
ConsumerState createState() => _ProxyManagerState();
}
class _ProxyManagerState extends ConsumerState<ProxyManager> {
_updateProxy(ProxyState proxyState) async { _updateProxy(ProxyState proxyState) async {
final isStart = proxyState.isStart; final isStart = proxyState.isStart;
final systemProxy = proxyState.systemProxy; final systemProxy = proxyState.systemProxy;
@@ -20,19 +26,21 @@ class ProxyManager extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) { void initState() {
return Selector3<AppFlowingState, Config, ClashConfig, ProxyState>( super.initState();
selector: (_, appFlowingState, config, clashConfig) => ProxyState( ref.listenManual(
isStart: appFlowingState.isStart, proxyStateProvider,
systemProxy: config.networkProps.systemProxy, (prev, next) {
port: clashConfig.mixedPort, if (prev != next) {
bassDomain: config.networkProps.bypassDomain, _updateProxy(next);
), }
builder: (_, state, child) {
_updateProxy(state);
return child!;
}, },
child: child, fireImmediately: true,
); );
} }
@override
Widget build(BuildContext context) {
return widget.child;
}
} }

View File

@@ -1,11 +1,11 @@
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/state.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 'package:tray_manager/tray_manager.dart'; import 'package:tray_manager/tray_manager.dart';
class TrayManager extends StatefulWidget { class TrayManager extends ConsumerStatefulWidget {
final Widget child; final Widget child;
const TrayManager({ const TrayManager({
@@ -14,43 +14,27 @@ class TrayManager extends StatefulWidget {
}); });
@override @override
State<TrayManager> createState() => _TrayContainerState(); ConsumerState<TrayManager> createState() => _TrayContainerState();
} }
class _TrayContainerState extends State<TrayManager> with TrayListener { class _TrayContainerState extends ConsumerState<TrayManager> with TrayListener {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
trayManager.addListener(this); trayManager.addListener(this);
ref.listenManual(
trayStateProvider,
(prev, next) {
if (prev != next) {
globalState.appController.updateTray();
}
},
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector4<AppState, AppFlowingState, Config, ClashConfig, TrayState>( return widget.child;
selector: (_, appState, appFlowingState, config, clashConfig) =>
TrayState(
mode: clashConfig.mode,
autoLaunch: config.appSetting.autoLaunch,
isStart: appFlowingState.isStart,
locale: config.appSetting.locale,
systemProxy: config.networkProps.systemProxy,
tunEnable: clashConfig.tun.enable,
brightness: appState.brightness,
port: clashConfig.mixedPort,
groups: appState.groups,
map: appState.selectedMap,
),
shouldRebuild: (prev, next) {
if (prev != next) {
globalState.appController.updateTray();
}
return prev != next;
},
builder: (_, state, child) {
return child!;
},
child: widget.child,
);
} }
@override @override

View File

@@ -1,11 +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/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/providers/state.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 VpnManager extends StatefulWidget { class VpnManager extends ConsumerStatefulWidget {
final Widget child; final Widget child;
const VpnManager({ const VpnManager({
@@ -14,44 +15,33 @@ class VpnManager extends StatefulWidget {
}); });
@override @override
State<VpnManager> createState() => _VpnContainerState(); ConsumerState<VpnManager> createState() => _VpnContainerState();
}
class _VpnContainerState extends ConsumerState<VpnManager> {
@override
void initState() {
super.initState();
ref.listenManual(vpnStateProvider, (prev, next) {
showTip();
});
} }
class _VpnContainerState extends State<VpnManager> {
showTip() { showTip() {
debouncer.call( debouncer.call(
DebounceTag.vpnTip, DebounceTag.vpnTip,
() { () {
WidgetsBinding.instance.addPostFrameCallback((_) { if (ref.read(runTimeProvider.notifier).isStart) {
final appFlowingState = globalState.appController.appFlowingState;
if (appFlowingState.isStart) {
globalState.showNotifier( globalState.showNotifier(
appLocalizations.vpnTip, appLocalizations.vpnTip,
); );
} }
});
}, },
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector2<Config, ClashConfig, VPNState>( return widget.child;
selector: (_, config, clashConfig) => VPNState(
accessControl: config.accessControl,
vpnProps: config.vpnProps,
stack: clashConfig.tun.stack,
),
shouldRebuild: (prev, next) {
if (prev != next) {
showTip();
}
return prev != next;
},
builder: (_, __, child) {
return child!;
},
child: widget.child,
);
} }
} }

View File

@@ -2,14 +2,15 @@ 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/providers/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:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_ext/window_ext.dart'; import 'package:window_ext/window_ext.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class WindowManager extends StatefulWidget { class WindowManager extends ConsumerStatefulWidget {
final Widget child; final Widget child;
const WindowManager({ const WindowManager({
@@ -18,17 +19,22 @@ class WindowManager extends StatefulWidget {
}); });
@override @override
State<WindowManager> createState() => _WindowContainerState(); ConsumerState<WindowManager> createState() => _WindowContainerState();
} }
class _WindowContainerState extends State<WindowManager> class _WindowContainerState extends ConsumerState<WindowManager>
with WindowListener, WindowExtListener { with WindowListener, WindowExtListener {
Function? updateLaunchDebounce; @override
Widget build(BuildContext context) {
return widget.child;
}
_autoLaunchContainer(Widget child) { @override
return Selector<Config, bool>( void initState() {
selector: (_, config) => config.appSetting.autoLaunch, super.initState();
shouldRebuild: (prev, next) { ref.listenManual(
appSettingProvider.select((state) => state.autoLaunch),
(prev, next) {
if (prev != next) { if (prev != next) {
debouncer.call( debouncer.call(
DebounceTag.autoLaunch, DebounceTag.autoLaunch,
@@ -37,23 +43,8 @@ class _WindowContainerState extends State<WindowManager>
}, },
); );
} }
return prev != next;
}, },
builder: (_, state, child) {
return child!;
},
child: child,
); );
}
@override
Widget build(BuildContext context) {
return _autoLaunchContainer(widget.child);
}
@override
void initState() {
super.initState();
windowExtManager.addListener(this); windowExtManager.addListener(this);
windowManager.addListener(this); windowManager.addListener(this);
} }
@@ -86,10 +77,11 @@ class _WindowContainerState extends State<WindowManager>
Future<void> onWindowMoved() async { Future<void> onWindowMoved() async {
super.onWindowMoved(); super.onWindowMoved();
final offset = await windowManager.getPosition(); final offset = await windowManager.getPosition();
final config = globalState.appController.config; ref.read(windowSettingProvider.notifier).updateState(
config.windowProps = config.windowProps.copyWith( (state) => state.copyWith(
top: offset.dy, top: offset.dy,
left: offset.dx, left: offset.dx,
),
); );
} }
@@ -97,10 +89,11 @@ class _WindowContainerState extends State<WindowManager>
Future<void> onWindowResized() async { Future<void> onWindowResized() async {
super.onWindowResized(); super.onWindowResized();
final size = await windowManager.getSize(); final size = await windowManager.getSize();
final config = globalState.appController.config; ref.read(windowSettingProvider.notifier).updateState(
config.windowProps = config.windowProps.copyWith( (state) => state.copyWith(
width: size.width, width: size.width,
height: size.height, height: size.height,
),
); );
} }
@@ -134,9 +127,9 @@ class WindowHeaderContainer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<AppState, int>( return Consumer(
selector: (_, appState) => appState.version, builder: (_, ref, child) {
builder: (_, version, child) { final version = ref.watch(versionProvider);
if (version <= 10 && Platform.isMacOS) { if (version <= 10 && Platform.isMacOS) {
return child!; return child!;
} }

View File

@@ -1,364 +1,41 @@
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:freezed_annotation/freezed_annotation.dart';
import 'common.dart'; import 'common.dart';
import 'core.dart'; import 'core.dart';
import 'profile.dart';
part 'generated/app.freezed.dart';
typedef DelayMap = Map<String, Map<String, int?>>; typedef DelayMap = Map<String, Map<String, int?>>;
class AppState with ChangeNotifier { @freezed
List<NavigationItem> _navigationItems; class AppState with _$AppState {
bool _isInit; const factory AppState({
VersionInfo? _versionInfo; @Default(false) bool isInit,
String _currentLabel; @Default(PageLabel.dashboard) PageLabel pageLabel,
SystemColorSchemes _systemColorSchemes; @Default([]) List<Package> packages,
num _sortNum; @Default(ColorSchemes()) ColorSchemes colorSchemes,
Mode _mode; @Default(0) int sortNum,
DelayMap _delayMap; required double viewWidth,
SelectedMap _selectedMap; @Default({}) DelayMap delayMap,
List<Group> _groups; @Default([]) List<Group> groups,
double _viewWidth; @Default(0) int checkIpNum,
final FixedList<Connection> _requests; Brightness? brightness,
num _checkIpNum; int? runTime,
List<ExternalProvider> _providers; @Default([]) List<ExternalProvider> providers,
List<Package> _packages; String? localIp,
Brightness? _brightness; required FixedList<Connection> requests,
int _version;
AppState({
required Mode mode,
required SelectedMap selectedMap,
required int version, required int version,
}) required FixedList<Log> logs,
: _navigationItems = [], required FixedList<Traffic> traffics,
_isInit = false, required Traffic totalTraffic,
_currentLabel = "dashboard", }) = _AppState;
_viewWidth = other
.getScreenSize()
.width,
_selectedMap = selectedMap,
_sortNum = 0,
_checkIpNum = 0,
_requests = FixedList(1000),
_mode = mode,
_brightness = null,
_delayMap = {},
_groups = [],
_providers = [],
_packages = [],
_systemColorSchemes = const SystemColorSchemes(),
_version = version;
String get currentLabel => _currentLabel;
set currentLabel(String value) {
if (_currentLabel != value) {
_currentLabel = value;
notifyListeners();
}
} }
List<NavigationItem> get navigationItems => _navigationItems; extension AppStateExt on AppState {
ViewMode get viewMode => other.getViewMode(viewWidth);
set navigationItems(List<NavigationItem> value) { bool get isStart => runTime != null;
if (!navigationItemListEquality.equals(_navigationItems, value)) {
_navigationItems = value;
notifyListeners();
}
}
List<NavigationItem> get currentNavigationItems {
NavigationItemMode navigationItemMode;
if (_viewWidth <= maxMobileWidth) {
navigationItemMode = NavigationItemMode.mobile;
} else {
navigationItemMode = NavigationItemMode.desktop;
}
return navigationItems
.where(
(element) => element.modes.contains(navigationItemMode),
)
.toList();
}
bool get isInit => _isInit;
set isInit(bool value) {
if (_isInit != value) {
_isInit = value;
notifyListeners();
}
}
String getDesc(String type, String proxyName) {
final groupTypeNamesList = GroupType.values.map((e) => e.name).toList();
if (!groupTypeNamesList.contains(type)) {
return type;
} else {
final index = groups.indexWhere((element) => element.name == proxyName);
if (index == -1) return type;
return "$type(${groups[index].now ?? '*'})";
}
}
String getRealProxyName(String proxyName) {
if (proxyName.isEmpty) return proxyName;
final index = groups.indexWhere((element) => element.name == proxyName);
if (index == -1) return proxyName;
final group = groups[index];
final currentSelectedName =
group.getCurrentSelectedName(selectedMap[proxyName] ?? '');
if (currentSelectedName.isEmpty) return proxyName;
return getRealProxyName(
currentSelectedName,
);
}
String? get showProxyName {
if (currentGroups.isEmpty) {
return UsedProxy.DIRECT.name;
}
final firstGroup = currentGroups.first;
final firstGroupName = firstGroup.name;
return selectedMap[firstGroupName] ?? firstGroup.now;
}
VersionInfo? get versionInfo => _versionInfo;
set versionInfo(VersionInfo? value) {
if (_versionInfo != value) {
_versionInfo = value;
notifyListeners();
}
}
List<Connection> get requests => _requests.list;
addRequest(Connection value) {
_requests.add(value);
notifyListeners();
}
SystemColorSchemes get systemColorSchemes => _systemColorSchemes;
set systemColorSchemes(SystemColorSchemes value) {
if (_systemColorSchemes != value) {
_systemColorSchemes = value;
notifyListeners();
}
}
List<Group> get groups => _groups;
set groups(List<Group> value) {
if (!groupListEquality.equals(_groups, value)) {
_groups = value;
notifyListeners();
}
}
num get sortNum => _sortNum;
set sortNum(num value) {
if (_sortNum != value) {
_sortNum = value;
notifyListeners();
}
}
num get checkIpNum => _checkIpNum;
set checkIpNum(num value) {
if (_checkIpNum != value) {
_checkIpNum = value;
notifyListeners();
}
}
Mode get mode => _mode;
set mode(Mode value) {
if (_mode != value) {
_mode = value;
notifyListeners();
}
}
SelectedMap get selectedMap {
return _selectedMap;
}
set selectedMap(SelectedMap value) {
if (!stringAndStringMapEquality.equals(_selectedMap, value)) {
_selectedMap = value;
notifyListeners();
}
}
List<Group> get currentGroups {
switch (mode) {
case Mode.direct:
return [];
case Mode.global:
return groups.toList();
case Mode.rule:
return groups
.where((item) => item.hidden == false)
.where((element) => element.name != GroupName.GLOBAL.name)
.toList();
}
}
double get viewWidth => _viewWidth;
set viewWidth(double value) {
if (_viewWidth != value) {
_viewWidth = value;
notifyListeners();
}
}
ViewMode get viewMode => other.getViewMode(_viewWidth);
DelayMap get delayMap {
return _delayMap;
}
set delayMap(DelayMap value) {
if (_delayMap != value) {
_delayMap = value;
notifyListeners();
}
}
setDelay(Delay delay) {
if (_delayMap[delay.url]?[delay.name] != delay.value) {
final DelayMap newDelayMap = Map.from(_delayMap);
if (newDelayMap[delay.url] == null) {
newDelayMap[delay.url] = {};
}
newDelayMap[delay.url]![delay.name] = delay.value;
_delayMap = newDelayMap;
notifyListeners();
}
}
List<Package> get packages => _packages;
set packages(List<Package> value) {
if (!packageListEquality.equals(_packages, value)) {
_packages = value;
notifyListeners();
}
}
List<ExternalProvider> get providers => _providers;
set providers(List<ExternalProvider> value) {
if (!externalProviderListEquality.equals(_providers, value)) {
_providers = value;
notifyListeners();
}
}
setProvider(ExternalProvider? provider) {
if (provider == null) return;
final index = _providers.indexWhere((item) => item.name == provider.name);
if (index == -1) return;
_providers = List.from(_providers)
..[index] = provider;
notifyListeners();
}
Group? getGroupWithName(String groupName) {
final index =
currentGroups.indexWhere((element) => element.name == groupName);
return index != -1 ? currentGroups[index] : null;
}
Brightness? get brightness => _brightness;
set brightness(Brightness? value) {
if (_brightness != value) {
_brightness = value;
notifyListeners();
}
}
int get version => _version;
set version(int value) {
if (_version != value) {
_version = value;
notifyListeners();
}
}
}
class AppFlowingState with ChangeNotifier {
int? _runTime;
final FixedList<Log> _logs;
List<Traffic> _traffics;
Traffic _totalTraffic;
String? _localIp;
AppFlowingState()
: _logs = FixedList(1000),
_traffics = [],
_totalTraffic = Traffic();
bool get isStart => _runTime != null;
int? get runTime => _runTime;
set runTime(int? value) {
if (_runTime != value) {
_runTime = value;
notifyListeners();
}
}
List<Log> get logs => _logs.list;
addLog(Log log) {
_logs.add(log);
notifyListeners();
}
List<Traffic> get traffics => _traffics;
set traffics(List<Traffic> value) {
if (_traffics != value) {
_traffics = value;
notifyListeners();
}
}
addTraffic(Traffic traffic) {
_traffics = List.from(_traffics)
..add(traffic);
const maxLength = 30;
_traffics = _traffics.safeSublist(_traffics.length - maxLength);
notifyListeners();
}
Traffic get totalTraffic => _totalTraffic;
set totalTraffic(Traffic value) {
if (_totalTraffic != value) {
_totalTraffic = value;
notifyListeners();
}
}
String? get localIp => _localIp;
set localIp(String? value) {
if (_localIp != value) {
_localIp = value;
notifyListeners();
}
}
} }

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