Optimize performance
Update core Optimize core stability Fix linux tun authority check error Fix some issues
This commit is contained in:
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -6,3 +6,9 @@
|
||||
path = plugins/flutter_distributor
|
||||
url = git@github.com:chen08209/flutter_distributor.git
|
||||
branch = FlClash
|
||||
[submodule "plugins/tray_manager"]
|
||||
path = plugins/tray_manager
|
||||
url = git@github.com:chen08209/tray_manager.git
|
||||
branch = main
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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:
|
||||
# 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
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
analyzer:
|
||||
plugins:
|
||||
- custom_lint
|
||||
|
||||
@@ -7,6 +7,7 @@ enum class AccessControlMode {
|
||||
}
|
||||
|
||||
data class AccessControl(
|
||||
val enable: Boolean,
|
||||
val mode: AccessControlMode,
|
||||
val acceptList: List<String>,
|
||||
val rejectList: List<String>,
|
||||
@@ -17,7 +18,7 @@ data class CIDR(val address: InetAddress, val prefixLength: Int)
|
||||
data class VpnOptions(
|
||||
val enable: Boolean,
|
||||
val port: Int,
|
||||
val accessControl: AccessControl?,
|
||||
val accessControl: AccessControl,
|
||||
val allowBypass: Boolean,
|
||||
val systemProxy: Boolean,
|
||||
val bypassDomain: List<String>,
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
@@ -92,11 +93,13 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
"setProtect" -> {
|
||||
val fd = call.argument<Int>("fd")
|
||||
if (fd != null) {
|
||||
if (flClashService is FlClashVpnService) {
|
||||
if (fd != null && flClashService is FlClashVpnService) {
|
||||
try {
|
||||
(flClashService as FlClashVpnService).protect(fd)
|
||||
result.success(true)
|
||||
} catch (e: RuntimeException) {
|
||||
result.success(false)
|
||||
}
|
||||
result.success(true)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
|
||||
@@ -68,17 +68,19 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
}
|
||||
addDnsServer(options.dnsServerAddress)
|
||||
setMtu(9000)
|
||||
options.accessControl?.let { accessControl ->
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
addAllowedApplication(it)
|
||||
options.accessControl.let { accessControl ->
|
||||
if (accessControl.enable) {
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
addAllowedApplication(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccessControlMode.rejectSelected -> {
|
||||
(accessControl.rejectList - packageName).forEach {
|
||||
addDisallowedApplication(it)
|
||||
AccessControlMode.rejectSelected -> {
|
||||
(accessControl.rejectList - packageName).forEach {
|
||||
addDisallowedApplication(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ targets:
|
||||
options:
|
||||
build_extensions:
|
||||
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
|
||||
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
|
||||
freezed:
|
||||
options:
|
||||
build_extensions:
|
||||
|
||||
Submodule core/Clash.Meta updated: 0c03d8e4b4...76b0d7e8bc
@@ -22,99 +22,99 @@ func (result ActionResult) Json() ([]byte, error) {
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (action Action) wrapMessage(data interface{}) []byte {
|
||||
sendAction := ActionResult{
|
||||
func (action Action) getResult(data interface{}) []byte {
|
||||
resultAction := ActionResult{
|
||||
Id: action.Id,
|
||||
Method: action.Method,
|
||||
Data: data,
|
||||
}
|
||||
res, _ := sendAction.Json()
|
||||
res, _ := resultAction.Json()
|
||||
return res
|
||||
}
|
||||
|
||||
func handleAction(action *Action, send func([]byte)) {
|
||||
func handleAction(action *Action, result func(data interface{})) {
|
||||
switch action.Method {
|
||||
case initClashMethod:
|
||||
data := action.Data.(string)
|
||||
send(action.wrapMessage(handleInitClash(data)))
|
||||
result(handleInitClash(data))
|
||||
return
|
||||
case getIsInitMethod:
|
||||
send(action.wrapMessage(handleGetIsInit()))
|
||||
result(handleGetIsInit())
|
||||
return
|
||||
case forceGcMethod:
|
||||
handleForceGc()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case shutdownMethod:
|
||||
send(action.wrapMessage(handleShutdown()))
|
||||
result(handleShutdown())
|
||||
return
|
||||
case validateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
send(action.wrapMessage(handleValidateConfig(data)))
|
||||
result(handleValidateConfig(data))
|
||||
return
|
||||
case updateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
send(action.wrapMessage(handleUpdateConfig(data)))
|
||||
result(handleUpdateConfig(data))
|
||||
return
|
||||
case getProxiesMethod:
|
||||
send(action.wrapMessage(handleGetProxies()))
|
||||
result(handleGetProxies())
|
||||
return
|
||||
case changeProxyMethod:
|
||||
data := action.Data.(string)
|
||||
handleChangeProxy(data, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getTrafficMethod:
|
||||
send(action.wrapMessage(handleGetTraffic()))
|
||||
result(handleGetTraffic())
|
||||
return
|
||||
case getTotalTrafficMethod:
|
||||
send(action.wrapMessage(handleGetTotalTraffic()))
|
||||
result(handleGetTotalTraffic())
|
||||
return
|
||||
case resetTrafficMethod:
|
||||
handleResetTraffic()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case asyncTestDelayMethod:
|
||||
data := action.Data.(string)
|
||||
handleAsyncTestDelay(data, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getConnectionsMethod:
|
||||
send(action.wrapMessage(handleGetConnections()))
|
||||
result(handleGetConnections())
|
||||
return
|
||||
case closeConnectionsMethod:
|
||||
send(action.wrapMessage(handleCloseConnections()))
|
||||
result(handleCloseConnections())
|
||||
return
|
||||
case closeConnectionMethod:
|
||||
id := action.Data.(string)
|
||||
send(action.wrapMessage(handleCloseConnection(id)))
|
||||
result(handleCloseConnection(id))
|
||||
return
|
||||
case getExternalProvidersMethod:
|
||||
send(action.wrapMessage(handleGetExternalProviders()))
|
||||
result(handleGetExternalProviders())
|
||||
return
|
||||
case getExternalProviderMethod:
|
||||
externalProviderName := action.Data.(string)
|
||||
send(action.wrapMessage(handleGetExternalProvider(externalProviderName)))
|
||||
result(handleGetExternalProvider(externalProviderName))
|
||||
case updateGeoDataMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
send(action.wrapMessage(err.Error()))
|
||||
result(err.Error())
|
||||
return
|
||||
}
|
||||
geoType := params["geo-type"]
|
||||
geoName := params["geo-name"]
|
||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case updateExternalProviderMethod:
|
||||
providerName := action.Data.(string)
|
||||
handleUpdateExternalProvider(providerName, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case sideLoadExternalProviderMethod:
|
||||
@@ -122,46 +122,56 @@ func handleAction(action *Action, send func([]byte)) {
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
send(action.wrapMessage(err.Error()))
|
||||
result(err.Error())
|
||||
return
|
||||
}
|
||||
providerName := params["providerName"]
|
||||
data := params["data"]
|
||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case startLogMethod:
|
||||
handleStartLog()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case stopLogMethod:
|
||||
handleStopLog()
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return
|
||||
case startListenerMethod:
|
||||
send(action.wrapMessage(handleStartListener()))
|
||||
result(handleStartListener())
|
||||
return
|
||||
case stopListenerMethod:
|
||||
send(action.wrapMessage(handleStopListener()))
|
||||
result(handleStopListener())
|
||||
return
|
||||
case getCountryCodeMethod:
|
||||
ip := action.Data.(string)
|
||||
handleGetCountryCode(ip, func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getMemoryMethod:
|
||||
handleGetMemory(func(value string) {
|
||||
send(action.wrapMessage(value))
|
||||
result(value)
|
||||
})
|
||||
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:
|
||||
handle := nextHandle(action, send)
|
||||
handle := nextHandle(action, result)
|
||||
if handle {
|
||||
return
|
||||
} else {
|
||||
send(action.wrapMessage(action.DefaultValue))
|
||||
result(action.DefaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
var (
|
||||
isRunning = false
|
||||
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))
|
||||
)
|
||||
|
||||
@@ -215,6 +215,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
targetConfig.Tun.Device = patchConfig.Tun.Device
|
||||
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
||||
targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
||||
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
|
||||
targetConfig.GeodataLoader = patchConfig.GeodataLoader
|
||||
targetConfig.Profile.StoreSelected = false
|
||||
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
||||
|
||||
@@ -8,12 +8,11 @@ import (
|
||||
)
|
||||
|
||||
type ConfigExtendedParams struct {
|
||||
IsPatch bool `json:"is-patch"`
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
|
||||
IsPatch bool `json:"is-patch"`
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
}
|
||||
|
||||
type GenerateConfigParams struct {
|
||||
@@ -80,6 +79,7 @@ const (
|
||||
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
|
||||
getRunTimeMethod Method = "getRunTime"
|
||||
getCurrentProfileNameMethod Method = "getCurrentProfileName"
|
||||
getProfileMethod Method = "getProfile"
|
||||
)
|
||||
|
||||
type Method string
|
||||
|
||||
41
core/go.mod
41
core/go.mod
@@ -6,7 +6,7 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
require (
|
||||
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/samber/lo v1.49.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -19,28 +19,28 @@ require (
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/ebitengine/purego v0.8.1 // indirect
|
||||
github.com/enfein/mieru/v3 v3.10.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/enfein/mieru/v3 v3.11.2 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // 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-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // 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/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // 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/klauspost/compress v1.17.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/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // 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/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/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-shadowsocks2 v0.2.2 // 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/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
|
||||
github.com/metacubex/utls v1.6.6 // 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/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // 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/pierrec/lz4/v4 v4.1.14 // 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/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
github.com/sagernet/fswatch v0.1.1 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||
github.com/sagernet/sing v0.5.1 // indirect
|
||||
github.com/sagernet/sing v0.5.2 // indirect
|
||||
github.com/sagernet/sing-mux v0.2.1 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 // 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/rabaead v0.0.0-20220730151906-ab6e06b96e8c // 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
|
||||
go.uber.org/mock v0.4.0 // 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/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
|
||||
88
core/go.sum
88
core/go.sum
@@ -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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ=
|
||||
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo=
|
||||
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/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
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/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
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.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
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/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
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/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
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.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
|
||||
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.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
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/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/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
|
||||
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/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
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/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
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/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
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/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/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc=
|
||||
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
|
||||
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/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/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.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA=
|
||||
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.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/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
|
||||
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/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/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/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.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
|
||||
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/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
||||
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/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/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
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/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/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
|
||||
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/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/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||
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/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
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/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
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.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
|
||||
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/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/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/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
|
||||
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
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/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
|
||||
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=
|
||||
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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
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/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
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/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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
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-20190412213103-97732733099d/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.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
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/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
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=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
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 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
25
core/hub.go
25
core/hub.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"core/state"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
@@ -26,10 +27,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{
|
||||
OnlyStatisticsProxy: false,
|
||||
}
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{}
|
||||
externalProviders = map[string]cp.Provider{}
|
||||
logSubscriber observable.Subscription[log.Event]
|
||||
currentConfig *config.Config
|
||||
@@ -152,7 +151,7 @@ func handleChangeProxy(data string, fn func(string string)) {
|
||||
}
|
||||
|
||||
func handleGetTraffic() string {
|
||||
up, down := statistic.DefaultManager.Current(configParams.OnlyStatisticsProxy)
|
||||
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -166,7 +165,7 @@ func handleGetTraffic() string {
|
||||
}
|
||||
|
||||
func handleGetTotalTraffic() string {
|
||||
up, down := statistic.DefaultManager.Total(configParams.OnlyStatisticsProxy)
|
||||
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -179,6 +178,15 @@ func handleGetTotalTraffic() string {
|
||||
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() {
|
||||
statistic.DefaultManager.ResetStatistic()
|
||||
}
|
||||
@@ -220,6 +228,7 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
||||
if params.TestUrl != "" {
|
||||
testUrl = params.TestUrl
|
||||
}
|
||||
delayData.Url = testUrl
|
||||
|
||||
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
|
||||
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() {
|
||||
adapter.UrlTestHook = func(url string, name string, delay uint16) {
|
||||
delayData := &Delay{
|
||||
|
||||
@@ -49,8 +49,8 @@ func invokeAction(paramsChar *C.char, port C.longlong) {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
go handleAction(action, func(bytes []byte) {
|
||||
bridge.SendToPort(i, string(bytes))
|
||||
go handleAction(action, func(data interface{}) {
|
||||
bridge.SendToPort(i, string(action.getResult(data)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func sendMessage(message Message) {
|
||||
}
|
||||
bridge.SendToPort(messagePort, string(Action{
|
||||
Method: messageMethod,
|
||||
}.wrapMessage(res)))
|
||||
}.getResult(res)))
|
||||
}
|
||||
|
||||
//export startListener
|
||||
|
||||
@@ -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) {
|
||||
m.invokeMap.Store(id, value)
|
||||
m.chanLock.Lock()
|
||||
@@ -74,7 +62,7 @@ func (m *InvokeManager) completer(id string, value string) {
|
||||
m.chanLock.Unlock()
|
||||
}
|
||||
|
||||
func (m *InvokeManager) await(id string) {
|
||||
func (m *InvokeManager) await(id string) string {
|
||||
m.chanLock.Lock()
|
||||
if _, ok := m.chanMap[id]; !ok {
|
||||
m.chanMap[id] = make(chan struct{})
|
||||
@@ -85,12 +73,17 @@ func (m *InvokeManager) await(id string) {
|
||||
timeout := time.After(500 * time.Millisecond)
|
||||
select {
|
||||
case <-ch:
|
||||
return
|
||||
res, ok := m.invokeMap.Load(id)
|
||||
m.invokeMap.Delete(id)
|
||||
if ok {
|
||||
return res.(string)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
case <-timeout:
|
||||
m.completer(id, "")
|
||||
return
|
||||
return ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -195,7 +188,6 @@ func initSocketHook() {
|
||||
})
|
||||
|
||||
fdInvokeMap.await(id)
|
||||
fdInvokeMap.delete(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -214,10 +206,7 @@ func init() {
|
||||
Id: id,
|
||||
Metadata: metadata,
|
||||
})
|
||||
processInvokeMap.await(id)
|
||||
res := processInvokeMap.load(id)
|
||||
processInvokeMap.delete(id)
|
||||
return res, nil
|
||||
return processInvokeMap.await(id), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,14 +214,14 @@ func handleGetAndroidVpnOptions() string {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
options := state.AndroidVpnOptions{
|
||||
Enable: state.CurrentState.Enable,
|
||||
Enable: state.CurrentState.VpnProps.Enable,
|
||||
Port: currentConfig.General.MixedPort,
|
||||
Ipv4Address: state.DefaultIpv4Address,
|
||||
Ipv6Address: state.GetIpv6Address(),
|
||||
AccessControl: state.CurrentState.AccessControl,
|
||||
SystemProxy: state.CurrentState.SystemProxy,
|
||||
AllowBypass: state.CurrentState.AllowBypass,
|
||||
RouteAddress: state.CurrentState.RouteAddress,
|
||||
AccessControl: state.CurrentState.VpnProps.AccessControl,
|
||||
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
|
||||
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
|
||||
RouteAddress: currentConfig.General.Tun.RouteAddress,
|
||||
BypassDomain: state.CurrentState.BypassDomain,
|
||||
DnsServerAddress: state.GetDnsServerAddress(),
|
||||
}
|
||||
@@ -244,10 +233,6 @@ func handleGetAndroidVpnOptions() string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleSetState(params string) {
|
||||
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
||||
}
|
||||
|
||||
func handleUpdateDns(value string) {
|
||||
go func() {
|
||||
log.Infoln("[DNS] updateDns %s", value)
|
||||
@@ -263,46 +248,41 @@ func handleGetCurrentProfileName() string {
|
||||
return state.CurrentState.CurrentProfileName
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
switch action.Method {
|
||||
case startTunMethod:
|
||||
data := action.Data.(string)
|
||||
var fd int
|
||||
_ = json.Unmarshal([]byte(data), &fd)
|
||||
send(action.wrapMessage(handleStartTun(fd)))
|
||||
result(handleStartTun(fd))
|
||||
return true
|
||||
case stopTunMethod:
|
||||
handleStopTun()
|
||||
send(action.wrapMessage(true))
|
||||
return true
|
||||
case setStateMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetState(data)
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return true
|
||||
case getAndroidVpnOptionsMethod:
|
||||
send(action.wrapMessage(handleGetAndroidVpnOptions()))
|
||||
result(handleGetAndroidVpnOptions())
|
||||
return true
|
||||
case updateDnsMethod:
|
||||
data := action.Data.(string)
|
||||
handleUpdateDns(data)
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return true
|
||||
case setFdMapMethod:
|
||||
fdId := action.Data.(string)
|
||||
handleSetFdMap(fdId)
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return true
|
||||
case setProcessMapMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetProcessMap(data)
|
||||
send(action.wrapMessage(true))
|
||||
result(true)
|
||||
return true
|
||||
case getRunTimeMethod:
|
||||
send(action.wrapMessage(handleGetRunTime()))
|
||||
result(handleGetRunTime())
|
||||
return true
|
||||
case getCurrentProfileNameMethod:
|
||||
send(action.wrapMessage(handleGetCurrentProfileName()))
|
||||
result(handleGetCurrentProfileName())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
package main
|
||||
|
||||
func nextHandle(action *Action) {
|
||||
return action
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func sendMessage(message Message) {
|
||||
}
|
||||
send(Action{
|
||||
Method: messageMethod,
|
||||
}.wrapMessage(res))
|
||||
}.getResult(res))
|
||||
}
|
||||
|
||||
func send(data []byte) {
|
||||
@@ -61,12 +61,12 @@ func startServer(arg string) {
|
||||
return
|
||||
}
|
||||
|
||||
go handleAction(action, func(bytes []byte) {
|
||||
send(bytes)
|
||||
go handleAction(action, func(data interface{}) {
|
||||
send(action.getResult(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, send func([]byte)) bool {
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//go:build android && cgo
|
||||
|
||||
package state
|
||||
|
||||
import "net/netip"
|
||||
|
||||
var DefaultIpv4Address = "172.19.0.1/30"
|
||||
var DefaultDnsAddress = "172.19.0.2"
|
||||
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
|
||||
@@ -13,13 +13,14 @@ type AndroidVpnOptions struct {
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
BypassDomain []string `json:"bypassDomain"`
|
||||
RouteAddress []string `json:"routeAddress"`
|
||||
RouteAddress []netip.Prefix `json:"routeAddress"`
|
||||
Ipv4Address string `json:"ipv4Address"`
|
||||
Ipv6Address string `json:"ipv6Address"`
|
||||
DnsServerAddress string `json:"dnsServerAddress"`
|
||||
}
|
||||
|
||||
type AccessControl struct {
|
||||
Enable bool `json:"enable"`
|
||||
Mode string `json:"mode"`
|
||||
AcceptList []string `json:"acceptList"`
|
||||
RejectList []string `json:"rejectList"`
|
||||
@@ -31,20 +32,23 @@ type AndroidVpnRawOptions struct {
|
||||
AccessControl *AccessControl `json:"accessControl"`
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
RouteAddress []string `json:"routeAddress"`
|
||||
Ipv6 bool `json:"ipv6"`
|
||||
BypassDomain []string `json:"bypassDomain"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
AndroidVpnRawOptions
|
||||
CurrentProfileName string `json:"currentProfileName"`
|
||||
VpnProps AndroidVpnRawOptions `json:"vpn-props"`
|
||||
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 {
|
||||
if CurrentState.Ipv6 {
|
||||
if CurrentState.VpnProps.Ipv6 {
|
||||
return DefaultIpv6Address
|
||||
} else {
|
||||
return ""
|
||||
|
||||
@@ -33,7 +33,7 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
|
||||
}
|
||||
prefix4 = append(prefix4, tempPrefix4)
|
||||
var prefix6 []netip.Prefix
|
||||
if state.CurrentState.Ipv6 {
|
||||
if state.CurrentState.VpnProps.Ipv6 {
|
||||
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
|
||||
if err != nil {
|
||||
log.Errorln("startTUN error:", err)
|
||||
|
||||
@@ -7,57 +7,27 @@ import 'package:fl_clash/l10n/l10n.dart';
|
||||
import 'package:fl_clash/manager/hotkey_manager.dart';
|
||||
import 'package:fl_clash/manager/manager.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
import 'models/models.dart';
|
||||
import 'pages/pages.dart';
|
||||
|
||||
runAppWithPreferences(
|
||||
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 {
|
||||
class Application extends ConsumerStatefulWidget {
|
||||
const Application({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Application> createState() => ApplicationState();
|
||||
ConsumerState<Application> createState() => ApplicationState();
|
||||
}
|
||||
|
||||
class ApplicationState extends State<Application> {
|
||||
late SystemColorSchemes systemColorSchemes;
|
||||
class ApplicationState extends ConsumerState<Application> {
|
||||
late ColorSchemes systemColorSchemes;
|
||||
Timer? _autoUpdateGroupTaskTimer;
|
||||
Timer? _autoUpdateProfilesTaskTimer;
|
||||
|
||||
@@ -73,7 +43,7 @@ class ApplicationState extends State<Application> {
|
||||
ColorScheme _getAppColorScheme({
|
||||
required Brightness brightness,
|
||||
int? primaryColor,
|
||||
required SystemColorSchemes systemColorSchemes,
|
||||
required ColorSchemes systemColorSchemes,
|
||||
}) {
|
||||
if (primaryColor != null) {
|
||||
return ColorScheme.fromSeed(
|
||||
@@ -81,7 +51,7 @@ class ApplicationState extends State<Application> {
|
||||
brightness: brightness,
|
||||
);
|
||||
} else {
|
||||
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness);
|
||||
return systemColorSchemes.getColorSchemeForBrightness(brightness);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +60,21 @@ class ApplicationState extends State<Application> {
|
||||
super.initState();
|
||||
_autoUpdateGroupTask();
|
||||
_autoUpdateProfilesTask();
|
||||
globalState.appController = AppController(context);
|
||||
globalState.appController = AppController(context, ref);
|
||||
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 {
|
||||
final currentContext = globalState.navigatorKey.currentContext;
|
||||
if (currentContext != null) {
|
||||
globalState.appController = AppController(currentContext);
|
||||
globalState.appController = AppController(currentContext, ref);
|
||||
}
|
||||
await globalState.appController.init();
|
||||
globalState.appController.initLink();
|
||||
@@ -167,7 +146,7 @@ class ApplicationState extends State<Application> {
|
||||
ColorScheme? lightDynamic,
|
||||
ColorScheme? darkDynamic,
|
||||
) {
|
||||
systemColorSchemes = SystemColorSchemes(
|
||||
systemColorSchemes = ColorSchemes(
|
||||
lightColorScheme: lightDynamic,
|
||||
darkColorScheme: darkDynamic,
|
||||
);
|
||||
@@ -180,15 +159,11 @@ class ApplicationState extends State<Application> {
|
||||
Widget build(context) {
|
||||
return _buildPlatformWrap(
|
||||
_buildWrap(
|
||||
Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.appSetting.locale,
|
||||
themeMode: config.themeProps.themeMode,
|
||||
primaryColor: config.themeProps.primaryColor,
|
||||
prueBlack: config.themeProps.prueBlack,
|
||||
fontFamily: config.themeProps.fontFamily,
|
||||
),
|
||||
builder: (_, state, child) {
|
||||
Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final locale =
|
||||
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||
final themeProps = ref.watch(themeSettingProvider);
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||
@@ -204,11 +179,9 @@ class ApplicationState extends State<Application> {
|
||||
return MessageManager(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, container) {
|
||||
final appController = globalState.appController;
|
||||
final maxWidth = container.maxWidth;
|
||||
if (appController.appState.viewWidth != maxWidth) {
|
||||
globalState.appController.updateViewWidth(maxWidth);
|
||||
}
|
||||
globalState.appController.updateViewWidth(
|
||||
container.maxWidth,
|
||||
);
|
||||
return _buildPage(child!);
|
||||
},
|
||||
),
|
||||
@@ -216,28 +189,28 @@ class ApplicationState extends State<Application> {
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: other.getLocaleForString(state.locale),
|
||||
locale: other.getLocaleForString(locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: state.themeMode,
|
||||
themeMode: themeProps.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
fontFamily: themeProps.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: state.fontFamily.value,
|
||||
fontFamily: themeProps.fontFamily.value,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
).toPrueBlack(state.prueBlack),
|
||||
primaryColor: themeProps.primaryColor,
|
||||
).toPrueBlack(themeProps.prueBlack),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
|
||||
@@ -63,15 +63,16 @@ class ClashCore {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> init({
|
||||
required ClashConfig clashConfig,
|
||||
required Config config,
|
||||
}) async {
|
||||
Future<bool> init() async {
|
||||
await initGeo();
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
return await clashInterface.init(homeDirPath);
|
||||
}
|
||||
|
||||
Future<bool> setState(CoreState state) async {
|
||||
return await clashInterface.setState(state);
|
||||
}
|
||||
|
||||
shutdown() async {
|
||||
await clashInterface.shutdown();
|
||||
}
|
||||
@@ -234,6 +235,14 @@ class ClashCore {
|
||||
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() {
|
||||
clashInterface.resetTraffic();
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/clash/message.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/common/future.dart';
|
||||
import 'package:fl_clash/common/other.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/material.dart' hide Action;
|
||||
|
||||
mixin ClashInterface {
|
||||
Future<bool> init(String homeDir);
|
||||
@@ -66,6 +63,10 @@ mixin ClashInterface {
|
||||
FutureOr<bool> closeConnection(String id);
|
||||
|
||||
FutureOr<bool> closeConnections();
|
||||
|
||||
FutureOr<String> getProfile(String id);
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
}
|
||||
|
||||
mixin AndroidClashInterface {
|
||||
@@ -73,8 +74,6 @@ mixin AndroidClashInterface {
|
||||
|
||||
Future<bool> setProcessMap(ProcessMapItem item);
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
|
||||
Future<bool> stopTun();
|
||||
|
||||
Future<bool> updateDns(String value);
|
||||
@@ -106,6 +105,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
case ActionMethod.closeConnections:
|
||||
case ActionMethod.closeConnection:
|
||||
case ActionMethod.stopListener:
|
||||
case ActionMethod.setState:
|
||||
completer?.complete(result.data as bool);
|
||||
return;
|
||||
case ActionMethod.changeProxy:
|
||||
@@ -137,7 +137,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
completer?.complete(result.data);
|
||||
}
|
||||
} 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
|
||||
shutdown() async {
|
||||
return await invoke<bool>(
|
||||
@@ -232,6 +240,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
return await invoke<String>(
|
||||
method: ActionMethod.updateConfig,
|
||||
data: json.encode(updateConfigParams),
|
||||
timeout: Duration(minutes: 2),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -239,6 +248,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
||||
Future<String> getProxies() {
|
||||
return invoke<String>(
|
||||
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
|
||||
FutureOr<String> getTotalTraffic() {
|
||||
return invoke<String>(
|
||||
|
||||
@@ -67,7 +67,6 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
switch (result.method) {
|
||||
case ActionMethod.setFdMap:
|
||||
case ActionMethod.setProcessMap:
|
||||
case ActionMethod.setState:
|
||||
case ActionMethod.stopTun:
|
||||
case ActionMethod.updateDns:
|
||||
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
|
||||
Future<DateTime?> startTun(int fd) async {
|
||||
final res = await invoke<String>(
|
||||
@@ -259,7 +250,7 @@ class ClashLibHandler {
|
||||
|
||||
setProcessMap(ProcessMapItem processMapItem) {
|
||||
final processMapItemChar =
|
||||
json.encode(processMapItem).toNativeUtf8().cast<Char>();
|
||||
json.encode(processMapItem).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setProcessMap(processMapItemChar);
|
||||
malloc.free(processMapItemChar);
|
||||
}
|
||||
@@ -321,10 +312,10 @@ class ClashLibHandler {
|
||||
}
|
||||
|
||||
Future<String> quickStart(
|
||||
String homeDir,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
CoreState state,
|
||||
) {
|
||||
String homeDir,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
CoreState state,
|
||||
) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'dart:typed_data';
|
||||
import 'package:fl_clash/clash/interface.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/core.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
class ClashService extends ClashHandlerInterface {
|
||||
static ClashService? _instance;
|
||||
@@ -14,6 +15,8 @@ class ClashService extends ClashHandlerInterface {
|
||||
|
||||
Completer<Socket> socketCompleter = Completer();
|
||||
|
||||
bool isStarting = false;
|
||||
|
||||
Process? process;
|
||||
|
||||
factory ClashService() {
|
||||
@@ -27,48 +30,61 @@ class ClashService extends ClashHandlerInterface {
|
||||
}
|
||||
|
||||
_initServer() async {
|
||||
final address = !Platform.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
type: InternetAddressType.unix,
|
||||
)
|
||||
: InternetAddress(
|
||||
localhost,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
await _deleteSocketFile();
|
||||
final server = await ServerSocket.bind(
|
||||
address,
|
||||
0,
|
||||
shared: true,
|
||||
);
|
||||
serverCompleter.complete(server);
|
||||
await for (final socket in server) {
|
||||
await _destroySocket();
|
||||
socketCompleter.complete(socket);
|
||||
socket
|
||||
.transform(
|
||||
StreamTransformer<Uint8List, String>.fromHandlers(
|
||||
handleData: (Uint8List data, EventSink<String> sink) {
|
||||
sink.add(utf8.decode(data, allowMalformed: true));
|
||||
runZonedGuarded(() async {
|
||||
final address = !Platform.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
type: InternetAddressType.unix,
|
||||
)
|
||||
: InternetAddress(
|
||||
localhost,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
await _deleteSocketFile();
|
||||
final server = await ServerSocket.bind(
|
||||
address,
|
||||
0,
|
||||
shared: true,
|
||||
);
|
||||
serverCompleter.complete(server);
|
||||
await for (final socket in server) {
|
||||
await _destroySocket();
|
||||
socketCompleter.complete(socket);
|
||||
socket
|
||||
.transform(
|
||||
StreamTransformer<Uint8List, String>.fromHandlers(
|
||||
handleData: (Uint8List data, EventSink<String> sink) {
|
||||
sink.add(utf8.decode(data, allowMalformed: true));
|
||||
},
|
||||
),
|
||||
)
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}, (error, stack) {
|
||||
commonPrint.log(error.toString());
|
||||
if(error is SocketException){
|
||||
globalState.showNotifier(error.toString());
|
||||
globalState.appController.restartCore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
reStart() async {
|
||||
if (isStarting == true) {
|
||||
return;
|
||||
}
|
||||
isStarting = true;
|
||||
socketCompleter = Completer();
|
||||
if (process != null) {
|
||||
await shutdown();
|
||||
}
|
||||
@@ -90,6 +106,7 @@ class ClashService extends ClashHandlerInterface {
|
||||
],
|
||||
);
|
||||
process!.stdout.listen((_) {});
|
||||
isStarting = false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -125,7 +142,6 @@ class ClashService extends ClashHandlerInterface {
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
await super.shutdown();
|
||||
if (Platform.isWindows) {
|
||||
await request.stopCoreByHelper();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ extension ColorExtension on Color {
|
||||
}
|
||||
|
||||
Color get toSoft {
|
||||
return withOpacity(0.12);
|
||||
return withOpacity(0.15);
|
||||
}
|
||||
|
||||
Color get toLittle {
|
||||
|
||||
@@ -35,4 +35,5 @@ export 'tray.dart';
|
||||
export 'window.dart';
|
||||
export 'windows.dart';
|
||||
export 'render.dart';
|
||||
export 'view.dart';
|
||||
export 'mixin.dart';
|
||||
export 'print.dart';
|
||||
@@ -11,6 +11,8 @@ import 'package:flutter/material.dart';
|
||||
const appName = "FlClash";
|
||||
const appHelperService = "FlClashHelperService";
|
||||
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";
|
||||
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
|
||||
const helperPort = 47890;
|
||||
@@ -33,16 +35,6 @@ final double kHeaderHeight = system.isDesktop
|
||||
? 40
|
||||
: 28
|
||||
: 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 localhost = "127.0.0.1";
|
||||
const clashConfigKey = "clash_config";
|
||||
@@ -53,8 +45,6 @@ const repository = "chen08209/FlClash";
|
||||
const defaultExternalController = "127.0.0.1:9090";
|
||||
const maxMobileWidth = 600;
|
||||
const maxLaptopWidth = 840;
|
||||
const geodataLoaderMemconservative = "memconservative";
|
||||
const geodataLoaderStandard = "standard";
|
||||
const defaultTestUrl = "https://www.gstatic.com/generate_204";
|
||||
final filter = ImageFilter.blur(
|
||||
sigmaX: 5,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ extension CompleterExt<T> on Completer<T> {
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
required String functionName,
|
||||
}) {
|
||||
final realTimeout = timeout ?? const Duration(seconds: 1);
|
||||
final realTimeout = timeout ?? const Duration(seconds: 30);
|
||||
Timer(realTimeout + commonDuration, () {
|
||||
if (onLast != null) {
|
||||
onLast();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
|
||||
import '../state.dart';
|
||||
import 'constant.dart';
|
||||
|
||||
class FlClashHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
@@ -14,10 +13,9 @@ class FlClashHttpOverrides extends HttpOverrides {
|
||||
if ([localhost].contains(url.host)) {
|
||||
return "DIRECT";
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
final port = appController.clashConfig.mixedPort;
|
||||
final isStart = appController.appFlowingState.isStart;
|
||||
debugPrint("find $url proxy:$isStart");
|
||||
final port = globalState.config.patchClashConfig.mixedPort;
|
||||
final isStart = globalState.appState.runTime != null;
|
||||
commonPrint.log("find $url proxy:$isStart");
|
||||
if (!isStart) return "DIRECT";
|
||||
return "PROXY localhost:$port";
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'print.dart';
|
||||
|
||||
typedef InstallConfigCallBack = void Function(String url);
|
||||
|
||||
@@ -15,11 +16,11 @@ class LinkManager {
|
||||
}
|
||||
|
||||
initAppLinksListen(installConfigCallBack) async {
|
||||
debugPrint("initAppLinksListen");
|
||||
commonPrint.log("initAppLinksListen");
|
||||
destroy();
|
||||
subscription = _appLinks.uriLinkStream.listen(
|
||||
(uri) {
|
||||
debugPrint('onAppLink: $uri');
|
||||
commonPrint.log('onAppLink: $uri');
|
||||
if (uri.host == 'install-config') {
|
||||
final parameters = uri.queryParameters;
|
||||
final url = parameters['url'];
|
||||
|
||||
@@ -2,9 +2,9 @@ import 'dart:collection';
|
||||
|
||||
class FixedList<T> {
|
||||
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) {
|
||||
if (_list.length == maxLength) {
|
||||
@@ -13,11 +13,22 @@ class FixedList<T> {
|
||||
_list.add(item);
|
||||
}
|
||||
|
||||
clear() {
|
||||
_list.clear();
|
||||
}
|
||||
|
||||
List<T> get list => List.unmodifiable(_list);
|
||||
|
||||
int get length => _list.length;
|
||||
|
||||
T operator [](int index) => _list[index];
|
||||
|
||||
FixedList<T> copyWith() {
|
||||
return FixedList(
|
||||
maxLength,
|
||||
list: _list,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FixedMap<K, V> {
|
||||
@@ -36,7 +47,7 @@ class FixedMap<K, V> {
|
||||
_queue.add(key);
|
||||
}
|
||||
|
||||
clear(){
|
||||
clear() {
|
||||
_map.clear();
|
||||
_queue.clear();
|
||||
}
|
||||
|
||||
@@ -4,19 +4,26 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class Measure {
|
||||
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(
|
||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
||||
);
|
||||
),
|
||||
_fontFamily = fontFamily ?? "";
|
||||
|
||||
Size computeTextSize(
|
||||
Text text, {
|
||||
double maxWidth = double.infinity,
|
||||
}) {
|
||||
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,
|
||||
textScaler: _textScale,
|
||||
textDirection: text.textDirection ?? TextDirection.ltr,
|
||||
|
||||
46
lib/common/mixin.dart
Normal file
46
lib/common/mixin.dart
Normal 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;
|
||||
}
|
||||
@@ -6,55 +6,81 @@ import 'package:flutter/material.dart';
|
||||
class Navigation {
|
||||
static Navigation? _instance;
|
||||
|
||||
getItems({
|
||||
List<NavigationItem> getItems({
|
||||
bool openLogs = false,
|
||||
bool hasProxies = false,
|
||||
}) {
|
||||
return [
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.space_dashboard),
|
||||
label: "dashboard",
|
||||
fragment: DashboardFragment(),
|
||||
label: PageLabel.dashboard,
|
||||
fragment: DashboardFragment(
|
||||
key: GlobalObjectKey(PageLabel.dashboard),
|
||||
),
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.rocket),
|
||||
label: "proxies",
|
||||
fragment: const ProxiesFragment(),
|
||||
icon: const Icon(Icons.article),
|
||||
label: PageLabel.proxies,
|
||||
fragment: const ProxiesFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.proxies,
|
||||
),
|
||||
),
|
||||
modes: hasProxies
|
||||
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
|
||||
: [],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.folder),
|
||||
label: "profiles",
|
||||
fragment: ProfilesFragment(),
|
||||
label: PageLabel.profiles,
|
||||
fragment: ProfilesFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.profiles,
|
||||
),
|
||||
),
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.view_timeline),
|
||||
label: "requests",
|
||||
fragment: RequestsFragment(),
|
||||
label: PageLabel.requests,
|
||||
fragment: RequestsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.requests,
|
||||
),
|
||||
),
|
||||
description: "requestsDesc",
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.ballot),
|
||||
label: "connections",
|
||||
fragment: ConnectionsFragment(),
|
||||
label: PageLabel.connections,
|
||||
fragment: ConnectionsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.connections,
|
||||
),
|
||||
),
|
||||
description: "connectionsDesc",
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.storage),
|
||||
label: "resources",
|
||||
label: PageLabel.resources,
|
||||
description: "resourcesDesc",
|
||||
keep: false,
|
||||
fragment: Resources(),
|
||||
fragment: Resources(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.resources,
|
||||
),
|
||||
),
|
||||
modes: [NavigationItemMode.more],
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.adb),
|
||||
label: "logs",
|
||||
fragment: const LogsFragment(),
|
||||
label: PageLabel.logs,
|
||||
fragment: const LogsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.logs,
|
||||
),
|
||||
),
|
||||
description: "logsDesc",
|
||||
modes: openLogs
|
||||
? [NavigationItemMode.desktop, NavigationItemMode.more]
|
||||
@@ -62,8 +88,12 @@ class Navigation {
|
||||
),
|
||||
const NavigationItem(
|
||||
icon: Icon(Icons.construction),
|
||||
label: "tools",
|
||||
fragment: ToolsFragment(),
|
||||
label: PageLabel.tools,
|
||||
fragment: ToolsFragment(
|
||||
key: GlobalObjectKey(
|
||||
PageLabel.tools,
|
||||
),
|
||||
),
|
||||
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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:flutter/material.dart';
|
||||
|
||||
class BaseNavigator {
|
||||
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>(
|
||||
CommonDesktopRoute(
|
||||
builder: (context) => child,
|
||||
@@ -68,7 +70,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
|
||||
Duration get transitionDuration => const Duration(milliseconds: 500);
|
||||
|
||||
@override
|
||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
|
||||
Duration get reverseTransitionDuration => const Duration(milliseconds: 250);
|
||||
}
|
||||
|
||||
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
|
||||
@@ -272,7 +274,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
|
||||
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 bandWidth = shadowWidth / (colors.length - 1);
|
||||
|
||||
|
||||
@@ -2,8 +2,15 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension NumExt on num {
|
||||
String fixed({digit = 2}) {
|
||||
return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
|
||||
String fixed({decimals = 2}) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,19 @@ import 'dart:convert';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
|
||||
import 'constant.dart';
|
||||
|
||||
class Preferences {
|
||||
static Preferences? _instance;
|
||||
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
|
||||
|
||||
Future<bool> get isInit async => await sharedPreferencesCompleter.future != null;
|
||||
Future<bool> get isInit async =>
|
||||
await sharedPreferencesCompleter.future != null;
|
||||
|
||||
Preferences._internal() {
|
||||
SharedPreferences.getInstance().then((value) => sharedPreferencesCompleter.complete(value))
|
||||
.onError((_,__)=>sharedPreferencesCompleter.complete(null));
|
||||
SharedPreferences.getInstance()
|
||||
.then((value) => sharedPreferencesCompleter.complete(value))
|
||||
.onError((_, __) => sharedPreferencesCompleter.complete(null));
|
||||
}
|
||||
|
||||
factory Preferences() {
|
||||
@@ -23,7 +24,6 @@ class Preferences {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
|
||||
Future<ClashConfig?> getClashConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final clashConfigString = preferences?.getString(clashConfigKey);
|
||||
@@ -32,29 +32,26 @@ class Preferences {
|
||||
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 {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
final configString = preferences?.getString(configKey);
|
||||
if (configString == null) return null;
|
||||
final configMap = json.decode(configString);
|
||||
return Config.fromJson(configMap);
|
||||
return Config.compatibleFromJson(configMap);
|
||||
}
|
||||
|
||||
Future<bool> saveConfig(Config config) async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
return await preferences?.setString(
|
||||
configKey,
|
||||
json.encode(config),
|
||||
) ?? false;
|
||||
configKey,
|
||||
json.encode(config),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
clearClashConfig() async {
|
||||
final preferences = await sharedPreferencesCompleter.future;
|
||||
preferences?.remove(clashConfigKey);
|
||||
}
|
||||
|
||||
clearPreferences() async {
|
||||
|
||||
31
lib/common/print.dart
Normal file
31
lib/common/print.dart
Normal 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();
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class Render {
|
||||
@@ -23,14 +23,14 @@ class Render {
|
||||
|
||||
pause() {
|
||||
debouncer.call(
|
||||
"render_pause",
|
||||
DebounceTag.renderPause,
|
||||
_pause,
|
||||
duration: Duration(seconds: 5),
|
||||
duration: Duration(seconds: 15),
|
||||
);
|
||||
}
|
||||
|
||||
resume() {
|
||||
debouncer.cancel("render_pause");
|
||||
debouncer.cancel(DebounceTag.renderPause);
|
||||
_resume();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class Render {
|
||||
_drawFrame = _dispatcher.onDrawFrame;
|
||||
_dispatcher.onBeginFrame = null;
|
||||
_dispatcher.onDrawFrame = null;
|
||||
debugPrint("[App] pause");
|
||||
commonPrint.log("pause");
|
||||
}
|
||||
|
||||
void _resume() {
|
||||
@@ -50,7 +50,7 @@ class Render {
|
||||
_dispatcher.onBeginFrame = _beginFrame;
|
||||
_dispatcher.onDrawFrame = _drawFrame;
|
||||
_dispatcher.scheduleFrame();
|
||||
debugPrint("[App] resume");
|
||||
commonPrint.log("resume");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
@@ -14,11 +13,10 @@ class Request {
|
||||
String? userAgent;
|
||||
|
||||
Request() {
|
||||
_dio = Dio();
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
return handler.next(options); // 继续请求
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
headers: {
|
||||
"User-Agent": browserUa,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -30,7 +28,7 @@ class Request {
|
||||
url,
|
||||
options: Options(
|
||||
headers: {
|
||||
"User-Agent": globalState.appController.clashConfig.globalUa
|
||||
"User-Agent": globalState.ua,
|
||||
},
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
@@ -71,31 +69,32 @@ class Request {
|
||||
return data;
|
||||
}
|
||||
|
||||
final List<String> _ipInfoSources = [
|
||||
"https://ipwho.is/?fields=ip&output=csv",
|
||||
"https://ipinfo.io/ip",
|
||||
"https://ifconfig.me/ip/",
|
||||
];
|
||||
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
|
||||
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
|
||||
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
|
||||
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
|
||||
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
|
||||
};
|
||||
|
||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||
for (final source in _ipInfoSources) {
|
||||
for (final source in _ipInfoSources.entries) {
|
||||
try {
|
||||
final response = await _dio
|
||||
.get<String>(
|
||||
source,
|
||||
cancelToken: cancelToken,
|
||||
)
|
||||
.timeout(httpTimeoutDuration);
|
||||
final response = await _dio.get<Map<String, dynamic>>(
|
||||
source.key,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
);
|
||||
if (response.statusCode != 200 || response.data == null) {
|
||||
continue;
|
||||
}
|
||||
final ipInfo = await clashCore.getCountryCode(response.data!);
|
||||
if (ipInfo == null && source != _ipInfoSources.last) {
|
||||
if (response.data == null) {
|
||||
continue;
|
||||
}
|
||||
return ipInfo;
|
||||
return source.value(response.data!);
|
||||
} catch (e) {
|
||||
debugPrint("checkIp error ===> $e");
|
||||
commonPrint.log("checkIp error ===> $e");
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||
throw "cancelled";
|
||||
}
|
||||
@@ -110,6 +109,9 @@ class Request {
|
||||
.get(
|
||||
"http://$localhost:$helperPort/ping",
|
||||
options: Options(
|
||||
headers: {
|
||||
"User-Agent": browserUa,
|
||||
},
|
||||
responseType: ResponseType.plain,
|
||||
),
|
||||
)
|
||||
|
||||
0
lib/common/state.dart
Normal file
0
lib/common/state.dart
Normal file
@@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'print.dart';
|
||||
|
||||
extension StringExtension on String {
|
||||
bool get isUrl {
|
||||
@@ -43,8 +43,17 @@ extension StringExtension on String {
|
||||
RegExp(this);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
commonPrint.log(e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StringExtensionSafe on String? {
|
||||
String getSafeValue(String defaultValue) {
|
||||
if (this == null || this!.isEmpty) {
|
||||
return defaultValue;
|
||||
}
|
||||
return this!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class System {
|
||||
} else if (Platform.isLinux) {
|
||||
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
|
||||
final output = result.stdout.trim();
|
||||
if (output.startsWith('root:') && output.contains('rwx')) {
|
||||
if (output.startsWith('root:') && output.contains('rws')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -39,10 +39,7 @@ class Tray {
|
||||
}
|
||||
|
||||
update({
|
||||
required AppState appState,
|
||||
required AppFlowingState appFlowingState,
|
||||
required Config config,
|
||||
required ClashConfig clashConfig,
|
||||
required TrayState trayState,
|
||||
bool focus = false,
|
||||
}) async {
|
||||
if (Platform.isAndroid) {
|
||||
@@ -50,7 +47,7 @@ class Tray {
|
||||
}
|
||||
if (!Platform.isLinux) {
|
||||
await _updateSystemTray(
|
||||
brightness: appState.brightness,
|
||||
brightness: trayState.brightness,
|
||||
force: focus,
|
||||
);
|
||||
}
|
||||
@@ -63,9 +60,7 @@ class Tray {
|
||||
);
|
||||
menuItems.add(showMenuItem);
|
||||
final startMenuItem = MenuItem.checkbox(
|
||||
label: appFlowingState.isStart
|
||||
? appLocalizations.stop
|
||||
: appLocalizations.start,
|
||||
label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
|
||||
onClick: (_) async {
|
||||
globalState.appController.updateStart();
|
||||
},
|
||||
@@ -80,23 +75,22 @@ class Tray {
|
||||
onClick: (_) {
|
||||
globalState.appController.changeMode(mode);
|
||||
},
|
||||
checked: mode == clashConfig.mode,
|
||||
checked: mode == trayState.mode,
|
||||
),
|
||||
);
|
||||
}
|
||||
menuItems.add(MenuItem.separator());
|
||||
if (!Platform.isWindows) {
|
||||
final groups = appState.currentGroups;
|
||||
for (final group in groups) {
|
||||
for (final group in trayState.groups) {
|
||||
List<MenuItem> subMenuItems = [];
|
||||
for (final proxy in group.all) {
|
||||
subMenuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: proxy.name,
|
||||
checked: appState.selectedMap[group.name] == proxy.name,
|
||||
checked: trayState.selectedMap[group.name] == proxy.name,
|
||||
onClick: (_) {
|
||||
final appController = globalState.appController;
|
||||
appController.config.updateCurrentSelectedMap(
|
||||
appController.updateCurrentSelectedMap(
|
||||
group.name,
|
||||
proxy.name,
|
||||
);
|
||||
@@ -117,18 +111,18 @@ class Tray {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (groups.isNotEmpty) {
|
||||
if (trayState.groups.isNotEmpty) {
|
||||
menuItems.add(MenuItem.separator());
|
||||
}
|
||||
}
|
||||
if (appFlowingState.isStart) {
|
||||
if (trayState.isStart) {
|
||||
menuItems.add(
|
||||
MenuItem.checkbox(
|
||||
label: appLocalizations.tun,
|
||||
onClick: (_) {
|
||||
globalState.appController.updateTun();
|
||||
},
|
||||
checked: clashConfig.tun.enable,
|
||||
checked: trayState.tunEnable,
|
||||
),
|
||||
);
|
||||
menuItems.add(
|
||||
@@ -137,7 +131,7 @@ class Tray {
|
||||
onClick: (_) {
|
||||
globalState.appController.updateSystemProxy();
|
||||
},
|
||||
checked: config.networkProps.systemProxy,
|
||||
checked: trayState.systemProxy,
|
||||
),
|
||||
);
|
||||
menuItems.add(MenuItem.separator());
|
||||
@@ -147,12 +141,12 @@ class Tray {
|
||||
onClick: (_) async {
|
||||
globalState.appController.updateAutoLaunch();
|
||||
},
|
||||
checked: config.appSetting.autoLaunch,
|
||||
checked: trayState.autoLaunch,
|
||||
);
|
||||
final copyEnvVarMenuItem = MenuItem(
|
||||
label: appLocalizations.copyEnvVar,
|
||||
onClick: (_) async {
|
||||
await _copyEnv(clashConfig.mixedPort);
|
||||
await _copyEnv(trayState.port);
|
||||
},
|
||||
);
|
||||
menuItems.add(autoStartMenuItem);
|
||||
@@ -169,12 +163,25 @@ class Tray {
|
||||
await trayManager.setContextMenu(menu);
|
||||
if (Platform.isLinux) {
|
||||
await _updateSystemTray(
|
||||
brightness: appState.brightness,
|
||||
brightness: trayState.brightness,
|
||||
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 {
|
||||
final url = "http://127.0.0.1:$port";
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
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:screen_retriever/screen_retriever.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class Window {
|
||||
init(WindowProps props, int version) async {
|
||||
init(int version) async {
|
||||
final props = globalState.config.windowProps;
|
||||
final acquire = await singleInstanceLock.acquire();
|
||||
if (!acquire) {
|
||||
exit(0);
|
||||
@@ -24,6 +25,8 @@ class Window {
|
||||
);
|
||||
if (!Platform.isMacOS || version > 10) {
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
}
|
||||
if(!Platform.isMacOS){
|
||||
final left = props.left ?? 0;
|
||||
final top = props.top ?? 0;
|
||||
final right = left + props.width;
|
||||
@@ -33,7 +36,7 @@ class Window {
|
||||
} else {
|
||||
final displays = await screenRetriever.getAllDisplays();
|
||||
final isPositionValid = displays.any(
|
||||
(display) {
|
||||
(display) {
|
||||
final displayBounds = Rect.fromLTWH(
|
||||
display.visiblePosition!.dx,
|
||||
display.visiblePosition!.dy,
|
||||
@@ -60,10 +63,10 @@ class Window {
|
||||
}
|
||||
|
||||
show() async {
|
||||
render?.resume();
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
render?.resume();
|
||||
}
|
||||
|
||||
Future<bool> isVisible() async {
|
||||
@@ -75,9 +78,9 @@ class Window {
|
||||
}
|
||||
|
||||
hide() async {
|
||||
render?.pause();
|
||||
await windowManager.hide();
|
||||
await windowManager.setSkipTaskbar(true);
|
||||
render?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class Windows {
|
||||
@@ -54,7 +53,7 @@ class Windows {
|
||||
calloc.free(argumentsPtr);
|
||||
calloc.free(operationPtr);
|
||||
|
||||
debugPrint("[Windows] runas: $command $arguments resultCode:$result");
|
||||
commonPrint.log("windows runas: $command $arguments resultCode:$result");
|
||||
|
||||
if (result < 42) {
|
||||
return false;
|
||||
|
||||
@@ -8,28 +8,24 @@ import 'package:archive/archive.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/archive.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'common/common.dart';
|
||||
import 'models/models.dart';
|
||||
|
||||
class AppController {
|
||||
final BuildContext context;
|
||||
late AppState appState;
|
||||
late AppFlowingState appFlowingState;
|
||||
late Config config;
|
||||
late ClashConfig clashConfig;
|
||||
bool lastTunEnable = false;
|
||||
int? lastProfileModified;
|
||||
|
||||
AppController(this.context) {
|
||||
appState = context.read<AppState>();
|
||||
config = context.read<Config>();
|
||||
clashConfig = context.read<ClashConfig>();
|
||||
appFlowingState = context.read<AppFlowingState>();
|
||||
}
|
||||
final BuildContext context;
|
||||
final WidgetRef _ref;
|
||||
|
||||
AppController(this.context, WidgetRef ref) : _ref = ref;
|
||||
|
||||
updateClashConfigDebounce() {
|
||||
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
|
||||
@@ -41,13 +37,13 @@ class AppController {
|
||||
|
||||
addCheckIpNumDebounce() {
|
||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
||||
appState.checkIpNum++;
|
||||
_ref.read(checkIpNumProvider.notifier).add();
|
||||
});
|
||||
}
|
||||
|
||||
applyProfileDebounce() {
|
||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
||||
applyProfile(isPrue: true);
|
||||
applyProfile();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,11 +63,12 @@ class AppController {
|
||||
}
|
||||
|
||||
restartCore() async {
|
||||
await globalState.restartCore(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
);
|
||||
await clashService?.reStart();
|
||||
await initCore();
|
||||
|
||||
if (_ref.read(runTimeProvider.notifier).isStart) {
|
||||
await globalState.handleStart();
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(bool isStart) async {
|
||||
@@ -81,13 +78,12 @@ class AppController {
|
||||
updateTraffic,
|
||||
]);
|
||||
final currentLastModified =
|
||||
await config.getCurrentProfile()?.profileLastModified;
|
||||
if (currentLastModified == null ||
|
||||
globalState.lastProfileModified == null) {
|
||||
await _ref.read(currentProfileProvider)?.profileLastModified;
|
||||
if (currentLastModified == null || lastProfileModified == null) {
|
||||
addCheckIpNumDebounce();
|
||||
return;
|
||||
}
|
||||
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) {
|
||||
if (currentLastModified <= (lastProfileModified ?? 0)) {
|
||||
addCheckIpNumDebounce();
|
||||
return;
|
||||
}
|
||||
@@ -95,9 +91,10 @@ class AppController {
|
||||
} else {
|
||||
await globalState.handleStop();
|
||||
await clashCore.resetTraffic();
|
||||
appFlowingState.traffics = [];
|
||||
appFlowingState.totalTraffic = Traffic();
|
||||
appFlowingState.runTime = null;
|
||||
_ref.read(trafficsProvider.notifier).clear();
|
||||
_ref.read(totalTrafficProvider.notifier).value = Traffic();
|
||||
_ref.read(runTimeProvider.notifier).value = null;
|
||||
// tray.updateTrayTitle(null);
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
}
|
||||
@@ -107,107 +104,214 @@ class AppController {
|
||||
if (startTime != null) {
|
||||
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
||||
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
||||
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
|
||||
_ref.read(runTimeProvider.notifier).value = nowTimeStamp - startTimeStamp;
|
||||
} else {
|
||||
appFlowingState.runTime = null;
|
||||
_ref.read(runTimeProvider.notifier).value = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateTraffic() {
|
||||
globalState.updateTraffic(
|
||||
config: config,
|
||||
appFlowingState: appFlowingState,
|
||||
);
|
||||
updateTraffic() async {
|
||||
final traffic = await clashCore.getTraffic();
|
||||
_ref.read(trafficsProvider.notifier).addTraffic(traffic);
|
||||
_ref.read(totalTrafficProvider.notifier).value =
|
||||
await clashCore.getTotalTraffic();
|
||||
}
|
||||
|
||||
addProfile(Profile profile) async {
|
||||
config.setProfile(profile);
|
||||
if (config.currentProfileId != null) return;
|
||||
await changeProfile(profile.id);
|
||||
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||
if (_ref.read(currentProfileIdProvider) != null) return;
|
||||
_ref.read(currentProfileIdProvider.notifier).value = profile.id;
|
||||
}
|
||||
|
||||
deleteProfile(String id) async {
|
||||
config.deleteProfileById(id);
|
||||
_ref.read(profilesProvider.notifier).deleteProfileById(id);
|
||||
clearEffect(id);
|
||||
if (config.currentProfileId == id) {
|
||||
if (config.profiles.isNotEmpty) {
|
||||
final updateId = config.profiles.first.id;
|
||||
changeProfile(updateId);
|
||||
if (globalState.config.currentProfileId == id) {
|
||||
final profiles = globalState.config.profiles;
|
||||
final currentProfileId = _ref.read(currentProfileIdProvider.notifier);
|
||||
if (profiles.isNotEmpty) {
|
||||
final updateId = profiles.first.id;
|
||||
currentProfileId.value = updateId;
|
||||
} else {
|
||||
changeProfile(null);
|
||||
currentProfileId.value = null;
|
||||
updateStatus(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateProviders() async {
|
||||
await globalState.updateProviders(appState);
|
||||
_ref.read(providersProvider.notifier).value =
|
||||
await clashCore.getExternalProviders();
|
||||
}
|
||||
|
||||
updateLocalIp() async {
|
||||
appFlowingState.localIp = null;
|
||||
_ref.read(localIpProvider.notifier).value = null;
|
||||
await Future.delayed(commonDuration);
|
||||
appFlowingState.localIp = await other.getLocalIpAddress();
|
||||
_ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress();
|
||||
}
|
||||
|
||||
Future<void> updateProfile(Profile profile) async {
|
||||
final newProfile = await profile.update();
|
||||
config.setProfile(
|
||||
newProfile.copyWith(isUpdating: false),
|
||||
);
|
||||
if (profile.id == config.currentProfile?.id) {
|
||||
_ref
|
||||
.read(profilesProvider.notifier)
|
||||
.setProfile(newProfile.copyWith(isUpdating: false));
|
||||
if (profile.id == _ref.read(currentProfileIdProvider)) {
|
||||
applyProfileDebounce();
|
||||
}
|
||||
}
|
||||
|
||||
_setProfile(Profile profile) {
|
||||
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||
}
|
||||
|
||||
setProfile(Profile profile) {
|
||||
config.setProfile(profile);
|
||||
if (profile.id == config.currentProfile?.id) {
|
||||
_setProfile(profile);
|
||||
if (profile.id == _ref.read(currentProfileIdProvider)) {
|
||||
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;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
await commonScaffoldState?.loadingRun(() async {
|
||||
await globalState.updateClashConfig(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
isPatch: isPatch,
|
||||
await _updateClashConfig(
|
||||
isPatch,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future applyProfile({bool isPrue = false}) async {
|
||||
if (isPrue) {
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
Future<void> _updateClashConfig([bool? isPatch]) async {
|
||||
final profile = _ref.watch(currentProfileProvider);
|
||||
await _ref.read(currentProfileProvider)?.checkAndUpdate();
|
||||
final patchConfig = _ref.read(patchClashConfigProvider);
|
||||
final appSetting = _ref.read(appSettingProvider);
|
||||
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 {
|
||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||
if (commonScaffoldState?.mounted != true) return;
|
||||
await commonScaffoldState?.loadingRun(() async {
|
||||
await globalState.applyProfile(
|
||||
appState: appState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
await _applyProfile();
|
||||
});
|
||||
}
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
changeProfile(String? value) async {
|
||||
if (value == config.currentProfileId) return;
|
||||
config.currentProfileId = value;
|
||||
handleChangeProfile() {
|
||||
_ref.read(delayDataSourceProvider.notifier).value = {};
|
||||
applyProfile();
|
||||
}
|
||||
|
||||
updateBrightness(Brightness brightness) {
|
||||
_ref.read(appBrightnessProvider.notifier).value = brightness;
|
||||
}
|
||||
|
||||
autoUpdateProfiles() async {
|
||||
for (final profile in config.profiles) {
|
||||
for (final profile in _ref.read(profilesProvider)) {
|
||||
if (!profile.autoUpdate) continue;
|
||||
final isNotNeedUpdate = profile.lastUpdateDate
|
||||
?.add(
|
||||
@@ -218,20 +322,29 @@ class AppController {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
updateProfile(profile);
|
||||
await updateProfile(profile);
|
||||
} catch (e) {
|
||||
appFlowingState.addLog(
|
||||
Log(
|
||||
logLevel: LogLevel.info,
|
||||
payload: e.toString(),
|
||||
),
|
||||
);
|
||||
_ref.read(logsProvider.notifier).addLog(
|
||||
Log(
|
||||
logLevel: LogLevel.info,
|
||||
payload: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateGroups() async {
|
||||
_ref.read(groupsProvider.notifier).value = await retry(
|
||||
task: () async {
|
||||
return await clashCore.getProxiesGroups();
|
||||
},
|
||||
retryIf: (res) => res.isEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
updateProfiles() async {
|
||||
for (final profile in config.profiles) {
|
||||
for (final profile in _ref.read(profilesProvider)) {
|
||||
if (profile.type == ProfileType.file) {
|
||||
continue;
|
||||
}
|
||||
@@ -239,34 +352,33 @@ class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateGroups() async {
|
||||
await globalState.updateGroups(appState);
|
||||
}
|
||||
|
||||
updateSystemColorSchemes(SystemColorSchemes systemColorSchemes) {
|
||||
appState.systemColorSchemes = systemColorSchemes;
|
||||
updateSystemColorSchemes(ColorSchemes colorSchemes) {
|
||||
_ref.read(appSchemesProvider.notifier).value = colorSchemes;
|
||||
}
|
||||
|
||||
savePreferences() async {
|
||||
debugPrint("[APP] savePreferences");
|
||||
await preferences.saveConfig(config);
|
||||
await preferences.saveClashConfig(clashConfig);
|
||||
commonPrint.log("save preferences");
|
||||
await preferences.saveConfig(globalState.config);
|
||||
}
|
||||
|
||||
changeProxy({
|
||||
required String groupName,
|
||||
required String proxyName,
|
||||
}) async {
|
||||
await globalState.changeProxy(
|
||||
config: config,
|
||||
groupName: groupName,
|
||||
proxyName: proxyName,
|
||||
await clashCore.changeProxy(
|
||||
ChangeProxyParams(
|
||||
groupName: groupName,
|
||||
proxyName: proxyName,
|
||||
),
|
||||
);
|
||||
if (_ref.read(appSettingProvider).closeConnections) {
|
||||
clashCore.closeConnections();
|
||||
}
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
handleBackOrExit() async {
|
||||
if (config.appSetting.minimizeOnExit) {
|
||||
if (_ref.read(appSettingProvider).minimizeOnExit) {
|
||||
if (system.isDesktop) {
|
||||
await savePreferencesDebounce();
|
||||
}
|
||||
@@ -289,7 +401,7 @@ class AppController {
|
||||
}
|
||||
|
||||
autoCheckUpdate() async {
|
||||
if (!config.appSetting.autoCheckUpdate) return;
|
||||
if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
|
||||
final res = await request.checkForUpdate();
|
||||
checkUpdateResultHandle(data: res);
|
||||
}
|
||||
@@ -346,7 +458,7 @@ class AppController {
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(text: appLocalizations.cacheCorrupt),
|
||||
);
|
||||
if (res) {
|
||||
if (res == true) {
|
||||
final file = File(await appPath.sharedPreferencesPath);
|
||||
final isExists = await file.exists();
|
||||
if (isExists) {
|
||||
@@ -356,33 +468,43 @@ class AppController {
|
||||
await handleExit();
|
||||
}
|
||||
|
||||
Future<void> initCore() async {
|
||||
final isInit = await clashCore.isInit;
|
||||
if (!isInit) {
|
||||
await clashCore.setState(
|
||||
globalState.getCoreState(),
|
||||
);
|
||||
await clashCore.init();
|
||||
}
|
||||
await applyProfile();
|
||||
}
|
||||
|
||||
init() async {
|
||||
await _handlePreference();
|
||||
await _handlerDisclaimer();
|
||||
await globalState.initCore(
|
||||
appState: appState,
|
||||
clashConfig: clashConfig,
|
||||
config: config,
|
||||
);
|
||||
await initCore();
|
||||
await _initStatus();
|
||||
updateTray(true);
|
||||
autoLaunch?.updateStatus(
|
||||
config.appSetting.autoLaunch,
|
||||
_ref.read(appSettingProvider).autoLaunch,
|
||||
);
|
||||
autoUpdateProfiles();
|
||||
autoCheckUpdate();
|
||||
if (!config.appSetting.silentLaunch) {
|
||||
if (!_ref.read(appSettingProvider).silentLaunch) {
|
||||
window?.show();
|
||||
} else {
|
||||
window?.hide();
|
||||
}
|
||||
_ref.read(initProvider.notifier).value = true;
|
||||
}
|
||||
|
||||
_initStatus() async {
|
||||
if (Platform.isAndroid) {
|
||||
await globalState.updateStartTime();
|
||||
}
|
||||
final status =
|
||||
globalState.isStart == true ? true : config.appSetting.autoRun;
|
||||
final status = globalState.isStart == true
|
||||
? true
|
||||
: _ref.read(appSettingProvider).autoRun;
|
||||
|
||||
await updateStatus(status);
|
||||
if (!status) {
|
||||
@@ -391,18 +513,23 @@ class AppController {
|
||||
}
|
||||
|
||||
setDelay(Delay delay) {
|
||||
appState.setDelay(delay);
|
||||
_ref.read(delayDataSourceProvider.notifier).setDelay(delay);
|
||||
}
|
||||
|
||||
toPage(
|
||||
int index, {
|
||||
bool hasAnimate = false,
|
||||
}) {
|
||||
if (index > appState.currentNavigationItems.length - 1) {
|
||||
final navigations = _ref.read(currentNavigationsStateProvider).value;
|
||||
if (index > navigations.length - 1) {
|
||||
return;
|
||||
}
|
||||
appState.currentLabel = appState.currentNavigationItems[index].label;
|
||||
if ((config.appSetting.isAnimateToPage || hasAnimate)) {
|
||||
_ref.read(currentPageLabelProvider.notifier).value =
|
||||
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(
|
||||
index,
|
||||
duration: kTabScrollDuration,
|
||||
@@ -414,9 +541,9 @@ class AppController {
|
||||
}
|
||||
|
||||
toProfiles() {
|
||||
final index = appState.currentNavigationItems.indexWhere(
|
||||
(element) => element.label == "profiles",
|
||||
);
|
||||
final index = _ref.read(currentNavigationsStateProvider).value.indexWhere(
|
||||
(element) => element.label == PageLabel.profiles,
|
||||
);
|
||||
if (index != -1) {
|
||||
toPage(index);
|
||||
}
|
||||
@@ -476,9 +603,9 @@ class AppController {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
disclaimerAccepted: true,
|
||||
);
|
||||
_ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(disclaimerAccepted: true),
|
||||
);
|
||||
Navigator.of(context).pop<bool>(true);
|
||||
},
|
||||
child: Text(appLocalizations.agree),
|
||||
@@ -490,7 +617,7 @@ class AppController {
|
||||
}
|
||||
|
||||
_handlerDisclaimer() async {
|
||||
if (config.appSetting.disclaimerAccepted) {
|
||||
if (_ref.read(appSettingProvider).disclaimerAccepted) {
|
||||
return;
|
||||
}
|
||||
final isDisclaimerAccepted = await showDisclaimer();
|
||||
@@ -551,20 +678,12 @@ class AppController {
|
||||
|
||||
updateViewWidth(double width) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
appState.viewWidth = width;
|
||||
_ref.read(viewWidthProvider.notifier).value = width;
|
||||
});
|
||||
}
|
||||
|
||||
int? getDelay(String proxyName, [String? url]) {
|
||||
final currentDelayMap = appState.delayMap[getRealTestUrl(url)];
|
||||
return currentDelayMap?[appState.getRealProxyName(proxyName)];
|
||||
}
|
||||
|
||||
String getRealTestUrl(String? url) {
|
||||
if (url == null || url.isEmpty) {
|
||||
return config.appSetting.testUrl;
|
||||
}
|
||||
return url;
|
||||
setProvider(ExternalProvider? provider) {
|
||||
_ref.read(providersProvider.notifier).setProvider(provider);
|
||||
}
|
||||
|
||||
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)
|
||||
..sort(
|
||||
(a, b) {
|
||||
final aDelay = getDelay(a.name, url);
|
||||
final bDelay = getDelay(b.name, url);
|
||||
final aDelay =
|
||||
_ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl));
|
||||
final bDelay =
|
||||
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl));
|
||||
if (aDelay == null && bDelay == null) {
|
||||
return 0;
|
||||
}
|
||||
@@ -598,20 +722,16 @@ class AppController {
|
||||
}
|
||||
|
||||
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
|
||||
return switch (config.proxiesStyle.sortType) {
|
||||
return switch (_ref.read(proxiesStyleSettingProvider).sortType) {
|
||||
ProxiesSortType.none => proxies,
|
||||
ProxiesSortType.delay => _sortOfDelay(getRealTestUrl(url), proxies),
|
||||
ProxiesSortType.delay => _sortOfDelay(
|
||||
proxies: proxies,
|
||||
testUrl: url,
|
||||
),
|
||||
ProxiesSortType.name => _sortOfName(proxies),
|
||||
};
|
||||
}
|
||||
|
||||
String getCurrentSelectedName(String groupName) {
|
||||
final group = appState.getGroupWithName(groupName);
|
||||
return group?.getCurrentSelectedName(
|
||||
config.currentSelectedMap[groupName] ?? '') ??
|
||||
'';
|
||||
}
|
||||
|
||||
clearEffect(String profileId) async {
|
||||
final profilePath = await appPath.getProfilePath(profileId);
|
||||
final providersPath = await appPath.getProvidersPath(profileId);
|
||||
@@ -625,38 +745,67 @@ class AppController {
|
||||
});
|
||||
}
|
||||
|
||||
bool get isMobileView {
|
||||
return appState.viewMode == ViewMode.mobile;
|
||||
}
|
||||
|
||||
updateTun() {
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: !clashConfig.tun.enable,
|
||||
);
|
||||
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.tun(enable: !state.tun.enable),
|
||||
);
|
||||
}
|
||||
|
||||
updateSystemProxy() {
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
systemProxy: !config.networkProps.systemProxy,
|
||||
);
|
||||
_ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
systemProxy: state.systemProxy,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
clashConfig.mode = mode;
|
||||
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(mode: mode),
|
||||
);
|
||||
if (mode == Mode.global) {
|
||||
config.updateCurrentGroupName(GroupName.GLOBAL.name);
|
||||
updateCurrentGroupName(GroupName.GLOBAL.name);
|
||||
}
|
||||
addCheckIpNumDebounce();
|
||||
}
|
||||
|
||||
updateAutoLaunch() {
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
autoLaunch: !config.appSetting.autoLaunch,
|
||||
);
|
||||
_ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
autoLaunch: !state.autoLaunch,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
updateVisible() async {
|
||||
@@ -669,18 +818,24 @@ class AppController {
|
||||
}
|
||||
|
||||
updateMode() {
|
||||
final index = Mode.values.indexWhere((item) => item == clashConfig.mode);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
||||
clashConfig.mode = Mode.values[nextIndex];
|
||||
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) {
|
||||
final index = Mode.values.indexWhere((item) => item == state.mode);
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
||||
return state.copyWith(
|
||||
mode: Mode.values[nextIndex],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> exportLogs() async {
|
||||
final logsRaw = appFlowingState.logs.map(
|
||||
(item) => item.toString(),
|
||||
);
|
||||
final logsRaw = _ref.read(logsProvider).list.map(
|
||||
(item) => item.toString(),
|
||||
);
|
||||
final data = await Isolate.run<List<int>>(() async {
|
||||
final logsRawString = logsRaw.join("\n");
|
||||
return utf8.encode(logsRawString);
|
||||
@@ -695,12 +850,10 @@ class AppController {
|
||||
Future<List<int>> backupData() async {
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
final profilesPath = await appPath.profilesPath;
|
||||
final configJson = config.toJson();
|
||||
final clashConfigJson = clashConfig.toJson();
|
||||
final configJson = globalState.config.toJson();
|
||||
return Isolate.run<List<int>>(() async {
|
||||
final archive = Archive();
|
||||
archive.add("config.json", configJson);
|
||||
archive.add("clashConfig.json", clashConfigJson);
|
||||
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
||||
final zipEncoder = ZipEncoder();
|
||||
return zipEncoder.encode(archive) ?? [];
|
||||
@@ -709,11 +862,7 @@ class AppController {
|
||||
|
||||
updateTray([bool focus = false]) async {
|
||||
tray.update(
|
||||
appState: appState,
|
||||
appFlowingState: appFlowingState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
focus: focus,
|
||||
trayState: _ref.read(trayStateProvider),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -732,32 +881,64 @@ class AppController {
|
||||
archive.files.where((item) => !item.name.endsWith(".json"));
|
||||
final configIndex =
|
||||
configs.indexWhere((config) => config.name == "config.json");
|
||||
final clashConfigIndex =
|
||||
configs.indexWhere((config) => config.name == "clashConfig.json");
|
||||
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
|
||||
if (configIndex == -1) throw "invalid backup file";
|
||||
final configFile = configs[configIndex];
|
||||
final clashConfigFile = configs[clashConfigIndex];
|
||||
final tempConfig = Config.fromJson(
|
||||
var tempConfig = Config.compatibleFromJson(
|
||||
json.decode(
|
||||
utf8.decode(configFile.content),
|
||||
),
|
||||
);
|
||||
final tempClashConfig = ClashConfig.fromJson(
|
||||
json.decode(
|
||||
utf8.decode(clashConfigFile.content),
|
||||
),
|
||||
);
|
||||
for (final profile in profiles) {
|
||||
final filePath = join(homeDirPath, profile.name);
|
||||
final file = File(filePath);
|
||||
await file.create(recursive: true);
|
||||
await file.writeAsBytes(profile.content);
|
||||
}
|
||||
if (recoveryOption == RecoveryOption.onlyProfiles) {
|
||||
config.update(tempConfig, RecoveryOption.onlyProfiles);
|
||||
} else {
|
||||
config.update(tempConfig, RecoveryOption.all);
|
||||
clashConfig.update(tempClashConfig);
|
||||
final clashConfigIndex =
|
||||
configs.indexWhere((config) => config.name == "clashConfig.json");
|
||||
if (clashConfigIndex != -1) {
|
||||
final clashConfigFile = configs[clashConfigIndex];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,24 @@ const desktopPlatforms = [
|
||||
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 }
|
||||
|
||||
@@ -45,7 +62,7 @@ extension GroupTypeExtension on GroupType {
|
||||
)
|
||||
.toList();
|
||||
|
||||
bool get isURLTestOrFallback {
|
||||
bool get isComputedSelected {
|
||||
return [GroupType.URLTest, GroupType.Fallback].contains(this);
|
||||
}
|
||||
|
||||
@@ -138,6 +155,13 @@ enum DnsMode {
|
||||
hosts
|
||||
}
|
||||
|
||||
enum ExternalControllerStatus {
|
||||
@JsonValue("")
|
||||
close,
|
||||
@JsonValue("127.0.0.1:9090")
|
||||
open
|
||||
}
|
||||
|
||||
enum KeyboardModifier {
|
||||
alt([
|
||||
PhysicalKeyboardKey.altLeft,
|
||||
@@ -238,6 +262,7 @@ enum ActionMethod {
|
||||
stopListener,
|
||||
getCountryCode,
|
||||
getMemory,
|
||||
getProfile,
|
||||
|
||||
///Android,
|
||||
setFdMap,
|
||||
@@ -270,7 +295,11 @@ enum DebounceTag {
|
||||
handleWill,
|
||||
updateDelay,
|
||||
vpnTip,
|
||||
autoLaunch
|
||||
autoLaunch,
|
||||
renderPause,
|
||||
updatePageIndex,
|
||||
pageChange,
|
||||
proxiesTabChange,
|
||||
}
|
||||
|
||||
enum DashboardWidget {
|
||||
@@ -341,3 +370,19 @@ enum DashboardWidget {
|
||||
return dashboardWidgets[index];
|
||||
}
|
||||
}
|
||||
|
||||
enum GeodataLoader {
|
||||
standard,
|
||||
memconservative,
|
||||
}
|
||||
|
||||
enum PageLabel {
|
||||
dashboard,
|
||||
proxies,
|
||||
profiles,
|
||||
tools,
|
||||
logs,
|
||||
requests,
|
||||
resources,
|
||||
connections,
|
||||
}
|
||||
|
||||
@@ -4,20 +4,21 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/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});
|
||||
|
||||
@override
|
||||
State<AccessFragment> createState() => _AccessFragmentState();
|
||||
ConsumerState<AccessFragment> createState() => _AccessFragmentState();
|
||||
}
|
||||
|
||||
class _AccessFragmentState extends State<AccessFragment> {
|
||||
class _AccessFragmentState extends ConsumerState<AccessFragment> {
|
||||
List<String> acceptList = [];
|
||||
List<String> rejectList = [];
|
||||
late ScrollController _controller;
|
||||
@@ -28,10 +29,11 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
_updateInitList();
|
||||
_controller = ScrollController();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appState = globalState.appController.appState;
|
||||
final appState = globalState.appState;
|
||||
if (appState.packages.isEmpty) {
|
||||
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() {
|
||||
final accessControl = globalState.appController.config.accessControl;
|
||||
acceptList = accessControl.acceptList;
|
||||
rejectList = accessControl.rejectList;
|
||||
acceptList = globalState.config.vpnProps.accessControl.acceptList;
|
||||
rejectList = globalState.config.vpnProps.accessControl.rejectList;
|
||||
}
|
||||
|
||||
Widget _buildSearchButton() {
|
||||
@@ -59,9 +60,13 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
acceptList: acceptList,
|
||||
rejectList: rejectList,
|
||||
),
|
||||
).then((_) => setState(() {
|
||||
).then(
|
||||
(_) => setState(
|
||||
() {
|
||||
_updateInitList();
|
||||
}));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
);
|
||||
@@ -77,28 +82,29 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
return IconButton(
|
||||
tooltip: tooltip,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
final isAccept =
|
||||
config.accessControl.mode == AccessControlMode.acceptSelected;
|
||||
if (isSelectedAll) {
|
||||
config.accessControl = switch (isAccept) {
|
||||
true => config.accessControl.copyWith(
|
||||
acceptList: [],
|
||||
),
|
||||
false => config.accessControl.copyWith(
|
||||
rejectList: [],
|
||||
),
|
||||
};
|
||||
} else {
|
||||
config.accessControl = switch (isAccept) {
|
||||
true => config.accessControl.copyWith(
|
||||
acceptList: allValueList,
|
||||
),
|
||||
false => config.accessControl.copyWith(
|
||||
rejectList: allValueList,
|
||||
),
|
||||
};
|
||||
}
|
||||
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
||||
final isAccept =
|
||||
state.accessControl.mode == AccessControlMode.acceptSelected;
|
||||
if (isSelectedAll) {
|
||||
return switch (isAccept) {
|
||||
true => state.copyWith.accessControl(
|
||||
acceptList: [],
|
||||
),
|
||||
false => state.copyWith.accessControl(
|
||||
rejectList: [],
|
||||
),
|
||||
};
|
||||
} else {
|
||||
return switch (isAccept) {
|
||||
true => state.copyWith.accessControl(
|
||||
acceptList: allValueList,
|
||||
),
|
||||
false => state.copyWith.accessControl(
|
||||
rejectList: allValueList,
|
||||
),
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: isSelectedAll
|
||||
? const Icon(Icons.deselect)
|
||||
@@ -106,223 +112,239 @@ class _AccessFragmentState extends State<AccessFragment> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
title: appLocalizations.proxiesSetting,
|
||||
context: context,
|
||||
body: AccessControlWidget(
|
||||
context: context,
|
||||
_intelligentSelected() async {
|
||||
final appState = globalState.appState;
|
||||
final config = globalState.config;
|
||||
final accessControl = config.vpnProps.accessControl;
|
||||
final packageNames = appState.packages
|
||||
.where(
|
||||
(item) =>
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
_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
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.isAccessControl,
|
||||
builder: (_, isAccessControl, child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: ListItem.switchItem(
|
||||
title: Text(appLocalizations.appAccessControl),
|
||||
delegate: SwitchDelegate(
|
||||
value: isAccessControl,
|
||||
onChanged: (isAccessControl) {
|
||||
final config = context.read<Config>();
|
||||
config.isAccessControl = isAccessControl;
|
||||
},
|
||||
),
|
||||
),
|
||||
final state = ref.watch(packageListSelectorStateProvider);
|
||||
final accessControl = state.accessControl;
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: ListItem.switchItem(
|
||||
title: Text(appLocalizations.appAccessControl),
|
||||
delegate: SwitchDelegate(
|
||||
value: accessControl.enable,
|
||||
onChanged: (enable) {
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith.accessControl(
|
||||
enable: enable,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Divider(
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: child!,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
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(
|
||||
children: [
|
||||
ActivateBox(
|
||||
active: isAccessControl,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: 16,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
appLocalizations.selected,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Flexible(
|
||||
child: SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
"${valueList.length}",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(describe),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Divider(
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: DisabledMask(
|
||||
status: !accessControl.enable,
|
||||
child: Column(
|
||||
children: [
|
||||
ActivateBox(
|
||||
active: accessControl.enable,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: 16,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: _buildSearchButton(),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSelectedAllButton(
|
||||
isSelectedAll: valueList.length ==
|
||||
packageNameList.length,
|
||||
allValueList: packageNameList,
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
appLocalizations.selected,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Flexible(
|
||||
child: SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
"${valueList.length}",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSettingButton(),
|
||||
),
|
||||
child: Text(describe),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: _buildSearchButton(),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSelectedAllButton(
|
||||
isSelectedAll:
|
||||
valueList.length == packageNameList.length,
|
||||
allValueList: packageNameList,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: _buildSettingButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: packages.isEmpty
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: CommonScrollBar(
|
||||
controller: _controller,
|
||||
child: ListView.builder(
|
||||
controller: _controller,
|
||||
itemCount: packages.length,
|
||||
itemExtent: 72,
|
||||
itemBuilder: (_, index) {
|
||||
final package = packages[index];
|
||||
return PackageListItem(
|
||||
key: Key(package.packageName),
|
||||
package: package,
|
||||
value:
|
||||
valueList.contains(package.packageName),
|
||||
isActive: isAccessControl,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
valueList.add(package.packageName);
|
||||
} else {
|
||||
valueList.remove(package.packageName);
|
||||
}
|
||||
final config =
|
||||
globalState.appController.config;
|
||||
if (accessControlMode ==
|
||||
AccessControlMode.acceptSelected) {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
acceptList: valueList,
|
||||
);
|
||||
} else {
|
||||
config.accessControl =
|
||||
config.accessControl.copyWith(
|
||||
rejectList: valueList,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: packages.isEmpty
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: CommonScrollBar(
|
||||
controller: _controller,
|
||||
child: ListView.builder(
|
||||
controller: _controller,
|
||||
itemCount: packages.length,
|
||||
itemExtent: 72,
|
||||
itemBuilder: (_, index) {
|
||||
final package = packages[index];
|
||||
return PackageListItem(
|
||||
key: Key(package.packageName),
|
||||
package: package,
|
||||
value: valueList.contains(package.packageName),
|
||||
isActive: accessControl.enable,
|
||||
onChanged: (value) {
|
||||
_handleSelected(valueList, package, value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -343,45 +365,47 @@ class PackageListItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActivateBox(
|
||||
active: isActive,
|
||||
child: ListItem.checkbox(
|
||||
leading: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: FutureBuilder<ImageProvider?>(
|
||||
future: app?.getPackageIcon(package.packageName),
|
||||
builder: (_, snapshot) {
|
||||
if (!snapshot.hasData && snapshot.data == null) {
|
||||
return Container();
|
||||
} else {
|
||||
return Image(
|
||||
image: snapshot.data!,
|
||||
gaplessPlayback: true,
|
||||
width: 48,
|
||||
height: 48,
|
||||
);
|
||||
}
|
||||
},
|
||||
return FadeScaleEnterBox(
|
||||
child: ActivateBox(
|
||||
active: isActive,
|
||||
child: ListItem.checkbox(
|
||||
leading: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: FutureBuilder<ImageProvider?>(
|
||||
future: app?.getPackageIcon(package.packageName),
|
||||
builder: (_, snapshot) {
|
||||
if (!snapshot.hasData && snapshot.data == null) {
|
||||
return Container();
|
||||
} else {
|
||||
return Image(
|
||||
image: snapshot.data!,
|
||||
gaplessPlayback: true,
|
||||
width: 48,
|
||||
height: 48,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
package.label,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
title: Text(
|
||||
package.label,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
package.packageName,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
subtitle: Text(
|
||||
package.packageName,
|
||||
style: const TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
delegate: CheckboxDelegate(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
delegate: CheckboxDelegate(
|
||||
value: value,
|
||||
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() {
|
||||
final lowQuery = query.toLowerCase();
|
||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
||||
selector: (_, appState, config) => PackageListSelectorState(
|
||||
packages: appState.packages,
|
||||
accessControl: config.accessControl,
|
||||
isAccessControl: config.isAccessControl,
|
||||
),
|
||||
builder: (context, state, __) {
|
||||
return Consumer(
|
||||
builder: (context, ref, __) {
|
||||
final state = ref.watch(packageListSelectorStateProvider);
|
||||
final accessControl = state.accessControl;
|
||||
final accessControlMode = accessControl.mode;
|
||||
final packages = state.getList(
|
||||
@@ -449,7 +489,7 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
package.packageName.contains(lowQuery),
|
||||
)
|
||||
.toList();
|
||||
final isAccessControl = state.isAccessControl;
|
||||
final isAccessControl = state.accessControl.enable;
|
||||
final currentList = accessControl.currentList;
|
||||
final packageNameList = packages.map((e) => e.packageName).toList();
|
||||
final valueList = currentList.intersection(packageNameList);
|
||||
@@ -465,21 +505,12 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
value: valueList.contains(package.packageName),
|
||||
isActive: isAccessControl,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
valueList.add(package.packageName);
|
||||
} else {
|
||||
valueList.remove(package.packageName);
|
||||
}
|
||||
final config = globalState.appController.config;
|
||||
if (accessControlMode == AccessControlMode.acceptSelected) {
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
acceptList: valueList,
|
||||
);
|
||||
} else {
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
rejectList: valueList,
|
||||
);
|
||||
}
|
||||
_handleSelected(
|
||||
ref,
|
||||
valueList,
|
||||
package,
|
||||
value,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -500,14 +531,16 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
class AccessControlWidget extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
|
||||
const AccessControlWidget({
|
||||
class AccessControlPanel extends ConsumerStatefulWidget {
|
||||
const AccessControlPanel({
|
||||
super.key,
|
||||
required this.context,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState createState() => _AccessControlPanelState();
|
||||
}
|
||||
|
||||
class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
|
||||
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
|
||||
return switch (mode) {
|
||||
AccessControlMode.acceptSelected => Icons.adjust_outlined,
|
||||
@@ -552,9 +585,11 @@ class AccessControlWidget extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, AccessControlMode>(
|
||||
selector: (_, config) => config.accessControl.mode,
|
||||
builder: (_, accessControlMode, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final accessControlMode = ref.watch(
|
||||
vpnSettingProvider.select((state) => state.accessControl.mode),
|
||||
);
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
@@ -566,10 +601,11 @@ class AccessControlWidget extends StatelessWidget {
|
||||
),
|
||||
isSelected: accessControlMode == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
mode: item,
|
||||
);
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith.accessControl(
|
||||
mode: item,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
@@ -588,9 +624,11 @@ class AccessControlWidget extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, AccessSortType>(
|
||||
selector: (_, config) => config.accessControl.sort,
|
||||
builder: (_, accessSortType, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final accessSortType = ref.watch(
|
||||
vpnSettingProvider.select((state) => state.accessControl.sort),
|
||||
);
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
@@ -602,10 +640,11 @@ class AccessControlWidget extends StatelessWidget {
|
||||
),
|
||||
isSelected: accessSortType == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
sort: item,
|
||||
);
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith.accessControl(
|
||||
sort: item,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -624,9 +663,12 @@ class AccessControlWidget extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, bool>(
|
||||
selector: (_, config) => config.accessControl.isFilterSystemApp,
|
||||
builder: (_, isFilterSystemApp, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final isFilterSystemApp = ref.watch(
|
||||
vpnSettingProvider
|
||||
.select((state) => state.accessControl.isFilterSystemApp),
|
||||
);
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
@@ -635,10 +677,11 @@ class AccessControlWidget extends StatelessWidget {
|
||||
_getTextWithIsFilterSystemApp(item),
|
||||
isSelected: isFilterSystemApp == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.accessControl = config.accessControl.copyWith(
|
||||
isFilterSystemApp: item,
|
||||
);
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith.accessControl(
|
||||
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 {
|
||||
await globalState.safeRun(() {
|
||||
final data = globalState.appController.config.accessControl.toJson();
|
||||
final data = globalState.config.vpnProps.accessControl.toJson();
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: json.encode(data),
|
||||
),
|
||||
);
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
_pasteToClipboard() async {
|
||||
await globalState.safeRun(() async {
|
||||
final config = globalState.appController.config;
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text == null) return;
|
||||
config.accessControl = AccessControl.fromJson(
|
||||
json.decode(text),
|
||||
);
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
await globalState.safeRun(
|
||||
() async {
|
||||
final data = await Clipboard.getData('text/plain');
|
||||
final text = data?.text;
|
||||
if (text == null) return;
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
accessControl: AccessControl.fromJson(
|
||||
json.decode(text),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@@ -725,7 +740,9 @@ class AccessControlWidget extends StatelessWidget {
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.auto_awesome),
|
||||
label: appLocalizations.intelligentSelected,
|
||||
onPressed: _intelligentSelected,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(1);
|
||||
},
|
||||
),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.paste),
|
||||
|
||||
@@ -1,61 +1,258 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CloseConnectionsSwitch extends StatelessWidget {
|
||||
const CloseConnectionsSwitch({super.key});
|
||||
class CloseConnectionsItem extends ConsumerWidget {
|
||||
const CloseConnectionsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.closeConnections,
|
||||
builder: (_, closeConnections, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.autoCloseConnections),
|
||||
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: closeConnections,
|
||||
onChanged: (value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
closeConnections: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final closeConnections = ref.watch(
|
||||
appSettingProvider.select((state) => state.closeConnections),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.autoCloseConnections),
|
||||
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: closeConnections,
|
||||
onChanged: (value) async {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
closeConnections: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UsageSwitch extends StatelessWidget {
|
||||
const UsageSwitch({super.key});
|
||||
class UsageItem extends ConsumerWidget {
|
||||
const UsageItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.onlyStatisticsProxy,
|
||||
builder: (_, onlyProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: onlyProxy,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
onlyStatisticsProxy: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final onlyStatisticsProxy = ref.watch(
|
||||
appSettingProvider.select((state) => state.onlyStatisticsProxy),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.onlyStatisticsProxy),
|
||||
subtitle: Text(appLocalizations.onlyStatisticsProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: onlyStatisticsProxy,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> items = [
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.minimizeOnExit,
|
||||
builder: (_, isMinimizeOnExit, child) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.minimizeOnExit),
|
||||
subtitle: Text(appLocalizations.minimizeOnExitDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isMinimizeOnExit,
|
||||
onChanged: (bool value) {
|
||||
final config = context.read<Config>();
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
minimizeOnExit: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MinimizeItem(),
|
||||
if (system.isDesktop) ...[
|
||||
AutoLaunchItem(),
|
||||
SilentLaunchItem(),
|
||||
],
|
||||
AutoRunItem(),
|
||||
if (Platform.isAndroid) ...[
|
||||
HiddenItem(),
|
||||
AnimateTabItem(),
|
||||
],
|
||||
OpenLogsItem(),
|
||||
CloseConnectionsItem(),
|
||||
UsageItem(),
|
||||
AutoCheckUpdateItem(),
|
||||
];
|
||||
return ListView.separated(
|
||||
itemBuilder: (_, index) {
|
||||
|
||||
@@ -4,14 +4,15 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/common/dav_client.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/fade_box.dart';
|
||||
import 'package:fl_clash/widgets/list.dart';
|
||||
import 'package:fl_clash/widgets/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class BackupAndRecovery extends StatelessWidget {
|
||||
class BackupAndRecovery extends ConsumerWidget {
|
||||
const BackupAndRecovery({super.key});
|
||||
|
||||
_showAddWebDAV(DAV? dav) async {
|
||||
@@ -121,139 +122,140 @@ class BackupAndRecovery extends StatelessWidget {
|
||||
_recoveryOnLocal(context, recoveryOption);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, DAV?>(
|
||||
selector: (_, config) => config.dav,
|
||||
builder: (_, dav, __) {
|
||||
final client = dav != null ? DAVClient(dav) : null;
|
||||
return ListView(
|
||||
children: [
|
||||
ListHeader(title: appLocalizations.remote),
|
||||
if (dav == null)
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: Text(appLocalizations.noInfo),
|
||||
subtitle: Text(appLocalizations.pleaseBindWebDAV),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_showAddWebDAV(dav);
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.bind,
|
||||
),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: TooltipText(
|
||||
text: Text(
|
||||
dav.user,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(appLocalizations.connectivity),
|
||||
FutureBuilder<bool>(
|
||||
future: client!.pingCompleter.future,
|
||||
builder: (_, snapshot) {
|
||||
return Center(
|
||||
child: FadeBox(
|
||||
child: snapshot.connectionState ==
|
||||
ConnectionState.waiting
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: snapshot.data == true
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
width: 12,
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_showAddWebDAV(dav);
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.edit,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
ListItem.input(
|
||||
title: Text(appLocalizations.file),
|
||||
subtitle: Text(dav.fileName),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.file,
|
||||
value: dav.fileName,
|
||||
resetValue: defaultDavFileName,
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.config.dav =
|
||||
globalState.appController.config.dav?.copyWith(
|
||||
fileName: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_backupOnWebDAV(context, client);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.remoteBackupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnWebDAV(context, client);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.remoteRecoveryDesc),
|
||||
),
|
||||
],
|
||||
ListHeader(title: appLocalizations.local),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_backupOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.localBackupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.localRecoveryDesc),
|
||||
),
|
||||
],
|
||||
_handleChange(String? value, WidgetRef ref) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref.read(appDAVSettingProvider.notifier).updateState(
|
||||
(state) => state?.copyWith(
|
||||
fileName: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final dav = ref.watch(appDAVSettingProvider);
|
||||
final client = dav != null ? DAVClient(dav) : null;
|
||||
return ListView(
|
||||
children: [
|
||||
ListHeader(title: appLocalizations.remote),
|
||||
if (dav == null)
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: Text(appLocalizations.noInfo),
|
||||
subtitle: Text(appLocalizations.pleaseBindWebDAV),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_showAddWebDAV(dav);
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.bind,
|
||||
),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
ListItem(
|
||||
leading: const Icon(Icons.account_box),
|
||||
title: TooltipText(
|
||||
text: Text(
|
||||
dav.user,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(appLocalizations.connectivity),
|
||||
FutureBuilder<bool>(
|
||||
future: client!.pingCompleter.future,
|
||||
builder: (_, snapshot) {
|
||||
return Center(
|
||||
child: FadeBox(
|
||||
child: snapshot.connectionState ==
|
||||
ConnectionState.waiting
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: snapshot.data == true
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
width: 12,
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
_showAddWebDAV(dav);
|
||||
},
|
||||
child: Text(
|
||||
appLocalizations.edit,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
ListItem.input(
|
||||
title: Text(appLocalizations.file),
|
||||
subtitle: Text(dav.fileName),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.file,
|
||||
value: dav.fileName,
|
||||
resetValue: defaultDavFileName,
|
||||
onChanged: (value) {
|
||||
_handleChange(value, ref);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_backupOnWebDAV(context, client);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.remoteBackupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnWebDAV(context, client);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.remoteRecoveryDesc),
|
||||
),
|
||||
],
|
||||
ListHeader(title: appLocalizations.local),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_backupOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.backup),
|
||||
subtitle: Text(appLocalizations.localBackupDesc),
|
||||
),
|
||||
ListItem(
|
||||
onTap: () {
|
||||
_handleRecoveryOnLocal(context);
|
||||
},
|
||||
title: Text(appLocalizations.recovery),
|
||||
subtitle: Text(appLocalizations.localRecoveryDesc),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -302,16 +304,16 @@ class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
class WebDAVFormDialog extends StatefulWidget {
|
||||
class WebDAVFormDialog extends ConsumerStatefulWidget {
|
||||
final DAV? dav;
|
||||
|
||||
const WebDAVFormDialog({super.key, this.dav});
|
||||
|
||||
@override
|
||||
State<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
|
||||
ConsumerState<WebDAVFormDialog> createState() => _WebDAVFormDialogState();
|
||||
}
|
||||
|
||||
class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
class _WebDAVFormDialogState extends ConsumerState<WebDAVFormDialog> {
|
||||
late TextEditingController uriController;
|
||||
late TextEditingController userController;
|
||||
late TextEditingController passwordController;
|
||||
@@ -328,7 +330,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
|
||||
_submit() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
globalState.appController.config.dav = DAV(
|
||||
ref.read(appDAVSettingProvider.notifier).value = DAV(
|
||||
uri: uriController.text,
|
||||
user: userController.text,
|
||||
password: passwordController.text,
|
||||
@@ -337,7 +339,7 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
|
||||
}
|
||||
|
||||
_delete() {
|
||||
globalState.appController.config.dav = null;
|
||||
ref.read(appDAVSettingProvider.notifier).value = null;
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,238 +1,199 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package: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:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class OverrideItem extends StatelessWidget {
|
||||
class OverrideItem extends ConsumerWidget {
|
||||
const OverrideItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.overrideDns,
|
||||
builder: (_, override, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.overrideDns),
|
||||
subtitle: Text(appLocalizations.overrideDnsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: override,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.overrideDns = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final override = ref.watch(overrideDnsProvider);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.overrideDns),
|
||||
subtitle: Text(appLocalizations.overrideDnsDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: override,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(overrideDnsProvider.notifier).value = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StatusItem extends StatelessWidget {
|
||||
class StatusItem extends ConsumerWidget {
|
||||
const StatusItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.enable,
|
||||
builder: (_, enable, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.status),
|
||||
subtitle: Text(appLocalizations.statusDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
enable: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final enable =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.dns.enable));
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.status),
|
||||
subtitle: Text(appLocalizations.statusDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(enable: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PreferH3Item extends StatelessWidget {
|
||||
class PreferH3Item extends ConsumerWidget {
|
||||
const PreferH3Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.preferH3,
|
||||
builder: (_, preferH3, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("PreferH3"),
|
||||
subtitle: Text(appLocalizations.preferH3Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: preferH3,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
preferH3: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final preferH3 = ref
|
||||
.watch(patchClashConfigProvider.select((state) => state.dns.preferH3));
|
||||
return ListItem.switchItem(
|
||||
title: const Text("PreferH3"),
|
||||
subtitle: Text(appLocalizations.preferH3Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: preferH3,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(preferH3: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IPv6Item extends StatelessWidget {
|
||||
class IPv6Item extends ConsumerWidget {
|
||||
const IPv6Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("IPv6"),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
ipv6: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ipv6 = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.ipv6),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: const Text("IPv6"),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(ipv6: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RespectRulesItem extends StatelessWidget {
|
||||
class RespectRulesItem extends ConsumerWidget {
|
||||
const RespectRulesItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.respectRules,
|
||||
builder: (_, respectRules, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.respectRules),
|
||||
subtitle: Text(appLocalizations.respectRulesDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: respectRules,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
respectRules: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final respectRules = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.respectRules),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.respectRules),
|
||||
subtitle: Text(appLocalizations.respectRulesDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: respectRules,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(respectRules: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DnsModeItem extends StatelessWidget {
|
||||
class DnsModeItem extends ConsumerWidget {
|
||||
const DnsModeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, DnsMode>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.enhancedMode,
|
||||
builder: (_, enhancedMode, __) {
|
||||
return ListItem<DnsMode>.options(
|
||||
title: Text(appLocalizations.dnsMode),
|
||||
subtitle: Text(enhancedMode.name),
|
||||
delegate: OptionsDelegate(
|
||||
title: appLocalizations.dnsMode,
|
||||
options: DnsMode.values,
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(enhancedMode: value);
|
||||
},
|
||||
textBuilder: (dnsMode) => dnsMode.name,
|
||||
value: enhancedMode,
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final enhancedMode = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.enhancedMode),
|
||||
);
|
||||
return ListItem<DnsMode>.options(
|
||||
title: Text(appLocalizations.dnsMode),
|
||||
subtitle: Text(enhancedMode.name),
|
||||
delegate: OptionsDelegate(
|
||||
title: appLocalizations.dnsMode,
|
||||
options: DnsMode.values,
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(enhancedMode: value));
|
||||
},
|
||||
textBuilder: (dnsMode) => dnsMode.name,
|
||||
value: enhancedMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeIpRangeItem extends StatelessWidget {
|
||||
class FakeIpRangeItem extends ConsumerWidget {
|
||||
const FakeIpRangeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, String>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange,
|
||||
builder: (_, fakeIpRange, __) {
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.fakeipRange),
|
||||
subtitle: Text(fakeIpRange),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.fakeipRange,
|
||||
value: fakeIpRange,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.dns = clashConfig.dns.copyWith(
|
||||
fakeIpRange: value,
|
||||
);
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.fakeipRange,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final fakeIpRange = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.fakeIpRange),
|
||||
);
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.fakeipRange),
|
||||
subtitle: Text(fakeIpRange),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.fakeipRange,
|
||||
value: fakeIpRange,
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(fakeIpRange: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeIpFilterItem extends StatelessWidget {
|
||||
class FakeIpFilterItem extends ConsumerWidget {
|
||||
const FakeIpFilterItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final fakeIpFilter = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.fakeIpFilter),
|
||||
);
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.fakeipFilter),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.fakeipFilter,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, fakeIpFilter, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.fakeipFilter,
|
||||
items: fakeIpFilter,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fakeIpFilter: List.from(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
widget: ListPage(
|
||||
title: appLocalizations.fakeipFilter,
|
||||
items: fakeIpFilter,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(
|
||||
fakeIpFilter: List.from(items),
|
||||
));
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
@@ -241,33 +202,30 @@ class FakeIpFilterItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultNameserverItem extends StatelessWidget {
|
||||
class DefaultNameserverItem extends ConsumerWidget {
|
||||
const DefaultNameserverItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final defaultNameserver = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.defaultNameserver),
|
||||
);
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.defaultNameserver),
|
||||
subtitle: Text(appLocalizations.defaultNameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.defaultNameserver,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, defaultNameserver, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.defaultNameserver,
|
||||
items: defaultNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
defaultNameserver: List.from(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
widget: ListPage(
|
||||
title: appLocalizations.defaultNameserver,
|
||||
items: defaultNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(
|
||||
defaultNameserver: List.from(items),
|
||||
));
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
@@ -276,33 +234,30 @@ class DefaultNameserverItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class NameserverItem extends StatelessWidget {
|
||||
class NameserverItem extends ConsumerWidget {
|
||||
const NameserverItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final nameserver = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.nameserver),
|
||||
);
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.nameserver),
|
||||
subtitle: Text(appLocalizations.nameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
title: appLocalizations.nameserver,
|
||||
isBlur: false,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.nameserver,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, nameserver, __) {
|
||||
return ListPage(
|
||||
title: "域名服务器",
|
||||
items: nameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
nameserver: List.from(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
widget: ListPage(
|
||||
title: "域名服务器",
|
||||
items: nameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(
|
||||
nameserver: List.from(items),
|
||||
));
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
@@ -311,87 +266,77 @@ class NameserverItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class UseHostsItem extends StatelessWidget {
|
||||
class UseHostsItem extends ConsumerWidget {
|
||||
const UseHostsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.useHosts,
|
||||
builder: (_, useHosts, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.useHosts),
|
||||
delegate: SwitchDelegate(
|
||||
value: useHosts,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
useHosts: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final useHosts = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.useHosts),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.useHosts),
|
||||
delegate: SwitchDelegate(
|
||||
value: useHosts,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(useHosts: value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UseSystemHostsItem extends StatelessWidget {
|
||||
class UseSystemHostsItem extends ConsumerWidget {
|
||||
const UseSystemHostsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts,
|
||||
builder: (_, useSystemHosts, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.useSystemHosts),
|
||||
delegate: SwitchDelegate(
|
||||
value: useSystemHosts,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
useSystemHosts: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final useSystemHosts = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.useSystemHosts),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.useSystemHosts),
|
||||
delegate: SwitchDelegate(
|
||||
value: useSystemHosts,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(
|
||||
useSystemHosts: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NameserverPolicyItem extends StatelessWidget {
|
||||
class NameserverPolicyItem extends ConsumerWidget {
|
||||
const NameserverPolicyItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final nameserverPolicy = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.nameserverPolicy),
|
||||
);
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.nameserverPolicy),
|
||||
subtitle: Text(appLocalizations.nameserverPolicyDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.nameserverPolicy,
|
||||
widget: Selector<ClashConfig, Map<String, String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const MapEquality<String, String>().equals(prev, next),
|
||||
builder: (_, nameserverPolicy, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.nameserverPolicy,
|
||||
items: nameserverPolicy.entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
nameserverPolicy: Map.fromEntries(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
widget: ListPage(
|
||||
title: appLocalizations.nameserverPolicy,
|
||||
items: nameserverPolicy.entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(
|
||||
nameserverPolicy: Map.fromEntries(items),
|
||||
));
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
@@ -400,33 +345,31 @@ class NameserverPolicyItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ProxyServerNameserverItem extends StatelessWidget {
|
||||
class ProxyServerNameserverItem extends ConsumerWidget {
|
||||
const ProxyServerNameserverItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final proxyServerNameserver = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.proxyServerNameserver),
|
||||
);
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.proxyNameserver),
|
||||
subtitle: Text(appLocalizations.proxyNameserverDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.proxyNameserver,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, proxyServerNameserver, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.proxyNameserver,
|
||||
items: proxyServerNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
proxyServerNameserver: List.from(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
widget: ListPage(
|
||||
title: appLocalizations.proxyNameserver,
|
||||
items: proxyServerNameserver,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(
|
||||
proxyServerNameserver: List.from(items),
|
||||
));
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
@@ -435,33 +378,30 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class FallbackItem extends StatelessWidget {
|
||||
class FallbackItem extends ConsumerWidget {
|
||||
const FallbackItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final fallback = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.dns.fallback),
|
||||
);
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.fallback),
|
||||
subtitle: Text(appLocalizations.fallbackDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.fallback,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallback,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, fallback, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.fallback,
|
||||
items: fallback,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallback: List.from(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
widget: ListPage(
|
||||
title: appLocalizations.fallback,
|
||||
items: fallback,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns(
|
||||
fallback: List.from(items),
|
||||
));
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
@@ -470,101 +410,85 @@ class FallbackItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class GeoipItem extends StatelessWidget {
|
||||
class GeoipItem extends ConsumerWidget {
|
||||
const GeoipItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip,
|
||||
builder: (_, geoip, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("Geoip"),
|
||||
delegate: SwitchDelegate(
|
||||
value: geoip,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final geoip = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.geoip),
|
||||
);
|
||||
return ListItem.switchItem(
|
||||
title: const Text("Geoip"),
|
||||
delegate: SwitchDelegate(
|
||||
value: geoip,
|
||||
onChanged: (bool value) async {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||
geoip: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeoipCodeItem extends StatelessWidget {
|
||||
class GeoipCodeItem extends ConsumerWidget {
|
||||
const GeoipCodeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, String>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode,
|
||||
builder: (_, geoipCode, __) {
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.geoipCode),
|
||||
subtitle: Text(geoipCode),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.geoipCode,
|
||||
value: geoipCode,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
geoipCode: value,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.geoipCode,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final geoipCode = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.geoipCode),
|
||||
);
|
||||
return ListItem.input(
|
||||
title: Text(appLocalizations.geoipCode),
|
||||
subtitle: Text(geoipCode),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.geoipCode,
|
||||
value: geoipCode,
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||
geoipCode: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeositeItem extends StatelessWidget {
|
||||
class GeositeItem extends ConsumerWidget {
|
||||
const GeositeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final geosite = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.geosite),
|
||||
);
|
||||
return ListItem.open(
|
||||
title: const Text("Geosite"),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: "Geosite",
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, geosite, __) {
|
||||
return ListPage(
|
||||
title: "Geosite",
|
||||
items: geosite,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
geosite: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
widget: ListPage(
|
||||
title: "Geosite",
|
||||
items: geosite,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||
geosite: List.from(items),
|
||||
));
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
@@ -573,34 +497,30 @@ class GeositeItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class IpcidrItem extends StatelessWidget {
|
||||
class IpcidrItem extends ConsumerWidget {
|
||||
const IpcidrItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ipcidr = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.ipcidr),
|
||||
);
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.ipcidr),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.ipcidr,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, ipcidr, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.ipcidr,
|
||||
items: ipcidr,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
ipcidr: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
widget: ListPage(
|
||||
title: appLocalizations.ipcidr,
|
||||
items: ipcidr,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||
ipcidr: List.from(items),
|
||||
));
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
@@ -609,34 +529,30 @@ class IpcidrItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class DomainItem extends StatelessWidget {
|
||||
class DomainItem extends ConsumerWidget {
|
||||
const DomainItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final domain = ref.watch(
|
||||
patchClashConfigProvider
|
||||
.select((state) => state.dns.fallbackFilter.domain),
|
||||
);
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.domain),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: appLocalizations.domain,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (_, domain, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.domain,
|
||||
items: domain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
final dns = clashConfig.dns;
|
||||
clashConfig.dns = dns.copyWith(
|
||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
||||
domain: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
widget: ListPage(
|
||||
title: appLocalizations.domain,
|
||||
items: domain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||
domain: List.from(items),
|
||||
));
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
@@ -700,12 +616,12 @@ const dnsItems = <Widget>[
|
||||
FallbackFilterOptions(),
|
||||
];
|
||||
|
||||
class DnsListView extends StatelessWidget {
|
||||
class DnsListView extends ConsumerWidget {
|
||||
const DnsListView({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
_initActions(BuildContext context, WidgetRef ref) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
context.commonScaffoldState?.actions = [
|
||||
context.commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final res = await globalState.showMessage(
|
||||
@@ -717,7 +633,12 @@ class DnsListView extends StatelessWidget {
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
globalState.appController.clashConfig.dns = defaultDns;
|
||||
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
dns: defaultDns,
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
@@ -729,8 +650,8 @@ class DnsListView extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_initActions(context);
|
||||
Widget build(BuildContext context, ref) {
|
||||
_initActions(context, ref);
|
||||
return generateListView(
|
||||
dnsItems,
|
||||
);
|
||||
|
||||
@@ -1,207 +1,201 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class LogLevelItem extends StatelessWidget {
|
||||
class LogLevelItem extends ConsumerWidget {
|
||||
const LogLevelItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, LogLevel>(
|
||||
selector: (_, clashConfig) => clashConfig.logLevel,
|
||||
builder: (_, value, __) {
|
||||
return ListItem<LogLevel>.options(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(appLocalizations.logLevel),
|
||||
subtitle: Text(value.name),
|
||||
delegate: OptionsDelegate<LogLevel>(
|
||||
title: appLocalizations.logLevel,
|
||||
options: LogLevel.values,
|
||||
onChanged: (LogLevel? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.logLevel = value;
|
||||
},
|
||||
textBuilder: (logLevel) => logLevel.name,
|
||||
value: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final logLevel =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.logLevel));
|
||||
return ListItem<LogLevel>.options(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(appLocalizations.logLevel),
|
||||
subtitle: Text(logLevel.name),
|
||||
delegate: OptionsDelegate<LogLevel>(
|
||||
title: appLocalizations.logLevel,
|
||||
options: LogLevel.values,
|
||||
onChanged: (LogLevel? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
logLevel: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
textBuilder: (logLevel) => logLevel.name,
|
||||
value: logLevel,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UaItem extends StatelessWidget {
|
||||
class UaItem extends ConsumerWidget {
|
||||
const UaItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, String?>(
|
||||
selector: (_, clashConfig) => clashConfig.globalRealUa,
|
||||
builder: (_, value, __) {
|
||||
return ListItem<String?>.options(
|
||||
leading: const Icon(Icons.computer_outlined),
|
||||
title: const Text("UA"),
|
||||
subtitle: Text(value ?? appLocalizations.defaultText),
|
||||
delegate: OptionsDelegate<String?>(
|
||||
title: "UA",
|
||||
options: [
|
||||
null,
|
||||
"clash-verge/v1.6.6",
|
||||
"ClashforWindows/0.19.23",
|
||||
],
|
||||
value: value,
|
||||
onChanged: (ua) {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.globalRealUa = ua;
|
||||
},
|
||||
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final globalUa =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.globalUa));
|
||||
return ListItem<String?>.options(
|
||||
leading: const Icon(Icons.computer_outlined),
|
||||
title: const Text("UA"),
|
||||
subtitle: Text(globalUa ?? appLocalizations.defaultText),
|
||||
delegate: OptionsDelegate<String?>(
|
||||
title: "UA",
|
||||
options: [
|
||||
null,
|
||||
"clash-verge/v1.6.6",
|
||||
"ClashforWindows/0.19.23",
|
||||
],
|
||||
value: globalUa,
|
||||
onChanged: (value) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
globalUa: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
textBuilder: (ua) => ua ?? appLocalizations.defaultText,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KeepAliveIntervalItem extends StatelessWidget {
|
||||
class KeepAliveIntervalItem extends ConsumerWidget {
|
||||
const KeepAliveIntervalItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, int>(
|
||||
selector: (_, config) => config.keepAliveInterval,
|
||||
builder: (_, value, __) {
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.timer_outlined),
|
||||
title: Text(appLocalizations.keepAliveIntervalDesc),
|
||||
subtitle: Text("$value ${appLocalizations.seconds}"),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.keepAliveIntervalDesc,
|
||||
suffixText: appLocalizations.seconds,
|
||||
resetValue: "$defaultKeepAliveInterval",
|
||||
value: "$value",
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final intValue = int.parse(value);
|
||||
if (intValue <= 0) {
|
||||
throw "Invalid keepAliveInterval";
|
||||
}
|
||||
globalState.appController.clashConfig.keepAliveInterval =
|
||||
intValue;
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.keepAliveIntervalDesc,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
Widget build(BuildContext context, ref) {
|
||||
final keepAliveInterval = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.keepAliveInterval));
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.timer_outlined),
|
||||
title: Text(appLocalizations.keepAliveIntervalDesc),
|
||||
subtitle: Text("$keepAliveInterval ${appLocalizations.seconds}"),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.keepAliveIntervalDesc,
|
||||
suffixText: appLocalizations.seconds,
|
||||
resetValue: "$defaultKeepAliveInterval",
|
||||
value: "$keepAliveInterval",
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.safeRun(
|
||||
() {
|
||||
final intValue = int.parse(value);
|
||||
if (intValue <= 0) {
|
||||
throw "Invalid keepAliveInterval";
|
||||
}
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
keepAliveInterval: intValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
silence: false,
|
||||
title: appLocalizations.keepAliveIntervalDesc,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TestUrlItem extends StatelessWidget {
|
||||
class TestUrlItem extends ConsumerWidget {
|
||||
const TestUrlItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, String>(
|
||||
selector: (_, config) => config.appSetting.testUrl,
|
||||
builder: (_, value, __) {
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.timeline),
|
||||
title: Text(appLocalizations.testUrl),
|
||||
subtitle: Text(value),
|
||||
delegate: InputDelegate(
|
||||
resetValue: defaultTestUrl,
|
||||
title: appLocalizations.testUrl,
|
||||
value: value,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
if (!value.isUrl) {
|
||||
throw "Invalid url";
|
||||
}
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
testUrl: value,
|
||||
);
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.testUrl,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
),
|
||||
);
|
||||
Widget build(BuildContext context, ref) {
|
||||
final testUrl =
|
||||
ref.watch(appSettingProvider.select((state) => state.testUrl));
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.timeline),
|
||||
title: Text(appLocalizations.testUrl),
|
||||
subtitle: Text(testUrl),
|
||||
delegate: InputDelegate(
|
||||
resetValue: defaultTestUrl,
|
||||
title: appLocalizations.testUrl,
|
||||
value: testUrl,
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.safeRun(
|
||||
() {
|
||||
if (!value.isUrl) {
|
||||
throw "Invalid url";
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
testUrl: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
silence: false,
|
||||
title: appLocalizations.testUrl,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MixedPortItem extends StatelessWidget {
|
||||
class MixedPortItem extends ConsumerWidget {
|
||||
const MixedPortItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, int>(
|
||||
selector: (_, clashConfig) => clashConfig.mixedPort,
|
||||
builder: (_, value, __) {
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.adjust_outlined),
|
||||
title: Text(appLocalizations.proxyPort),
|
||||
subtitle: Text("$value"),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.proxyPort,
|
||||
value: "$value",
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
final mixedPort = int.parse(value);
|
||||
if (mixedPort < 1024 || mixedPort > 49151) {
|
||||
throw "Invalid port";
|
||||
}
|
||||
globalState.appController.clashConfig.mixedPort = mixedPort;
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.proxyPort,
|
||||
message: TextSpan(
|
||||
text: e.toString(),
|
||||
Widget build(BuildContext context, ref) {
|
||||
final mixedPort =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.mixedPort));
|
||||
return ListItem.input(
|
||||
leading: const Icon(Icons.adjust_outlined),
|
||||
title: Text(appLocalizations.proxyPort),
|
||||
subtitle: Text("$mixedPort"),
|
||||
delegate: InputDelegate(
|
||||
title: appLocalizations.proxyPort,
|
||||
value: "$mixedPort",
|
||||
onChanged: (String? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
globalState.safeRun(
|
||||
() {
|
||||
final mixedPort = int.parse(value);
|
||||
if (mixedPort < 1024 || mixedPort > 49151) {
|
||||
throw "Invalid port";
|
||||
}
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
mixedPort: mixedPort,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
resetValue: "$defaultMixedPort",
|
||||
),
|
||||
);
|
||||
},
|
||||
silence: false,
|
||||
title: appLocalizations.proxyPort,
|
||||
);
|
||||
},
|
||||
resetValue: "$defaultMixedPort",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HostsItem extends StatelessWidget {
|
||||
class HostsItem extends ConsumerWidget {
|
||||
const HostsItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final hosts =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.hosts));
|
||||
return ListItem.open(
|
||||
leading: const Icon(Icons.view_list_outlined),
|
||||
title: const Text("Hosts"),
|
||||
@@ -209,22 +203,17 @@ class HostsItem extends StatelessWidget {
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
title: "Hosts",
|
||||
widget: Selector<ClashConfig, HostsMap>(
|
||||
selector: (_, clashConfig) => clashConfig.hosts,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!const MapEquality<String, String>().equals(prev, next),
|
||||
builder: (_, hosts, ___) {
|
||||
final entries = hosts.entries;
|
||||
return ListPage(
|
||||
title: "Hosts",
|
||||
items: entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items){
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.hosts = Map.fromEntries(items);
|
||||
},
|
||||
);
|
||||
widget: ListPage(
|
||||
title: "Hosts",
|
||||
items: hosts.entries,
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
subtitleBuilder: (item) => Text(item.value),
|
||||
onChange: (items) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
hosts: Map.fromEntries(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
@@ -233,190 +222,192 @@ class HostsItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class Ipv6Item extends StatelessWidget {
|
||||
class Ipv6Item extends ConsumerWidget {
|
||||
const Ipv6Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.water_outlined),
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.ipv6 = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ipv6 =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.ipv6));
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.water_outlined),
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6Desc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
ipv6: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AllowLanItem extends StatelessWidget {
|
||||
class AllowLanItem extends ConsumerWidget {
|
||||
const AllowLanItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.allowLan,
|
||||
builder: (_, allowLan, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.device_hub),
|
||||
title: Text(appLocalizations.allowLan),
|
||||
subtitle: Text(appLocalizations.allowLanDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowLan,
|
||||
onChanged: (bool value) async {
|
||||
final clashConfig = context.read<ClashConfig>();
|
||||
clashConfig.allowLan = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final allowLan =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.allowLan));
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.device_hub),
|
||||
title: Text(appLocalizations.allowLan),
|
||||
subtitle: Text(appLocalizations.allowLanDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowLan,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
allowLan: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnifiedDelayItem extends StatelessWidget {
|
||||
class UnifiedDelayItem extends ConsumerWidget {
|
||||
const UnifiedDelayItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.unifiedDelay,
|
||||
builder: (_, unifiedDelay, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.compress_outlined),
|
||||
title: Text(appLocalizations.unifiedDelay),
|
||||
subtitle: Text(appLocalizations.unifiedDelayDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: unifiedDelay,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.unifiedDelay = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final unifiedDelay = ref
|
||||
.watch(patchClashConfigProvider.select((state) => state.unifiedDelay));
|
||||
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.compress_outlined),
|
||||
title: Text(appLocalizations.unifiedDelay),
|
||||
subtitle: Text(appLocalizations.unifiedDelayDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: unifiedDelay,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
unifiedDelay: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FindProcessItem extends StatelessWidget {
|
||||
class FindProcessItem extends ConsumerWidget {
|
||||
const FindProcessItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.findProcessMode == FindProcessMode.always,
|
||||
builder: (_, findProcess, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.polymer_outlined),
|
||||
title: Text(appLocalizations.findProcessMode),
|
||||
subtitle: Text(appLocalizations.findProcessModeDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: findProcess,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.findProcessMode =
|
||||
value ? FindProcessMode.always : FindProcessMode.off;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final findProcess = ref.watch(patchClashConfigProvider
|
||||
.select((state) => state.findProcessMode == FindProcessMode.always));
|
||||
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.polymer_outlined),
|
||||
title: Text(appLocalizations.findProcessMode),
|
||||
subtitle: Text(appLocalizations.findProcessModeDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: findProcess,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
findProcessMode:
|
||||
value ? FindProcessMode.always : FindProcessMode.off,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TcpConcurrentItem extends StatelessWidget {
|
||||
class TcpConcurrentItem extends ConsumerWidget {
|
||||
const TcpConcurrentItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tcpConcurrent,
|
||||
builder: (_, tcpConcurrent, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.double_arrow_outlined),
|
||||
title: Text(appLocalizations.tcpConcurrent),
|
||||
subtitle: Text(appLocalizations.tcpConcurrentDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: tcpConcurrent,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.tcpConcurrent = value;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final tcpConcurrent = ref
|
||||
.watch(patchClashConfigProvider.select((state) => state.tcpConcurrent));
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.double_arrow_outlined),
|
||||
title: Text(appLocalizations.tcpConcurrent),
|
||||
subtitle: Text(appLocalizations.tcpConcurrentDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: tcpConcurrent,
|
||||
onChanged: (value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
tcpConcurrent: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GeodataLoaderItem extends StatelessWidget {
|
||||
class GeodataLoaderItem extends ConsumerWidget {
|
||||
const GeodataLoaderItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.geodataLoader == geodataLoaderMemconservative,
|
||||
builder: (_, memconservative, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.memory),
|
||||
title: Text(appLocalizations.geodataLoader),
|
||||
subtitle: Text(appLocalizations.geodataLoaderDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: memconservative,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.geodataLoader =
|
||||
value ? geodataLoaderMemconservative : geodataLoaderStandard;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final isMemconservative = ref.watch(patchClashConfigProvider.select(
|
||||
(state) => state.geodataLoader == GeodataLoader.memconservative));
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.memory),
|
||||
title: Text(appLocalizations.geodataLoader),
|
||||
subtitle: Text(appLocalizations.geodataLoaderDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: isMemconservative,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
geodataLoader: value
|
||||
? GeodataLoader.memconservative
|
||||
: GeodataLoader.standard,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalControllerItem extends StatelessWidget {
|
||||
class ExternalControllerItem extends ConsumerWidget {
|
||||
const ExternalControllerItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty,
|
||||
builder: (_, hasExternalController, __) {
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.api_outlined),
|
||||
title: Text(appLocalizations.externalController),
|
||||
subtitle: Text(appLocalizations.externalControllerDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: hasExternalController,
|
||||
onChanged: (bool value) async {
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.externalController =
|
||||
value ? defaultExternalController : '';
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final hasExternalController = ref.watch(patchClashConfigProvider.select(
|
||||
(state) => state.externalController == ExternalControllerStatus.open));
|
||||
return ListItem.switchItem(
|
||||
leading: const Icon(Icons.api_outlined),
|
||||
title: Text(appLocalizations.externalController),
|
||||
subtitle: Text(appLocalizations.externalControllerDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: hasExternalController,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
externalController: value
|
||||
? ExternalControllerStatus.open
|
||||
: ExternalControllerStatus.close,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final generalItems = const [
|
||||
final generalItems = <Widget>[
|
||||
LogLevelItem(),
|
||||
UaItem(),
|
||||
KeepAliveIntervalItem(),
|
||||
if (system.isDesktop) KeepAliveIntervalItem(),
|
||||
TestUrlItem(),
|
||||
MixedPortItem(),
|
||||
HostsItem(),
|
||||
|
||||
@@ -3,208 +3,193 @@ import 'dart:io';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class VPNItem extends StatelessWidget {
|
||||
class VPNItem extends ConsumerWidget {
|
||||
const VPNItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.enable,
|
||||
builder: (_, enable, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("VPN"),
|
||||
subtitle: Text(appLocalizations.vpnEnableDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (value) async {
|
||||
final config = globalState.appController.config;
|
||||
config.vpnProps = config.vpnProps.copyWith(
|
||||
enable: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final enable =
|
||||
ref.watch(vpnSettingProvider.select((state) => state.enable));
|
||||
return ListItem.switchItem(
|
||||
title: const Text("VPN"),
|
||||
subtitle: Text(appLocalizations.vpnEnableDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (value) async {
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
enable: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TUNItem extends StatelessWidget {
|
||||
class TUNItem extends ConsumerWidget {
|
||||
const TUNItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
||||
builder: (_, enable, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.tun),
|
||||
subtitle: Text(appLocalizations.tunDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (value) async {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final enable =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.tun.enable));
|
||||
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.tun),
|
||||
subtitle: Text(appLocalizations.tunDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: enable,
|
||||
onChanged: (value) async {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.tun(
|
||||
enable: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AllowBypassItem extends StatelessWidget {
|
||||
class AllowBypassItem extends ConsumerWidget {
|
||||
const AllowBypassItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.allowBypass,
|
||||
builder: (_, allowBypass, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.allowBypass),
|
||||
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowBypass,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
allowBypass: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final allowBypass =
|
||||
ref.watch(vpnSettingProvider.select((state) => state.allowBypass));
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.allowBypass),
|
||||
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: allowBypass,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
allowBypass: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VpnSystemProxyItem extends StatelessWidget {
|
||||
class VpnSystemProxyItem extends ConsumerWidget {
|
||||
const VpnSystemProxyItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
systemProxy: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final systemProxy =
|
||||
ref.watch(vpnSettingProvider.select((state) => state.systemProxy));
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
systemProxy: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SystemProxyItem extends StatelessWidget {
|
||||
class SystemProxyItem extends ConsumerWidget {
|
||||
const SystemProxyItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.networkProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final networkProps = config.networkProps;
|
||||
config.networkProps = networkProps.copyWith(
|
||||
systemProxy: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final systemProxy =
|
||||
ref.watch(networkSettingProvider.select((state) => state.systemProxy));
|
||||
|
||||
return ListItem.switchItem(
|
||||
title: Text(appLocalizations.systemProxy),
|
||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: systemProxy,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
systemProxy: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Ipv6Item extends StatelessWidget {
|
||||
class Ipv6Item extends ConsumerWidget {
|
||||
const Ipv6Item({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.vpnProps.ipv6,
|
||||
builder: (_, ipv6, __) {
|
||||
return ListItem.switchItem(
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
final config = globalState.appController.config;
|
||||
final vpnProps = config.vpnProps;
|
||||
config.vpnProps = vpnProps.copyWith(
|
||||
ipv6: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ipv6 = ref.watch(vpnSettingProvider.select((state) => state.ipv6));
|
||||
return ListItem.switchItem(
|
||||
title: const Text("IPv6"),
|
||||
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
||||
delegate: SwitchDelegate(
|
||||
value: ipv6,
|
||||
onChanged: (bool value) async {
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
ipv6: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TunStackItem extends StatelessWidget {
|
||||
class TunStackItem extends ConsumerWidget {
|
||||
const TunStackItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, TunStack>(
|
||||
selector: (_, clashConfig) => clashConfig.tun.stack,
|
||||
builder: (_, stack, __) {
|
||||
return ListItem.options(
|
||||
title: Text(appLocalizations.stackMode),
|
||||
subtitle: Text(stack.name),
|
||||
delegate: OptionsDelegate<TunStack>(
|
||||
value: stack,
|
||||
options: TunStack.values,
|
||||
textBuilder: (value) => value.name,
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
stack: value,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final stack =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.tun.stack));
|
||||
|
||||
return ListItem.options(
|
||||
title: Text(appLocalizations.stackMode),
|
||||
subtitle: Text(stack.name),
|
||||
delegate: OptionsDelegate<TunStack>(
|
||||
value: stack,
|
||||
options: TunStack.values,
|
||||
textBuilder: (value) => value.name,
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.tun(
|
||||
stack: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
title: appLocalizations.stackMode,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
title: appLocalizations.stackMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BypassDomainItem extends StatelessWidget {
|
||||
class BypassDomainItem extends ConsumerWidget {
|
||||
const BypassDomainItem({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
_initActions(BuildContext context, WidgetRef ref) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
context.commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
@@ -218,10 +203,11 @@ class BypassDomainItem extends StatelessWidget {
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
);
|
||||
ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
bypassDomain: defaultBypassDomain,
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
@@ -233,7 +219,10 @@ class BypassDomainItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final bypassDomain =
|
||||
ref.watch(networkSettingProvider.select((state) => state.bypassDomain));
|
||||
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.bypassDomain),
|
||||
subtitle: Text(appLocalizations.bypassDomainDesc),
|
||||
@@ -241,101 +230,91 @@ class BypassDomainItem extends StatelessWidget {
|
||||
isBlur: false,
|
||||
isScaffold: true,
|
||||
title: appLocalizations.bypassDomain,
|
||||
widget: Selector<Config, List<String>>(
|
||||
selector: (_, config) => config.networkProps.bypassDomain,
|
||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
||||
builder: (context, bypassDomain, __) {
|
||||
_initActions(context);
|
||||
return ListPage(
|
||||
title: appLocalizations.bypassDomain,
|
||||
items: bypassDomain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps = config.networkProps.copyWith(
|
||||
bypassDomain: List.from(items),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
widget: Builder(builder: (context) {
|
||||
_initActions(context, ref);
|
||||
return ListPage(
|
||||
title: appLocalizations.bypassDomain,
|
||||
items: bypassDomain,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
bypassDomain: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RouteModeItem extends StatelessWidget {
|
||||
class RouteModeItem extends ConsumerWidget {
|
||||
const RouteModeItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, RouteMode>(
|
||||
selector: (_, clashConfig) => clashConfig.routeMode,
|
||||
builder: (_, value, __) {
|
||||
return ListItem<RouteMode>.options(
|
||||
title: Text(appLocalizations.routeMode),
|
||||
subtitle: Text(Intl.message("routeMode_${value.name}")),
|
||||
delegate: OptionsDelegate<RouteMode>(
|
||||
title: appLocalizations.routeMode,
|
||||
options: RouteMode.values,
|
||||
onChanged: (RouteMode? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.routeMode = value;
|
||||
},
|
||||
textBuilder: (routeMode) => Intl.message(
|
||||
"routeMode_${routeMode.name}",
|
||||
),
|
||||
value: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
Widget build(BuildContext context, ref) {
|
||||
final routeMode =
|
||||
ref.watch(networkSettingProvider.select((state) => state.routeMode));
|
||||
return ListItem<RouteMode>.options(
|
||||
title: Text(appLocalizations.routeMode),
|
||||
subtitle: Text(Intl.message("routeMode_${routeMode.name}")),
|
||||
delegate: OptionsDelegate<RouteMode>(
|
||||
title: appLocalizations.routeMode,
|
||||
options: RouteMode.values,
|
||||
onChanged: (RouteMode? value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
routeMode: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
textBuilder: (routeMode) => Intl.message(
|
||||
"routeMode_${routeMode.name}",
|
||||
),
|
||||
value: routeMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RouteAddressItem extends StatelessWidget {
|
||||
class RouteAddressItem extends ConsumerWidget {
|
||||
const RouteAddressItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.routeMode == RouteMode.config,
|
||||
builder: (_, value, child) {
|
||||
if (value) {
|
||||
return child!;
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
child: ListItem.open(
|
||||
title: Text(appLocalizations.routeAddress),
|
||||
subtitle: Text(appLocalizations.routeAddressDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
isScaffold: true,
|
||||
Widget build(BuildContext context, ref) {
|
||||
final bypassPrivate = ref.watch(networkSettingProvider
|
||||
.select((state) => state.routeMode == RouteMode.bypassPrivate));
|
||||
if (bypassPrivate) {
|
||||
return Container();
|
||||
}
|
||||
final routeAddress = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.tun.routeAddress));
|
||||
return ListItem.open(
|
||||
title: Text(appLocalizations.routeAddress),
|
||||
subtitle: Text(appLocalizations.routeAddressDesc),
|
||||
delegate: OpenDelegate(
|
||||
isBlur: false,
|
||||
isScaffold: true,
|
||||
title: appLocalizations.routeAddress,
|
||||
widget: ListPage(
|
||||
title: appLocalizations.routeAddress,
|
||||
widget: Selector<ClashConfig, List<String>>(
|
||||
selector: (_, clashConfig) => clashConfig.includeRouteAddress,
|
||||
shouldRebuild: (prev, next) =>
|
||||
!stringListEquality.equals(prev, next),
|
||||
builder: (context, routeAddress, __) {
|
||||
return ListPage(
|
||||
title: appLocalizations.routeAddress,
|
||||
items: routeAddress,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
final clashConfig = globalState.appController.clashConfig;
|
||||
clashConfig.includeRouteAddress =
|
||||
Set<String>.from(items).toList();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
items: routeAddress,
|
||||
titleBuilder: (item) => Text(item),
|
||||
onChange: (items) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.tun(
|
||||
routeAddress: List.from(items),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
extendPageWidth: 360,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -347,7 +326,8 @@ final networkItems = [
|
||||
...generateSection(
|
||||
title: "VPN",
|
||||
items: [
|
||||
const SystemProxyItem(),
|
||||
const VpnSystemProxyItem(),
|
||||
const BypassDomainItem(),
|
||||
const AllowBypassItem(),
|
||||
const Ipv6Item(),
|
||||
],
|
||||
@@ -371,10 +351,10 @@ final networkItems = [
|
||||
),
|
||||
];
|
||||
|
||||
class NetworkListView extends StatelessWidget {
|
||||
class NetworkListView extends ConsumerWidget {
|
||||
const NetworkListView({super.key});
|
||||
|
||||
_initActions(BuildContext context) {
|
||||
_initActions(BuildContext context, WidgetRef ref) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
context.commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
@@ -388,9 +368,14 @@ class NetworkListView extends StatelessWidget {
|
||||
if (res != true) {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.config.vpnProps = defaultVpnProps;
|
||||
appController.clashConfig.tun = defaultTun;
|
||||
ref.read(vpnSettingProvider.notifier).updateState(
|
||||
(state) => defaultVpnProps,
|
||||
);
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
tun: defaultTun,
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: appLocalizations.reset,
|
||||
icon: const Icon(
|
||||
@@ -402,8 +387,8 @@ class NetworkListView extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_initActions(context);
|
||||
Widget build(BuildContext context, ref) {
|
||||
_initActions(context, ref);
|
||||
return generateListView(
|
||||
networkItems,
|
||||
);
|
||||
|
||||
@@ -4,21 +4,23 @@ import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'item.dart';
|
||||
|
||||
class ConnectionsFragment extends StatefulWidget {
|
||||
class ConnectionsFragment extends ConsumerStatefulWidget {
|
||||
const ConnectionsFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
|
||||
ConsumerState<ConnectionsFragment> createState() =>
|
||||
_ConnectionsFragmentState();
|
||||
}
|
||||
|
||||
class _ConnectionsFragmentState extends State<ConnectionsFragment>
|
||||
with ViewMixin {
|
||||
class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
|
||||
with PageMixin {
|
||||
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
|
||||
const ConnectionsState(),
|
||||
);
|
||||
@@ -71,15 +73,20 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateConnections();
|
||||
}
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
initViewState();
|
||||
ref.listenManual(
|
||||
isCurrentPageProvider(
|
||||
PageLabel.connections,
|
||||
handler: (pageLabel, viewMode) =>
|
||||
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
||||
),
|
||||
(prev, next) {
|
||||
if (prev != next && next == true) {
|
||||
initPageState();
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
_updateConnections();
|
||||
}
|
||||
|
||||
_handleBlockConnection(String id) async {
|
||||
@@ -100,56 +107,44 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<AppState, bool?>(
|
||||
selector: (_, appState) =>
|
||||
appState.currentLabel == 'connections' ||
|
||||
appState.viewMode == ViewMode.mobile &&
|
||||
appState.currentLabel == "tools",
|
||||
builder: (_, isCurrent, child) {
|
||||
if (isCurrent == null || isCurrent) {
|
||||
_initActions();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: ValueListenableBuilder<ConnectionsState>(
|
||||
valueListenable: _connectionsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final connections = state.list;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullConnectionsDesc,
|
||||
);
|
||||
}
|
||||
return CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.separated(
|
||||
controller: _scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
final connection = connections[index];
|
||||
return ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.block),
|
||||
onPressed: () {
|
||||
_handleBlockConnection(connection.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: connections.length,
|
||||
),
|
||||
return ValueListenableBuilder<ConnectionsState>(
|
||||
valueListenable: _connectionsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final connections = state.list;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullConnectionsDesc,
|
||||
);
|
||||
},
|
||||
),
|
||||
}
|
||||
return CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.separated(
|
||||
controller: _scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
final connection = connections[index];
|
||||
return ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.block),
|
||||
onPressed: () {
|
||||
_handleBlockConnection(connection.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 0,
|
||||
);
|
||||
},
|
||||
itemCount: connections.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class FindProcessBuilder extends StatelessWidget {
|
||||
final Widget Function(bool value) builder;
|
||||
@@ -18,11 +19,15 @@ class FindProcessBuilder extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) =>
|
||||
clashConfig.findProcessMode == FindProcessMode.always &&
|
||||
Platform.isAndroid,
|
||||
builder: (_, value, __) {
|
||||
return Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final value = ref.watch(
|
||||
patchClashConfigProvider.select(
|
||||
(state) =>
|
||||
state.findProcessMode == FindProcessMode.always &&
|
||||
Platform.isAndroid,
|
||||
),
|
||||
);
|
||||
return builder(value);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'item.dart';
|
||||
|
||||
double _preOffset = 0;
|
||||
|
||||
class RequestsFragment extends StatefulWidget {
|
||||
class RequestsFragment extends ConsumerStatefulWidget {
|
||||
const RequestsFragment({super.key});
|
||||
|
||||
@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 =
|
||||
ValueNotifier<ConnectionsState>(const ConnectionsState());
|
||||
List<Connection> _requests = [];
|
||||
@@ -51,18 +48,32 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final appController = globalState.appController;
|
||||
final appState = appController.appState;
|
||||
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||
connections: appState.requests,
|
||||
connections: globalState.appState.requests.list,
|
||||
);
|
||||
}
|
||||
|
||||
_initActions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
initViewState();
|
||||
ref.listenManual(
|
||||
isCurrentPageProvider(
|
||||
PageLabel.requests,
|
||||
handler: (pageLabel, viewMode) =>
|
||||
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
||||
),
|
||||
(prev, next) {
|
||||
if (prev != next && next == true) {
|
||||
initPageState();
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
ref.listenManual(
|
||||
requestsProvider.select((state) => state.list),
|
||||
(prev, next) {
|
||||
if (!connectionListEquality.equals(prev, next)) {
|
||||
_requests = next;
|
||||
updateRequestsThrottler();
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,22 +122,6 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
|
||||
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() {
|
||||
throttler.call("request", () {
|
||||
final isEquality = connectionListEquality.equals(
|
||||
@@ -144,93 +139,71 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
|
||||
}, 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
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
return FindProcessBuilder(builder: (value) {
|
||||
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
|
||||
return _wrapPage(
|
||||
_wrapRequestsUpdate(
|
||||
ValueListenableBuilder<ConnectionsState>(
|
||||
valueListenable: _requestsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final connections = state.list;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullRequestsDesc,
|
||||
);
|
||||
}
|
||||
final items = connections
|
||||
.map<Widget>(
|
||||
(connection) => ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
),
|
||||
)
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: NotificationListener<ScrollEndNotification>(
|
||||
onNotification: (details) {
|
||||
_preOffset = details.metrics.pixels;
|
||||
return false;
|
||||
return ValueListenableBuilder<ConnectionsState>(
|
||||
valueListenable: _requestsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final connections = state.list;
|
||||
if (connections.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullRequestsDesc,
|
||||
);
|
||||
}
|
||||
final items = connections
|
||||
.map<Widget>(
|
||||
(connection) => ConnectionItem(
|
||||
key: Key(connection.id),
|
||||
connection: connection,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
child: CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: NextClampingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemExtentBuilder: (index, __) {
|
||||
final widget = items[index];
|
||||
if (widget.runtimeType == Divider) {
|
||||
return 0;
|
||||
}
|
||||
final measure = globalState.measure;
|
||||
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||
final connection = connections[(index / 2).floor()];
|
||||
final height = _calcCacheHeight(connection);
|
||||
return height + bodyMediumHeight + 32;
|
||||
},
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: NotificationListener<ScrollEndNotification>(
|
||||
onNotification: (details) {
|
||||
_preOffset = details.metrics.pixels;
|
||||
return false;
|
||||
},
|
||||
child: CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: NextClampingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemExtentBuilder: (index, __) {
|
||||
final widget = items[index];
|
||||
if (widget.runtimeType == Divider) {
|
||||
return 0;
|
||||
}
|
||||
final measure = globalState.measure;
|
||||
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||
final connection = connections[(index / 2).floor()];
|
||||
final height = _calcCacheHeight(connection);
|
||||
return height + bodyMediumHeight + 32;
|
||||
},
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,31 +2,42 @@ import 'dart:math';
|
||||
|
||||
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/providers/providers.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'widgets/start_button.dart';
|
||||
|
||||
class DashboardFragment extends StatefulWidget {
|
||||
class DashboardFragment extends ConsumerStatefulWidget {
|
||||
const DashboardFragment({super.key});
|
||||
|
||||
@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>();
|
||||
|
||||
_initScaffold(bool isCurrent) {
|
||||
if (!isCurrent) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.floatingActionButton = const StartButton();
|
||||
commonScaffoldState?.actions = [
|
||||
@override
|
||||
initState() {
|
||||
ref.listenManual(
|
||||
isCurrentPageProvider(PageLabel.dashboard),
|
||||
(prev, next) {
|
||||
if (prev != next && next == true) {
|
||||
initPageState();
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
return super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? get floatingActionButton => const StartButton();
|
||||
|
||||
@override
|
||||
List<Widget> get actions => [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: key.currentState!.addedChildrenNotifier,
|
||||
builder: (_, addedChildren, child) {
|
||||
@@ -67,72 +78,59 @@ class _DashboardFragmentState extends State<DashboardFragment> {
|
||||
},
|
||||
),
|
||||
];
|
||||
});
|
||||
|
||||
_handleSave(List<GridItem> girdItems, WidgetRef ref) {
|
||||
final dashboardWidgets = girdItems
|
||||
.map(
|
||||
(item) => DashboardWidget.getDashboardWidget(item),
|
||||
)
|
||||
.toList();
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActiveBuilder(
|
||||
label: "dashboard",
|
||||
builder: (isCurrent, child) {
|
||||
_initScaffold(isCurrent);
|
||||
return child!;
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16).copyWith(
|
||||
bottom: 88,
|
||||
),
|
||||
child: Selector2<AppState, Config, DashboardState>(
|
||||
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,
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
children: [
|
||||
...state.dashboardWidgets
|
||||
.where(
|
||||
(item) => item.platforms.contains(
|
||||
SupportPlatform.currentPlatform,
|
||||
),
|
||||
)
|
||||
.map(
|
||||
(item) => item.widget,
|
||||
final dashboardState = ref.watch(dashboardStateProvider);
|
||||
final columns = max(4 * ((dashboardState.viewWidth / 350).ceil()), 8);
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16).copyWith(
|
||||
bottom: 88,
|
||||
),
|
||||
child: SuperGrid(
|
||||
key: key,
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
children: [
|
||||
...dashboardState.dashboardWidgets
|
||||
.where(
|
||||
(item) => item.platforms.contains(
|
||||
SupportPlatform.currentPlatform,
|
||||
),
|
||||
)
|
||||
.map(
|
||||
(item) => item.widget,
|
||||
),
|
||||
],
|
||||
onSave: (girdItems) {
|
||||
_handleSave(girdItems, ref);
|
||||
},
|
||||
addedItemsBuilder: (girdItems) {
|
||||
return DashboardWidget.values
|
||||
.where(
|
||||
(item) =>
|
||||
!girdItems.contains(item.widget) &&
|
||||
item.platforms.contains(
|
||||
SupportPlatform.currentPlatform,
|
||||
),
|
||||
],
|
||||
onSave: (girdItems) {
|
||||
final dashboardWidgets = girdItems
|
||||
.map(
|
||||
(item) => DashboardWidget.getDashboardWidget(item),
|
||||
)
|
||||
.toList();
|
||||
final config = globalState.appController.config;
|
||||
config.appSetting = config.appSetting.copyWith(
|
||||
dashboardWidgets: dashboardWidgets,
|
||||
);
|
||||
},
|
||||
addedItemsBuilder: (girdItems) {
|
||||
return DashboardWidget.values
|
||||
.where(
|
||||
(item) =>
|
||||
!girdItems.contains(item.widget) &&
|
||||
item.platforms.contains(
|
||||
SupportPlatform.currentPlatform,
|
||||
),
|
||||
)
|
||||
.map((item) => item.widget)
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.map((item) => item.widget)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
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/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class IntranetIP extends StatelessWidget {
|
||||
const IntranetIP({super.key});
|
||||
@@ -28,15 +28,15 @@ class IntranetIP extends StatelessWidget {
|
||||
children: [
|
||||
SizedBox(
|
||||
height: globalState.measure.bodyMediumHeight + 2,
|
||||
child: Selector<AppFlowingState, String?>(
|
||||
selector: (_, appFlowingState) => appFlowingState.localIp,
|
||||
builder: (_, value, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final localIp = ref.watch(localIpProvider);
|
||||
return FadeBox(
|
||||
child: value != null
|
||||
child: localIp != null
|
||||
? TooltipText(
|
||||
text: Text(
|
||||
value.isNotEmpty
|
||||
? value
|
||||
localIp.isNotEmpty
|
||||
? localIp
|
||||
: appLocalizations.noNetwork,
|
||||
style: context.textTheme.bodyMedium?.toLight
|
||||
.adjustSize(1),
|
||||
@@ -48,7 +48,9 @@ class IntranetIP extends StatelessWidget {
|
||||
padding: EdgeInsets.all(2),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CircularProgressIndicator(),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -47,6 +47,11 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
|
||||
@override
|
||||
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(
|
||||
height: getWidgetHeight(2),
|
||||
child: CommonCard(
|
||||
@@ -90,17 +95,14 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
||||
child: WaveView(
|
||||
waveAmplitude: 12.0,
|
||||
waveFrequency: 0.35,
|
||||
waveColor: context.colorScheme.secondaryContainer
|
||||
.blendDarken(context, factor: 0.1)
|
||||
.toLighter,
|
||||
waveColor: darkenLighter,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: WaveView(
|
||||
waveAmplitude: 12.0,
|
||||
waveFrequency: 0.9,
|
||||
waveColor: context.colorScheme.secondaryContainer
|
||||
.blendDarken(context, factor: 0.1),
|
||||
waveColor: darken,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -4,10 +4,11 @@ import 'package:dio/dio.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
||||
const NetworkDetectionState(
|
||||
@@ -16,14 +17,14 @@ final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
||||
),
|
||||
);
|
||||
|
||||
class NetworkDetection extends StatefulWidget {
|
||||
class NetworkDetection extends ConsumerStatefulWidget {
|
||||
const NetworkDetection({super.key});
|
||||
|
||||
@override
|
||||
State<NetworkDetection> createState() => _NetworkDetectionState();
|
||||
ConsumerState<NetworkDetection> createState() => _NetworkDetectionState();
|
||||
}
|
||||
|
||||
class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||
bool? _preIsStart;
|
||||
Timer? _setTimeoutTimer;
|
||||
CancelToken? cancelToken;
|
||||
@@ -31,6 +32,11 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
ref.listenManual(checkIpNumProvider, (prev, next) {
|
||||
if (prev != next) {
|
||||
_startCheck();
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -47,12 +53,13 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
}
|
||||
|
||||
_checkIp() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final appFlowingState = globalState.appController.appFlowingState;
|
||||
final appState = globalState.appState;
|
||||
final isInit = appState.isInit;
|
||||
if (!isInit) return;
|
||||
final isStart = appFlowingState.isStart;
|
||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
||||
final isStart = appState.runTime != null;
|
||||
if (_preIsStart == false &&
|
||||
_preIsStart == isStart &&
|
||||
_networkDetectionState.value.ipInfo != null) return;
|
||||
_clearSetTimeoutTimer();
|
||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||
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) {
|
||||
final String code = countryCode.toUpperCase();
|
||||
if (code.length != 2) {
|
||||
@@ -141,109 +130,130 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(1),
|
||||
child: _checkIpContainer(
|
||||
ValueListenableBuilder<NetworkDetectionState>(
|
||||
valueListenable: _networkDetectionState,
|
||||
builder: (_, state, __) {
|
||||
final ipInfo = state.ipInfo;
|
||||
final isTesting = state.isTesting;
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: globalState.measure.titleMediumHeight + 16,
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
bottom: 0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
ipInfo != null
|
||||
? Text(
|
||||
_countryCodeToEmoji(
|
||||
ipInfo.countryCode,
|
||||
),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.toLight
|
||||
.copyWith(
|
||||
fontFamily: FontFamily.twEmoji.value,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.network_check,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
child: ValueListenableBuilder<NetworkDetectionState>(
|
||||
valueListenable: _networkDetectionState,
|
||||
builder: (_, state, __) {
|
||||
final ipInfo = state.ipInfo;
|
||||
final isTesting = state.isTesting;
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: globalState.measure.titleMediumHeight + 16,
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
bottom: 0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
ipInfo != null
|
||||
? Text(
|
||||
_countryCodeToEmoji(
|
||||
ipInfo.countryCode,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.networkDetection,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
.titleMedium
|
||||
?.toLight
|
||||
.copyWith(
|
||||
fontFamily: FontFamily.twEmoji.value,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.network_check,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.networkDetection,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 2),
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
globalState.showMessage(
|
||||
title: appLocalizations.tip,
|
||||
message: TextSpan(
|
||||
text: appLocalizations.detectionTip,
|
||||
),
|
||||
cancelable: false,
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
size: 16,
|
||||
Icons.info_outline,
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 0,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: globalState.measure.bodyMediumHeight + 2,
|
||||
child: FadeBox(
|
||||
child: ipInfo != null
|
||||
? TooltipText(
|
||||
text: Text(
|
||||
ipInfo.ip,
|
||||
style: context.textTheme.bodyMedium?.toLight
|
||||
.adjustSize(1),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
: FadeBox(
|
||||
child: isTesting == false && ipInfo == null
|
||||
? Text(
|
||||
"timeout",
|
||||
style: context.textTheme.bodyMedium
|
||||
?.copyWith(color: Colors.red)
|
||||
.adjustSize(1),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
Container(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 0,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: globalState.measure.bodyMediumHeight + 2,
|
||||
child: FadeBox(
|
||||
child: ipInfo != null
|
||||
? TooltipText(
|
||||
text: Text(
|
||||
ipInfo.ip,
|
||||
style: context.textTheme.bodyMedium?.toLight
|
||||
.adjustSize(1),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
: FadeBox(
|
||||
child: isTesting == false && ipInfo == null
|
||||
? Text(
|
||||
"timeout",
|
||||
style: context.textTheme.bodyMedium
|
||||
?.copyWith(color: Colors.red)
|
||||
.adjustSize(1),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: const AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class NetworkSpeed extends StatefulWidget {
|
||||
const NetworkSpeed({super.key});
|
||||
@@ -49,9 +50,9 @@ class _NetworkSpeedState extends State<NetworkSpeed> {
|
||||
label: appLocalizations.networkSpeed,
|
||||
iconData: Icons.speed_sharp,
|
||||
),
|
||||
child: Selector<AppFlowingState, List<Traffic>>(
|
||||
selector: (_, appFlowingState) => appFlowingState.traffics,
|
||||
builder: (_, traffics, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final traffics = ref.watch(trafficsProvider).list;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class OutboundMode extends StatelessWidget {
|
||||
const OutboundMode({super.key});
|
||||
@@ -15,9 +15,10 @@ class OutboundMode extends StatelessWidget {
|
||||
final height = getWidgetHeight(2);
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Selector<ClashConfig, Mode>(
|
||||
selector: (_, clashConfig) => clashConfig.mode,
|
||||
builder: (_, mode, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final mode =
|
||||
ref.watch(patchClashConfigProvider.select((state) => state.mode));
|
||||
return CommonCard(
|
||||
onPressed: () {},
|
||||
info: Info(
|
||||
|
||||
@@ -1,78 +1,76 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/fragments/config/network.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class TUNButton extends StatelessWidget {
|
||||
const TUNButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LocaleBuilder(
|
||||
builder: (_) => SizedBox(
|
||||
height: getWidgetHeight(1),
|
||||
child: CommonCard(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
body: generateListView(generateSection(
|
||||
items: [
|
||||
if (system.isDesktop) const TUNItem(),
|
||||
const TunStackItem(),
|
||||
],
|
||||
)),
|
||||
title: appLocalizations.tun,
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
label: appLocalizations.tun,
|
||||
iconData: Icons.stacked_line_chart,
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(1),
|
||||
child: CommonCard(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
body: generateListView(generateSection(
|
||||
items: [
|
||||
if (system.isDesktop) const TUNItem(),
|
||||
const TunStackItem(),
|
||||
],
|
||||
)),
|
||||
title: appLocalizations.tun,
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
label: appLocalizations.tun,
|
||||
iconData: Icons.stacked_line_chart,
|
||||
),
|
||||
child: Container(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 4,
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
),
|
||||
child: Container(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 4,
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.options,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.adjustSize(-2)
|
||||
.toLight,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.options,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.adjustSize(-2)
|
||||
.toLight,
|
||||
),
|
||||
),
|
||||
Selector<ClashConfig, bool>(
|
||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
||||
builder: (_, enable, __) {
|
||||
return Switch(
|
||||
value: enable,
|
||||
onChanged: (value) {
|
||||
final clashConfig =
|
||||
globalState.appController.clashConfig;
|
||||
clashConfig.tun = clashConfig.tun.copyWith(
|
||||
enable: value,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final enable = ref.watch(patchClashConfigProvider
|
||||
.select((state) => state.tun.enable));
|
||||
return Switch(
|
||||
value: enable,
|
||||
onChanged: (value) {
|
||||
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||
(state) => state.copyWith.tun(
|
||||
enable: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -87,67 +85,68 @@ class SystemProxyButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: getWidgetHeight(1),
|
||||
child: LocaleBuilder(
|
||||
builder: (_) => CommonCard(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
body: generateListView(
|
||||
generateSection(
|
||||
items: [
|
||||
SystemProxyItem(),
|
||||
BypassDomainItem(),
|
||||
],
|
||||
),
|
||||
child: CommonCard(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
body: generateListView(
|
||||
generateSection(
|
||||
items: [
|
||||
SystemProxyItem(),
|
||||
BypassDomainItem(),
|
||||
],
|
||||
),
|
||||
title: appLocalizations.systemProxy,
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
label: appLocalizations.systemProxy,
|
||||
iconData: Icons.shuffle,
|
||||
),
|
||||
child: Container(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 4,
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.options,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.adjustSize(-2)
|
||||
.toLight,
|
||||
),
|
||||
title: appLocalizations.systemProxy,
|
||||
);
|
||||
},
|
||||
info: Info(
|
||||
label: appLocalizations.systemProxy,
|
||||
iconData: Icons.shuffle,
|
||||
),
|
||||
child: Container(
|
||||
padding: baseInfoEdgeInsets.copyWith(
|
||||
top: 4,
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
appLocalizations.options,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.adjustSize(-2)
|
||||
.toLight,
|
||||
),
|
||||
),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) => config.networkProps.systemProxy,
|
||||
builder: (_, systemProxy, __) {
|
||||
return Switch(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
value: systemProxy,
|
||||
onChanged: (value) {
|
||||
final config = globalState.appController.config;
|
||||
config.networkProps =
|
||||
config.networkProps.copyWith(systemProxy: value);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final systemProxy = ref.watch(networkSettingProvider
|
||||
.select((state) => state.systemProxy));
|
||||
return Switch(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
value: systemProxy,
|
||||
onChanged: (value) {
|
||||
ref.read(networkSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
systemProxy: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class StartButton extends StatefulWidget {
|
||||
const StartButton({super.key});
|
||||
@@ -19,7 +20,7 @@ class _StartButtonState extends State<StartButton>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isStart = globalState.appController.appFlowingState.isStart;
|
||||
isStart = globalState.appState.runTime != null;
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
value: isStart ? 1 : 0,
|
||||
@@ -34,11 +35,10 @@ class _StartButtonState extends State<StartButton>
|
||||
}
|
||||
|
||||
handleSwitchStart() {
|
||||
final appController = globalState.appController;
|
||||
if (isStart == appController.appFlowingState.isStart) {
|
||||
if (isStart == globalState.appState.isStart) {
|
||||
isStart = !isStart;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Selector2<AppState, Config, StartButtonSelectorState>(
|
||||
selector: (_, appState, config) => StartButtonSelectorState(
|
||||
isInit: appState.isInit,
|
||||
hasProfile: config.profiles.isNotEmpty,
|
||||
),
|
||||
builder: (_, state, child) {
|
||||
return Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final state = ref.watch(startButtonSelectorStateProvider);
|
||||
if (!state.isInit || !state.hasProfile) {
|
||||
return Container();
|
||||
}
|
||||
ref.listenManual(
|
||||
runTimeProvider.select((state) => state != null),
|
||||
(prev, next) {
|
||||
if (next != isStart) {
|
||||
isStart = next;
|
||||
updateController();
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
final textWidth = globalState.measure
|
||||
.computeTextSize(
|
||||
Text(
|
||||
@@ -86,53 +79,51 @@ class _StartButtonState extends State<StartButton>
|
||||
)
|
||||
.width +
|
||||
16;
|
||||
return _updateControllerContainer(
|
||||
AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: (_, child) {
|
||||
return SizedBox(
|
||||
width: 56 + textWidth * _controller.value,
|
||||
height: 56,
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
handleSwitchStart();
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedIcon(
|
||||
icon: AnimatedIcons.play_pause,
|
||||
progress: _controller,
|
||||
),
|
||||
return AnimatedBuilder(
|
||||
animation: _controller.view,
|
||||
builder: (_, child) {
|
||||
return SizedBox(
|
||||
width: 56 + textWidth * _controller.value,
|
||||
height: 56,
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
handleSwitchStart();
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedIcon(
|
||||
icon: AnimatedIcons.play_pause,
|
||||
progress: _controller,
|
||||
),
|
||||
Expanded(
|
||||
child: ClipRect(
|
||||
child: OverflowBox(
|
||||
maxWidth: textWidth,
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ClipRect(
|
||||
child: OverflowBox(
|
||||
maxWidth: textWidth,
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Selector<AppFlowingState, int?>(
|
||||
selector: (_, appFlowingState) => appFlowingState.runTime,
|
||||
builder: (_, int? value, __) {
|
||||
final text = other.getTimeText(value);
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final runTime = ref.watch(runTimeProvider);
|
||||
final text = other.getTimeText(runTime);
|
||||
return Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.titleMedium?.toSoftBold,
|
||||
|
||||
@@ -2,10 +2,11 @@ import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/common/common.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/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class TrafficUsage extends StatelessWidget {
|
||||
const TrafficUsage({super.key});
|
||||
@@ -62,9 +63,9 @@ class TrafficUsage extends StatelessWidget {
|
||||
iconData: Icons.data_saver_off,
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Selector<AppFlowingState, Traffic>(
|
||||
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
|
||||
builder: (_, totalTraffic, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final totalTraffic = ref.watch(totalTrafficProvider);
|
||||
final upTotalTrafficValue = totalTraffic.up;
|
||||
final downTotalTrafficValue = totalTraffic.down;
|
||||
return Padding(
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/card.dart';
|
||||
import 'package:fl_clash/widgets/list.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
extension IntlExt on Intl {
|
||||
static actionMessage(String messageText) =>
|
||||
@@ -38,29 +39,20 @@ class HotKeyFragment extends StatelessWidget {
|
||||
itemCount: HotAction.values.length,
|
||||
itemBuilder: (_, index) {
|
||||
final hotAction = HotAction.values[index];
|
||||
return Selector<Config, HotKeyAction>(
|
||||
selector: (_, config) {
|
||||
final index = config.hotKeyActions.indexWhere(
|
||||
(item) => item.action == hotAction,
|
||||
);
|
||||
return index != -1
|
||||
? config.hotKeyActions[index]
|
||||
: HotKeyAction(
|
||||
action: hotAction,
|
||||
);
|
||||
},
|
||||
builder: (_, value, __) {
|
||||
return Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final hotKeyAction = ref.watch(getHotKeyActionProvider(hotAction));
|
||||
return ListItem(
|
||||
title: Text(IntlExt.actionMessage(hotAction.name)),
|
||||
subtitle: Text(
|
||||
getSubtitle(value),
|
||||
getSubtitle(hotKeyAction),
|
||||
style: context.textTheme.bodyMedium
|
||||
?.copyWith(color: context.colorScheme.primary),
|
||||
),
|
||||
onTap: () {
|
||||
globalState.showCommonDialog(
|
||||
child: HotKeyRecorder(
|
||||
hotKeyAction: value,
|
||||
hotKeyAction: hotKeyAction,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -121,8 +113,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
|
||||
|
||||
_handleRemove() {
|
||||
Navigator.of(context).pop();
|
||||
final config = globalState.appController.config;
|
||||
config.updateOrAddHotKeyAction(
|
||||
globalState.appController.updateOrAddHotKeyAction(
|
||||
hotKeyActionNotifier.value.copyWith(
|
||||
modifiers: {},
|
||||
key: null,
|
||||
@@ -132,7 +123,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
|
||||
|
||||
_handleConfirm() {
|
||||
Navigator.of(context).pop();
|
||||
final config = globalState.appController.config;
|
||||
final config = globalState.config;
|
||||
final currentHotkeyAction = hotKeyActionNotifier.value;
|
||||
if (currentHotkeyAction.key == null ||
|
||||
currentHotkeyAction.modifiers.isEmpty) {
|
||||
@@ -158,7 +149,7 @@ class _HotKeyRecorderState extends State<HotKeyRecorder> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
config.updateOrAddHotKeyAction(
|
||||
globalState.appController.updateOrAddHotKeyAction(
|
||||
currentHotkeyAction,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../models/models.dart';
|
||||
import '../widgets/widgets.dart';
|
||||
|
||||
double _preOffset = 0;
|
||||
|
||||
class LogsFragment extends StatefulWidget {
|
||||
class LogsFragment extends ConsumerStatefulWidget {
|
||||
const LogsFragment({super.key});
|
||||
|
||||
@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 _scrollController = ScrollController(
|
||||
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
|
||||
@@ -31,10 +30,34 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final appController = globalState.appController;
|
||||
final appFlowingState = appController.appFlowingState;
|
||||
_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) {
|
||||
final cacheHeight = _cacheDynamicHeightMap.get(text);
|
||||
if (cacheHeight != null) {
|
||||
@@ -140,98 +157,66 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
|
||||
}, 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
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
_handleTryClearCache(constraints.maxWidth - 40);
|
||||
return Selector<AppState, bool?>(
|
||||
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,
|
||||
child: ValueListenableBuilder<LogsState>(
|
||||
valueListenable: _logsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final logs = state.list;
|
||||
if (logs.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullLogsDesc,
|
||||
);
|
||||
}
|
||||
final items = logs
|
||||
.map<Widget>(
|
||||
(log) => LogItem(
|
||||
key: Key(log.dateTime.toString()),
|
||||
log: log,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
),
|
||||
)
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return NotificationListener<ScrollEndNotification>(
|
||||
onNotification: (details) {
|
||||
_preOffset = details.metrics.pixels;
|
||||
return false;
|
||||
},
|
||||
child: CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: NextClampingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
itemExtentBuilder: (index, __) {
|
||||
final item = items[index];
|
||||
if (item.runtimeType == Divider) {
|
||||
return 0;
|
||||
}
|
||||
final log = logs[(index / 2).floor()];
|
||||
return _getItemHeight(log);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ValueListenableBuilder<LogsState>(
|
||||
valueListenable: _logsStateNotifier,
|
||||
builder: (_, state, __) {
|
||||
final logs = state.list;
|
||||
if (logs.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullLogsDesc,
|
||||
);
|
||||
}
|
||||
final items = logs
|
||||
.map<Widget>(
|
||||
(log) => LogItem(
|
||||
key: Key(log.dateTime.toString()),
|
||||
log: log,
|
||||
onClick: (value) {
|
||||
context.commonScaffoldState?.addKeyword(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
)
|
||||
.separated(
|
||||
const Divider(
|
||||
height: 0,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return NotificationListener<ScrollEndNotification>(
|
||||
onNotification: (details) {
|
||||
_preOffset = details.metrics.pixels;
|
||||
return false;
|
||||
},
|
||||
),
|
||||
),
|
||||
child: CommonScrollBar(
|
||||
controller: _scrollController,
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: NextClampingScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemBuilder: (_, index) {
|
||||
return items[index];
|
||||
},
|
||||
itemExtentBuilder: (index, __) {
|
||||
final item = items[index];
|
||||
if (item.runtimeType == Divider) {
|
||||
return 0;
|
||||
}
|
||||
final log = logs[(index / 2).floor()];
|
||||
return _getItemHeight(log);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
54
lib/fragments/profiles/custom_profile.dart
Normal file
54
lib/fragments/profiles/custom_profile.dart
Normal 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: "自定义",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'add_profile.dart';
|
||||
|
||||
@@ -19,7 +19,7 @@ class ProfilesFragment extends StatefulWidget {
|
||||
State<ProfilesFragment> createState() => _ProfilesFragmentState();
|
||||
}
|
||||
|
||||
class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
|
||||
Function? applyConfigDebounce;
|
||||
|
||||
_handleShowAddExtendPage() {
|
||||
@@ -33,21 +33,19 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
}
|
||||
|
||||
_updateProfiles() async {
|
||||
final appController = globalState.appController;
|
||||
final config = appController.config;
|
||||
final profiles = appController.config.profiles;
|
||||
final profiles = globalState.config.profiles;
|
||||
final messages = [];
|
||||
final updateProfiles = profiles.map<Future>(
|
||||
(profile) async {
|
||||
if (profile.type == ProfileType.file) return;
|
||||
config.setProfile(
|
||||
globalState.appController.setProfile(
|
||||
profile.copyWith(isUpdating: true),
|
||||
);
|
||||
try {
|
||||
await appController.updateProfile(profile);
|
||||
await globalState.appController.updateProfile(profile);
|
||||
} catch (e) {
|
||||
messages.add("${profile.label ?? profile.id}: $e \n");
|
||||
config.setProfile(
|
||||
globalState.appController.setProfile(
|
||||
profile.copyWith(
|
||||
isUpdating: false,
|
||||
),
|
||||
@@ -70,96 +68,90 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
||||
}
|
||||
}
|
||||
|
||||
_initScaffold() {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) {
|
||||
if (!mounted) return;
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
commonScaffoldState?.actions = [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_updateProfiles();
|
||||
},
|
||||
icon: const Icon(Icons.sync),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final profiles = globalState.appController.config.profiles;
|
||||
showSheet(
|
||||
title: appLocalizations.profilesSort,
|
||||
context: context,
|
||||
body: SizedBox(
|
||||
height: 400,
|
||||
child: ReorderableProfiles(profiles: profiles),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.sort),
|
||||
iconSize: 26,
|
||||
),
|
||||
];
|
||||
commonScaffoldState?.floatingActionButton = FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: _handleShowAddExtendPage,
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@override
|
||||
List<Widget> get actions => [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_updateProfiles();
|
||||
},
|
||||
icon: const Icon(Icons.sync),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final profiles = globalState.config.profiles;
|
||||
showSheet(
|
||||
title: appLocalizations.profilesSort,
|
||||
context: context,
|
||||
body: SizedBox(
|
||||
height: 400,
|
||||
child: ReorderableProfiles(profiles: profiles),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.sort),
|
||||
iconSize: 26,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget? get floatingActionButton => FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: _handleShowAddExtendPage,
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActiveBuilder(
|
||||
label: "profiles",
|
||||
builder: (isCurrent, child) {
|
||||
if (isCurrent) {
|
||||
_initScaffold();
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: Selector2<AppState, Config, ProfilesSelectorState>(
|
||||
selector: (_, appState, config) => ProfilesSelectorState(
|
||||
profiles: config.profiles,
|
||||
currentProfileId: config.currentProfileId,
|
||||
columns: other.getProfilesColumns(appState.viewWidth),
|
||||
),
|
||||
builder: (context, state, child) {
|
||||
if (state.profiles.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullProfileDesc,
|
||||
);
|
||||
}
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 88,
|
||||
),
|
||||
child: Grid(
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
crossAxisCount: state.columns,
|
||||
children: [
|
||||
for (int i = 0; i < state.profiles.length; i++)
|
||||
GridItem(
|
||||
child: ProfileItem(
|
||||
key: Key(state.profiles[i].id),
|
||||
profile: state.profiles[i],
|
||||
groupValue: state.currentProfileId,
|
||||
onChanged: globalState.appController.changeProfile,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return Consumer(
|
||||
builder: (_, ref, __) {
|
||||
ref.listenManual(
|
||||
isCurrentPageProvider(PageLabel.profiles),
|
||||
(prev, next) {
|
||||
if (prev != next && next == true) {
|
||||
initPageState();
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
final profilesSelectorState = ref.watch(profilesSelectorStateProvider);
|
||||
if (profilesSelectorState.profiles.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullProfileDesc,
|
||||
);
|
||||
},
|
||||
),
|
||||
}
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 88,
|
||||
),
|
||||
child: Grid(
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
crossAxisCount: profilesSelectorState.columns,
|
||||
children: [
|
||||
for (int i = 0; i < profilesSelectorState.profiles.length; i++)
|
||||
GridItem(
|
||||
child: ProfileItem(
|
||||
key: Key(profilesSelectorState.profiles[i].id),
|
||||
profile: profilesSelectorState.profiles[i],
|
||||
groupValue: profilesSelectorState.currentProfileId,
|
||||
onChanged: (profileId) {
|
||||
ref.read(currentProfileIdProvider.notifier).value =
|
||||
profileId;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -195,18 +187,17 @@ class ProfileItem extends StatelessWidget {
|
||||
|
||||
Future updateProfile() async {
|
||||
final appController = globalState.appController;
|
||||
final config = appController.config;
|
||||
if (profile.type == ProfileType.file) return;
|
||||
await globalState.safeRun(silence: false, () async {
|
||||
try {
|
||||
config.setProfile(
|
||||
appController.setProfile(
|
||||
profile.copyWith(
|
||||
isUpdating: true,
|
||||
),
|
||||
);
|
||||
await appController.updateProfile(profile);
|
||||
} catch (e) {
|
||||
config.setProfile(
|
||||
appController.setProfile(
|
||||
profile.copyWith(
|
||||
isUpdating: false,
|
||||
),
|
||||
@@ -256,16 +247,16 @@ class ProfileItem extends StatelessWidget {
|
||||
];
|
||||
}
|
||||
|
||||
_handleCopyLink(BuildContext context) async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: profile.url,
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.showNotifier(appLocalizations.copySuccess);
|
||||
}
|
||||
}
|
||||
// _handleCopyLink(BuildContext context) async {
|
||||
// await Clipboard.setData(
|
||||
// ClipboardData(
|
||||
// text: profile.url,
|
||||
// ),
|
||||
// );
|
||||
// if (context.mounted) {
|
||||
// context.showNotifier(appLocalizations.copySuccess);
|
||||
// }
|
||||
// }
|
||||
|
||||
_handleExportFile(BuildContext context) async {
|
||||
final commonScaffoldState = context.commonScaffoldState;
|
||||
@@ -286,6 +277,15 @@ class ProfileItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// _handlePushCustomPage(BuildContext context, String id) {
|
||||
// BaseNavigator.push(
|
||||
// context,
|
||||
// CustomProfile(
|
||||
// profileId: id,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final key = GlobalKey<CommonPopupBoxState>();
|
||||
@@ -326,14 +326,21 @@ class ProfileItem extends StatelessWidget {
|
||||
_handleUpdateProfile();
|
||||
},
|
||||
),
|
||||
ActionItemData(
|
||||
icon: Icons.copy,
|
||||
label: appLocalizations.copyLink,
|
||||
onPressed: () {
|
||||
_handleCopyLink(context);
|
||||
},
|
||||
),
|
||||
// ActionItemData(
|
||||
// icon: Icons.copy,
|
||||
// label: appLocalizations.copyLink,
|
||||
// onPressed: () {
|
||||
// _handleCopyLink(context);
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
// ActionItemData(
|
||||
// icon: Icons.extension_outlined,
|
||||
// label: "自定义",
|
||||
// onPressed: () {
|
||||
// _handlePushCustomPage(context, profile.id);
|
||||
// },
|
||||
// ),
|
||||
ActionItemData(
|
||||
icon: Icons.file_copy_outlined,
|
||||
label: appLocalizations.exportFile,
|
||||
@@ -499,7 +506,7 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
|
||||
child: FilledButton.tonal(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
globalState.appController.config.profiles = profiles;
|
||||
globalState.appController.setProfiles(profiles);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
|
||||
@@ -2,10 +2,11 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/fragments/proxies/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class ProxyCard extends StatelessWidget {
|
||||
final String groupName;
|
||||
@@ -35,30 +36,28 @@ class ProxyCard extends StatelessWidget {
|
||||
Widget _buildDelayText() {
|
||||
return SizedBox(
|
||||
height: measure.labelSmallHeight,
|
||||
child: Selector<AppState, int?>(
|
||||
selector: (context, appState) =>
|
||||
globalState.appController.getDelay(proxy.name,testUrl),
|
||||
builder: (context, delay, __) {
|
||||
return FadeBox(
|
||||
child: Builder(
|
||||
builder: (_) {
|
||||
if (delay == 0 || delay == null) {
|
||||
return SizedBox(
|
||||
height: measure.labelSmallHeight,
|
||||
width: measure.labelSmallHeight,
|
||||
child: delay == 0
|
||||
? const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.bolt),
|
||||
iconSize: globalState.measure.labelSmallHeight,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _handleTestCurrentDelay,
|
||||
),
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
child: Consumer(
|
||||
builder: (context, ref, __) {
|
||||
final delay = ref.watch(getDelayProvider(
|
||||
proxyName: proxy.name,
|
||||
testUrl: testUrl,
|
||||
));
|
||||
return delay == 0 || delay == null
|
||||
? SizedBox(
|
||||
height: measure.labelSmallHeight,
|
||||
width: measure.labelSmallHeight,
|
||||
child: delay == 0
|
||||
? const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.bolt),
|
||||
iconSize: globalState.measure.labelSmallHeight,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _handleTestCurrentDelay,
|
||||
),
|
||||
)
|
||||
: GestureDetector(
|
||||
onTap: _handleTestCurrentDelay,
|
||||
child: Text(
|
||||
delay > 0 ? '$delay ms' : "Timeout",
|
||||
@@ -70,9 +69,6 @@ class ProxyCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -102,18 +98,17 @@ class ProxyCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
_changeProxy(BuildContext context) async {
|
||||
final appController = globalState.appController;
|
||||
final isURLTestOrFallback = groupType.isURLTestOrFallback;
|
||||
_changeProxy(WidgetRef ref) async {
|
||||
final isComputedSelected = groupType.isComputedSelected;
|
||||
final isSelector = groupType == GroupType.Selector;
|
||||
if (isURLTestOrFallback || isSelector) {
|
||||
final currentProxyName =
|
||||
appController.config.currentSelectedMap[groupName];
|
||||
final nextProxyName = switch (isURLTestOrFallback) {
|
||||
if (isComputedSelected || isSelector) {
|
||||
final currentProxyName = ref.read(getProxyNameProvider(groupName));
|
||||
final nextProxyName = switch (isComputedSelected) {
|
||||
true => currentProxyName == proxy.name ? "" : proxy.name,
|
||||
false => proxy.name,
|
||||
};
|
||||
appController.config.updateCurrentSelectedMap(
|
||||
final appController = globalState.appController;
|
||||
appController.updateCurrentSelectedMap(
|
||||
groupName,
|
||||
nextProxyName,
|
||||
);
|
||||
@@ -130,108 +125,136 @@ class ProxyCard extends StatelessWidget {
|
||||
final measure = globalState.measure;
|
||||
final delayText = _buildDelayText();
|
||||
final proxyNameText = _buildProxyNameText(context);
|
||||
return currentSelectedProxyNameBuilder(
|
||||
groupName: groupName,
|
||||
builder: (currentGroupName) {
|
||||
return Stack(
|
||||
children: [
|
||||
CommonCard(
|
||||
return Stack(
|
||||
children: [
|
||||
Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final selectedProxyName =
|
||||
ref.watch(getSelectedProxyNameProvider(groupName));
|
||||
return CommonCard(
|
||||
key: key,
|
||||
enterAnimated: true,
|
||||
onPressed: () {
|
||||
_changeProxy(context);
|
||||
_changeProxy(ref);
|
||||
},
|
||||
isSelected: currentGroupName == proxy.name,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
proxyNameText,
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
isSelected: selectedProxyName == proxy.name,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
proxyNameText,
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (type == ProxyCardType.expand) ...[
|
||||
SizedBox(
|
||||
height: measure.bodySmallHeight,
|
||||
child: _ProxyDesc(
|
||||
proxy: proxy,
|
||||
),
|
||||
if (type == ProxyCardType.expand) ...[
|
||||
SizedBox(
|
||||
height: measure.bodySmallHeight,
|
||||
child: Selector<AppState, String>(
|
||||
selector: (context, appState) => appState.getDesc(
|
||||
proxy.type,
|
||||
proxy.name,
|
||||
),
|
||||
builder: (_, desc, __) {
|
||||
return EmojiText(
|
||||
desc,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 6,
|
||||
),
|
||||
delayText,
|
||||
] else
|
||||
SizedBox(
|
||||
height: measure.bodySmallHeight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
proxy.type,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color:
|
||||
context.textTheme.bodySmall?.color?.toLight,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
delayText,
|
||||
] else
|
||||
SizedBox(
|
||||
height: measure.bodySmallHeight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TooltipText(
|
||||
text: Text(
|
||||
proxy.type,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: context
|
||||
.textTheme.bodySmall?.color?.toLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
delayText,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (groupType.isURLTestOrFallback)
|
||||
Selector<Config, String>(
|
||||
selector: (_, config) {
|
||||
final selectedProxyName =
|
||||
config.currentSelectedMap[groupName];
|
||||
return selectedProxyName ?? '';
|
||||
},
|
||||
builder: (_, value, child) {
|
||||
if (value != proxy.name) return Container();
|
||||
return child!;
|
||||
},
|
||||
child: Positioned.fill(
|
||||
child: Container(
|
||||
alignment: Alignment.topRight,
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
child: const SelectIcon(),
|
||||
delayText,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (groupType.isComputedSelected)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: _ProxyComputedMark(
|
||||
groupName: groupName,
|
||||
proxy: proxy,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProxyDesc extends ConsumerWidget {
|
||||
final Proxy proxy;
|
||||
|
||||
const _ProxyDesc({
|
||||
required this.proxy,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final desc = ref.watch(
|
||||
getProxyDescProvider(proxy),
|
||||
);
|
||||
return EmojiText(
|
||||
desc,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.textTheme.bodySmall?.color?.toLight,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProxyComputedMark extends ConsumerWidget {
|
||||
final String groupName;
|
||||
final Proxy proxy;
|
||||
|
||||
const _ProxyComputedMark({
|
||||
required this.groupName,
|
||||
required this.proxy,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final proxyName = ref.watch(
|
||||
getProxyNameProvider(groupName),
|
||||
);
|
||||
if (proxyName != proxy.name) {
|
||||
return SizedBox();
|
||||
}
|
||||
return Container(
|
||||
alignment: Alignment.topRight,
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
child: const SelectIcon(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,6 @@ 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: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 {
|
||||
final measure = globalState.measure;
|
||||
@@ -30,9 +12,9 @@ double get listHeaderHeight {
|
||||
double getItemHeight(ProxyCardType proxyCardType) {
|
||||
final measure = globalState.measure;
|
||||
final baseHeight =
|
||||
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8 + 4;
|
||||
16 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8 + 4;
|
||||
return switch (proxyCardType) {
|
||||
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
|
||||
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 6,
|
||||
ProxyCardType.shrink => baseHeight,
|
||||
ProxyCardType.min => baseHeight - measure.bodyMediumHeight,
|
||||
};
|
||||
@@ -40,16 +22,16 @@ double getItemHeight(ProxyCardType proxyCardType) {
|
||||
|
||||
proxyDelayTest(Proxy proxy, [String? testUrl]) async {
|
||||
final appController = globalState.appController;
|
||||
final proxyName = appController.appState.getRealProxyName(proxy.name);
|
||||
final proxyName = appController.getRealProxyName(proxy.name);
|
||||
final url = appController.getRealTestUrl(testUrl);
|
||||
globalState.appController.setDelay(
|
||||
appController.setDelay(
|
||||
Delay(
|
||||
url: url,
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(
|
||||
appController.setDelay(
|
||||
await clashCore.getDelay(
|
||||
url,
|
||||
proxyName,
|
||||
@@ -60,21 +42,21 @@ proxyDelayTest(Proxy proxy, [String? testUrl]) async {
|
||||
delayTest(List<Proxy> proxies, [String? testUrl]) async {
|
||||
final appController = globalState.appController;
|
||||
final proxyNames = proxies
|
||||
.map((proxy) => appController.appState.getRealProxyName(proxy.name))
|
||||
.map((proxy) => appController.getRealProxyName(proxy.name))
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
final url = appController.getRealTestUrl(testUrl);
|
||||
|
||||
final delayProxies = proxyNames.map<Future>((proxyName) async {
|
||||
globalState.appController.setDelay(
|
||||
appController.setDelay(
|
||||
Delay(
|
||||
url: url,
|
||||
name: proxyName,
|
||||
value: 0,
|
||||
),
|
||||
);
|
||||
globalState.appController.setDelay(
|
||||
appController.setDelay(
|
||||
await clashCore.getDelay(
|
||||
url,
|
||||
proxyName,
|
||||
@@ -86,7 +68,7 @@ delayTest(List<Proxy> proxies, [String? testUrl]) async {
|
||||
for (final batchDelayProxies in batchesDelayProxies) {
|
||||
await Future.wait(batchDelayProxies);
|
||||
}
|
||||
appController.appState.sortNum++;
|
||||
appController.addSortNum();
|
||||
}
|
||||
|
||||
double getScrollToSelectedOffset({
|
||||
@@ -94,14 +76,11 @@ double getScrollToSelectedOffset({
|
||||
required List<Proxy> proxies,
|
||||
}) {
|
||||
final appController = globalState.appController;
|
||||
final columns = other.getProxiesColumns(
|
||||
appController.appState.viewWidth,
|
||||
appController.config.proxiesStyle.layout,
|
||||
);
|
||||
final proxyCardType = appController.config.proxiesStyle.cardType;
|
||||
final selectedName = appController.getCurrentSelectedName(groupName);
|
||||
final columns = appController.getProxiesColumns();
|
||||
final proxyCardType = globalState.config.proxiesStyle.cardType;
|
||||
final selectedProxyName = appController.getSelectedProxyName(groupName);
|
||||
final findSelectedIndex = proxies.indexWhere(
|
||||
(proxy) => proxy.name == selectedName,
|
||||
(proxy) => proxy.name == selectedProxyName,
|
||||
);
|
||||
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
|
||||
final rows = (selectedIndex / columns).floor();
|
||||
|
||||
@@ -3,10 +3,13 @@ import 'dart:math';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/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/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'card.dart';
|
||||
import 'common.dart';
|
||||
@@ -78,7 +81,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
} else {
|
||||
tempUnfoldSet.add(groupName);
|
||||
}
|
||||
globalState.appController.config.updateCurrentUnfoldSet(
|
||||
globalState.appController.updateCurrentUnfoldSet(
|
||||
tempUnfoldSet,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -105,7 +108,8 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
return itemHeightList;
|
||||
}
|
||||
|
||||
List<Widget> _buildItems({
|
||||
List<Widget> _buildItems(
|
||||
WidgetRef ref, {
|
||||
required List<String> groupNames,
|
||||
required int columns,
|
||||
required Set<String> currentUnfoldSet,
|
||||
@@ -115,7 +119,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
final GroupNameProxiesMap groupNameProxiesMap = {};
|
||||
for (final groupName in groupNames) {
|
||||
final group =
|
||||
globalState.appController.appState.getGroupWithName(groupName)!;
|
||||
ref.read(groupsProvider.select((state) => state.getGroup(groupName)));
|
||||
if (group == null) {
|
||||
continue;
|
||||
}
|
||||
final isExpand = currentUnfoldSet.contains(groupName);
|
||||
items.addAll([
|
||||
ListHeader(
|
||||
@@ -186,16 +193,21 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
return items;
|
||||
}
|
||||
|
||||
_buildHeader({
|
||||
_buildHeader(
|
||||
WidgetRef ref, {
|
||||
required String groupName,
|
||||
required Set<String> currentUnfoldSet,
|
||||
}) {
|
||||
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);
|
||||
return SizedBox(
|
||||
height: listHeaderHeight,
|
||||
child: ListHeader(
|
||||
enterAnimated: false,
|
||||
onScrollToSelected: _scrollToGroupSelected,
|
||||
key: Key(groupName),
|
||||
isExpand: isExpand,
|
||||
@@ -212,7 +224,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
return;
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
final currentGroups = appController.appState.currentGroups;
|
||||
final currentGroups = appController.getCurrentGroups();
|
||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
||||
final findIndex = groupNames.indexWhere((item) => item == groupName);
|
||||
final index = findIndex != -1 ? findIndex : 0;
|
||||
@@ -235,38 +247,16 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector2<AppState, Config, ProxiesListSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
final currentGroups = appState.currentGroups;
|
||||
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, __) {
|
||||
return Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final state = ref.watch(proxiesListSelectorStateProvider);
|
||||
if (state.groupNames.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullProxies,
|
||||
);
|
||||
}
|
||||
final items = _buildItems(
|
||||
ref,
|
||||
groupNames: state.groupNames,
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
columns: state.columns,
|
||||
@@ -318,6 +308,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
||||
bottom: 8,
|
||||
),
|
||||
child: _buildHeader(
|
||||
ref,
|
||||
groupName: state.groupNames[index],
|
||||
currentUnfoldSet: state.currentUnfoldSet,
|
||||
),
|
||||
@@ -343,8 +334,11 @@ class ListHeader extends StatefulWidget {
|
||||
final Function(String groupName) onScrollToSelected;
|
||||
final bool isExpand;
|
||||
|
||||
final bool enterAnimated;
|
||||
|
||||
const ListHeader({
|
||||
super.key,
|
||||
this.enterAnimated = true,
|
||||
required this.group,
|
||||
required this.onChange,
|
||||
required this.onScrollToSelected,
|
||||
@@ -417,57 +411,53 @@ class _ListHeaderState extends State<ListHeader>
|
||||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
return Selector<Config, ProxiesIconStyle>(
|
||||
selector: (_, config) => config.proxiesStyle.iconStyle,
|
||||
builder: (_, iconStyle, child) {
|
||||
return Selector<Config, String>(
|
||||
selector: (_, config) {
|
||||
final iconMapEntryList =
|
||||
config.proxiesStyle.iconMap.entries.toList();
|
||||
final index = iconMapEntryList.indexWhere((item) {
|
||||
try {
|
||||
return RegExp(item.key).hasMatch(groupName);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (index != -1) {
|
||||
return iconMapEntryList[index].value;
|
||||
return Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final iconStyle = ref.watch(
|
||||
proxiesStyleSettingProvider.select((state) => state.iconStyle));
|
||||
final icon = ref.watch(proxiesStyleSettingProvider.select((state) {
|
||||
final iconMapEntryList = state.iconMap.entries.toList();
|
||||
final index = iconMapEntryList.indexWhere((item) {
|
||||
try {
|
||||
return RegExp(item.key).hasMatch(groupName);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
return icon;
|
||||
},
|
||||
builder: (_, icon, __) {
|
||||
return switch (iconStyle) {
|
||||
ProxiesIconStyle.standard => Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
margin: const EdgeInsets.only(
|
||||
right: 16,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: CommonTargetIcon(
|
||||
src: icon,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
ProxiesIconStyle.icon => Container(
|
||||
margin: const EdgeInsets.only(
|
||||
right: 16,
|
||||
),
|
||||
child: CommonTargetIcon(
|
||||
src: icon,
|
||||
size: 42,
|
||||
),
|
||||
),
|
||||
ProxiesIconStyle.none => Container(),
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
if (index != -1) {
|
||||
return iconMapEntryList[index].value;
|
||||
}
|
||||
return this.icon;
|
||||
}));
|
||||
return switch (iconStyle) {
|
||||
ProxiesIconStyle.standard => Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
margin: const EdgeInsets.only(
|
||||
right: 16,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: CommonTargetIcon(
|
||||
src: icon,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
ProxiesIconStyle.icon => Container(
|
||||
margin: const EdgeInsets.only(
|
||||
right: 16,
|
||||
),
|
||||
child: CommonTargetIcon(
|
||||
src: icon,
|
||||
size: 42,
|
||||
),
|
||||
),
|
||||
ProxiesIconStyle.none => Container(),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -475,13 +465,15 @@ class _ListHeaderState extends State<ListHeader>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonCard(
|
||||
enterAnimated: widget.enterAnimated,
|
||||
key: widget.key,
|
||||
borderSide: WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
context.colorScheme.surfaceContainer,
|
||||
),
|
||||
radius: 14,
|
||||
type: CommonCardType.filled,
|
||||
child: Container(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
@@ -518,9 +510,13 @@ class _ListHeaderState extends State<ListHeader>
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: currentSelectedProxyNameBuilder(
|
||||
groupName: groupName,
|
||||
builder: (currentGroupName) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final proxyName = ref
|
||||
.watch(getSelectedProxyNameProvider(
|
||||
groupName,
|
||||
))
|
||||
.getSafeValue("");
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment:
|
||||
@@ -528,12 +524,12 @@ class _ListHeaderState extends State<ListHeader>
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (currentGroupName.isNotEmpty) ...[
|
||||
if (proxyName.isNotEmpty) ...[
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: EmojiText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
" · $currentGroupName",
|
||||
" · $proxyName",
|
||||
style: context.textTheme
|
||||
.labelMedium?.toLight,
|
||||
),
|
||||
|
||||
@@ -3,25 +3,25 @@ import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/app.dart';
|
||||
import 'package:fl_clash/models/core.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
typedef UpdatingMap = Map<String, bool>;
|
||||
|
||||
class Providers extends StatefulWidget {
|
||||
const Providers({
|
||||
class ProvidersView extends ConsumerStatefulWidget {
|
||||
const ProvidersView({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Providers> createState() => _ProvidersState();
|
||||
ConsumerState<ProvidersView> createState() => _ProvidersViewState();
|
||||
}
|
||||
|
||||
class _ProvidersState extends State<Providers> {
|
||||
class _ProvidersViewState extends ConsumerState<ProvidersView> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -43,12 +43,12 @@ class _ProvidersState extends State<Providers> {
|
||||
}
|
||||
|
||||
_updateProviders() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final providers = globalState.appController.appState.providers;
|
||||
final providers = ref.read(providersProvider);
|
||||
final providersNotifier = ref.read(providersProvider.notifier);
|
||||
final messages = [];
|
||||
final updateProviders = providers.map<Future>(
|
||||
(provider) async {
|
||||
appState.setProvider(
|
||||
providersNotifier.setProvider(
|
||||
provider.copyWith(isUpdating: true),
|
||||
);
|
||||
final message = await clashCore.updateExternalProvider(
|
||||
@@ -57,7 +57,7 @@ class _ProvidersState extends State<Providers> {
|
||||
if (message.isNotEmpty) {
|
||||
messages.add("${provider.name}: $message \n");
|
||||
}
|
||||
appState.setProvider(
|
||||
providersNotifier.setProvider(
|
||||
await clashCore.getExternalProvider(provider.name),
|
||||
);
|
||||
},
|
||||
@@ -83,35 +83,29 @@ class _ProvidersState extends State<Providers> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<AppState, List<ExternalProvider>>(
|
||||
selector: (_, appState) => appState.providers,
|
||||
builder: (_, providers, ___) {
|
||||
final proxyProviders =
|
||||
providers.where((item) => item.type == "Proxy").map(
|
||||
(item) => ProviderItem(
|
||||
provider: item,
|
||||
),
|
||||
);
|
||||
final ruleProviders =
|
||||
providers.where((item) => item.type == "Rule").map(
|
||||
(item) => ProviderItem(
|
||||
provider: item,
|
||||
),
|
||||
);
|
||||
final proxySection = generateSection(
|
||||
title: appLocalizations.proxyProviders,
|
||||
items: proxyProviders,
|
||||
final providers = ref.watch(providersProvider);
|
||||
final proxyProviders = providers.where((item) => item.type == "Proxy").map(
|
||||
(item) => ProviderItem(
|
||||
provider: item,
|
||||
),
|
||||
);
|
||||
final ruleSection = generateSection(
|
||||
title: appLocalizations.ruleProviders,
|
||||
items: ruleProviders,
|
||||
final ruleProviders = providers.where((item) => item.type == "Rule").map(
|
||||
(item) => ProviderItem(
|
||||
provider: item,
|
||||
),
|
||||
);
|
||||
return generateListView([
|
||||
...proxySection,
|
||||
...ruleSection,
|
||||
]);
|
||||
},
|
||||
final proxySection = generateSection(
|
||||
title: appLocalizations.proxyProviders,
|
||||
items: proxyProviders,
|
||||
);
|
||||
final ruleSection = generateSection(
|
||||
title: appLocalizations.ruleProviders,
|
||||
items: ruleProviders,
|
||||
);
|
||||
return generateListView([
|
||||
...proxySection,
|
||||
...ruleSection,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,11 +118,11 @@ class ProviderItem extends StatelessWidget {
|
||||
});
|
||||
|
||||
_handleUpdateProvider() async {
|
||||
final appState = globalState.appController.appState;
|
||||
final appController = globalState.appController;
|
||||
if (provider.vehicleType != "HTTP") return;
|
||||
await globalState.safeRun(
|
||||
() async {
|
||||
appState.setProvider(
|
||||
appController.setProvider(
|
||||
provider.copyWith(
|
||||
isUpdating: true,
|
||||
),
|
||||
@@ -140,7 +134,7 @@ class ProviderItem extends StatelessWidget {
|
||||
},
|
||||
silence: false,
|
||||
);
|
||||
appState.setProvider(
|
||||
appController.setProvider(
|
||||
await clashCore.getExternalProvider(provider.name),
|
||||
);
|
||||
await globalState.appController.updateGroupsDebounce();
|
||||
@@ -149,7 +143,6 @@ class ProviderItem extends StatelessWidget {
|
||||
_handleSideLoadProvider() async {
|
||||
await globalState.safeRun<void>(() async {
|
||||
final platformFile = await picker.pickerFile();
|
||||
final appState = globalState.appController.appState;
|
||||
final bytes = platformFile?.bytes;
|
||||
if (bytes == null || provider.path == null) return;
|
||||
final file = await File(provider.path!).create(recursive: true);
|
||||
@@ -160,7 +153,7 @@ class ProviderItem extends StatelessWidget {
|
||||
data: utf8.decode(bytes),
|
||||
);
|
||||
if (message.isNotEmpty) throw message;
|
||||
appState.setProvider(
|
||||
globalState.appController.setProvider(
|
||||
await clashCore.getExternalProvider(provider.name),
|
||||
);
|
||||
if (message.isNotEmpty) throw message;
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/fragments/proxies/list.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/fragments/proxies/providers.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'providers.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'common.dart';
|
||||
import 'setting.dart';
|
||||
import 'tab.dart';
|
||||
|
||||
class ProxiesFragment extends StatefulWidget {
|
||||
class ProxiesFragment extends ConsumerStatefulWidget {
|
||||
const ProxiesFragment({super.key});
|
||||
|
||||
@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();
|
||||
bool _hasProviders = false;
|
||||
bool _isTab = false;
|
||||
|
||||
_initActions(ProxiesType proxiesType, bool hasProvider) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.commonScaffoldState?.actions = [
|
||||
if (hasProvider) ...[
|
||||
@override
|
||||
get actions => [
|
||||
if (_hasProviders)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showExtendPage(
|
||||
isScaffold: true,
|
||||
extendPageWidth: 360,
|
||||
context,
|
||||
body: const Providers(),
|
||||
body: const ProvidersView(),
|
||||
title: appLocalizations.providers,
|
||||
);
|
||||
},
|
||||
@@ -38,71 +40,28 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
|
||||
Icons.poll_outlined,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (proxiesType == ProxiesType.tab) ...[
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_proxiesTabKey.currentState?.scrollToGroupSelected();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.adjust_outlined,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showExtendPage(
|
||||
context,
|
||||
extendPageWidth: 360,
|
||||
title: appLocalizations.iconConfiguration,
|
||||
body: Selector<Config, Map<String, String>>(
|
||||
selector: (_, config) => config.proxiesStyle.iconMap,
|
||||
shouldRebuild: (prev, next) {
|
||||
return !stringAndStringMapEntryIterableEquality.equals(
|
||||
prev.entries,
|
||||
next.entries,
|
||||
);
|
||||
},
|
||||
builder: (_, iconMap, __) {
|
||||
final entries = iconMap.entries.toList();
|
||||
return ListPage(
|
||||
title: appLocalizations.iconConfiguration,
|
||||
items: entries,
|
||||
keyLabel: appLocalizations.regExp,
|
||||
valueLabel: appLocalizations.icon,
|
||||
keyBuilder: (item) => Key(item.key),
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
leadingBuilder: (item) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: CommonTargetIcon(
|
||||
src: item.value,
|
||||
size: 42,
|
||||
),
|
||||
),
|
||||
subtitleBuilder: (item) => Text(
|
||||
item.value,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onChange: (entries) {
|
||||
final config = globalState.appController.config;
|
||||
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||
iconMap: Map.fromEntries(entries),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
_isTab
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_proxiesTabKey.currentState?.scrollToGroupSelected();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.adjust_outlined,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.style_outlined,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
showExtendPage(
|
||||
context,
|
||||
extendPageWidth: 360,
|
||||
title: appLocalizations.iconConfiguration,
|
||||
body: _IconConfigView(),
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.style_outlined,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
@@ -116,28 +75,88 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
|
||||
),
|
||||
)
|
||||
];
|
||||
});
|
||||
|
||||
@override
|
||||
get floatingActionButton => _isTab
|
||||
? DelayTestButton(
|
||||
onClick: () async {
|
||||
await delayTest(
|
||||
currentTabProxies,
|
||||
currentTabTestUrl,
|
||||
);
|
||||
},
|
||||
)
|
||||
: null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
ref.listenManual(
|
||||
proxiesActionsStateProvider,
|
||||
fireImmediately: true,
|
||||
(prev, next) {
|
||||
if (prev == next) {
|
||||
return;
|
||||
}
|
||||
if (next.pageLabel == PageLabel.proxies) {
|
||||
_hasProviders = next.hasProviders;
|
||||
_isTab = next.type == ProxiesType.tab;
|
||||
initPageState();
|
||||
return;
|
||||
}
|
||||
},
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
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,
|
||||
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();
|
||||
return ListPage(
|
||||
title: appLocalizations.iconConfiguration,
|
||||
items: entries,
|
||||
keyLabel: appLocalizations.regExp,
|
||||
valueLabel: appLocalizations.icon,
|
||||
keyBuilder: (item) => Key(item.key),
|
||||
titleBuilder: (item) => Text(item.key),
|
||||
leadingBuilder: (item) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: CommonTargetIcon(
|
||||
src: item.value,
|
||||
size: 42,
|
||||
),
|
||||
),
|
||||
subtitleBuilder: (item) => Text(
|
||||
item.value,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onChange: (entries) {
|
||||
ref.read(proxiesStyleSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
iconMap: Map.fromEntries(entries),
|
||||
),
|
||||
ProxiesType.list => const ProxiesListFragment(),
|
||||
},
|
||||
);
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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/providers/config.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ProxiesSetting extends StatelessWidget {
|
||||
const ProxiesSetting({super.key});
|
||||
@@ -56,10 +55,11 @@ class ProxiesSetting extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, ProxiesType>(
|
||||
selector: (_, config) => config.proxiesStyle.type,
|
||||
builder: (_, proxiesType, __) {
|
||||
final config = globalState.appController.config;
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final proxiesType = ref.watch(proxiesStyleSettingProvider.select(
|
||||
(state) => state.type,
|
||||
));
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
@@ -71,9 +71,13 @@ class ProxiesSetting extends StatelessWidget {
|
||||
),
|
||||
isSelected: proxiesType == item,
|
||||
onPressed: () {
|
||||
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||
type: item,
|
||||
);
|
||||
ref
|
||||
.read(proxiesStyleSettingProvider.notifier)
|
||||
.updateState((state) {
|
||||
return state.copyWith(
|
||||
type: item,
|
||||
);
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
@@ -92,10 +96,11 @@ class ProxiesSetting extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, ProxiesSortType>(
|
||||
selector: (_, config) => config.proxiesStyle.sortType,
|
||||
builder: (_, proxiesSortType, __) {
|
||||
final config = globalState.appController.config;
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final sortType = ref.watch(proxiesStyleSettingProvider.select(
|
||||
(state) => state.sortType,
|
||||
));
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
@@ -105,11 +110,15 @@ class ProxiesSetting extends StatelessWidget {
|
||||
label: _getStringProxiesSortType(item),
|
||||
iconData: _getIconWithProxiesSortType(item),
|
||||
),
|
||||
isSelected: proxiesSortType == item,
|
||||
isSelected: sortType == item,
|
||||
onPressed: () {
|
||||
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||
sortType: item,
|
||||
);
|
||||
ref
|
||||
.read(proxiesStyleSettingProvider.notifier)
|
||||
.updateState((state) {
|
||||
return state.copyWith(
|
||||
sortType: item,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -128,21 +137,26 @@ class ProxiesSetting extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, ProxyCardType>(
|
||||
selector: (_, config) => config.proxiesStyle.cardType,
|
||||
builder: (_, proxyCardType, __) {
|
||||
final config = globalState.appController.config;
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final cardType = ref.watch(proxiesStyleSettingProvider.select(
|
||||
(state) => state.cardType,
|
||||
));
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
for (final item in ProxyCardType.values)
|
||||
SettingTextCard(
|
||||
Intl.message(item.name),
|
||||
isSelected: item == proxyCardType,
|
||||
isSelected: item == cardType,
|
||||
onPressed: () {
|
||||
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||
cardType: item,
|
||||
);
|
||||
ref
|
||||
.read(proxiesStyleSettingProvider.notifier)
|
||||
.updateState((state) {
|
||||
return state.copyWith(
|
||||
cardType: item,
|
||||
);
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
@@ -163,21 +177,26 @@ class ProxiesSetting extends StatelessWidget {
|
||||
horizontal: 16,
|
||||
),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, ProxiesLayout>(
|
||||
selector: (_, config) => config.proxiesStyle.layout,
|
||||
builder: (_, proxiesLayout, __) {
|
||||
final config = globalState.appController.config;
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final layout = ref.watch(proxiesStyleSettingProvider.select(
|
||||
(state) => state.layout,
|
||||
));
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
for (final item in ProxiesLayout.values)
|
||||
SettingTextCard(
|
||||
getTextForProxiesLayout(item),
|
||||
isSelected: item == proxiesLayout,
|
||||
isSelected: item == layout,
|
||||
onPressed: () {
|
||||
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||
layout: item,
|
||||
);
|
||||
ref
|
||||
.watch(proxiesStyleSettingProvider.notifier)
|
||||
.updateState((state) {
|
||||
return state.copyWith(
|
||||
layout: item,
|
||||
);
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
@@ -196,9 +215,11 @@ class ProxiesSetting extends StatelessWidget {
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Selector<Config, ProxiesIconStyle>(
|
||||
selector: (_, config) => config.proxiesStyle.iconStyle,
|
||||
builder: (_, iconStyle, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final iconStyle = ref.watch(proxiesStyleSettingProvider.select(
|
||||
(state) => state.iconStyle,
|
||||
));
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
@@ -207,10 +228,13 @@ class ProxiesSetting extends StatelessWidget {
|
||||
_getTextWithProxiesIconStyle(item),
|
||||
isSelected: iconStyle == item,
|
||||
onPressed: () {
|
||||
final config = globalState.appController.config;
|
||||
config.proxiesStyle = config.proxiesStyle.copyWith(
|
||||
iconStyle: item,
|
||||
);
|
||||
ref
|
||||
.read(proxiesStyleSettingProvider.notifier)
|
||||
.updateState((state) {
|
||||
return state.copyWith(
|
||||
iconStyle: item,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -234,11 +258,11 @@ class ProxiesSetting extends StatelessWidget {
|
||||
..._buildSortSetting(),
|
||||
..._buildLayoutSetting(),
|
||||
..._buildSizeSetting(),
|
||||
Selector<Config, bool>(
|
||||
selector: (_, config) =>
|
||||
config.proxiesStyle.type == ProxiesType.list,
|
||||
builder: (_, value, child) {
|
||||
if (value) {
|
||||
Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final isList = ref.watch(proxiesStyleSettingProvider
|
||||
.select((state) => state.type == ProxiesType.list));
|
||||
if (isList) {
|
||||
return child!;
|
||||
}
|
||||
return Container();
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
import 'dart:math';
|
||||
|
||||
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/models/common.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'card.dart';
|
||||
import 'common.dart';
|
||||
|
||||
List<Proxy> currentProxies = [];
|
||||
String? currentTestUrl;
|
||||
List<Proxy> currentTabProxies = [];
|
||||
String? currentTabTestUrl;
|
||||
|
||||
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
|
||||
|
||||
class ProxiesTabFragment extends StatefulWidget {
|
||||
class ProxiesTabFragment extends ConsumerStatefulWidget {
|
||||
const ProxiesTabFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ProxiesTabFragment> createState() => ProxiesTabFragmentState();
|
||||
ConsumerState<ProxiesTabFragment> createState() => ProxiesTabFragmentState();
|
||||
}
|
||||
|
||||
class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
class ProxiesTabFragmentState extends ConsumerState<ProxiesTabFragment>
|
||||
with TickerProviderStateMixin {
|
||||
TabController? _tabController;
|
||||
final _hasMoreButtonNotifier = ValueNotifier<bool>(false);
|
||||
GroupNameKeyMap _keyMap = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_handleTabListen();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_destroyTabController();
|
||||
@@ -36,17 +42,17 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
}
|
||||
|
||||
scrollToGroupSelected() {
|
||||
final currentGroupName = globalState.appController.config.currentGroupName;
|
||||
final currentGroupName = globalState.appController.getCurrentGroupName();
|
||||
_keyMap[currentGroupName]?.currentState?.scrollToSelected();
|
||||
}
|
||||
|
||||
_buildMoreButton() {
|
||||
return Selector<AppState, bool>(
|
||||
selector: (_, appState) => appState.viewMode == ViewMode.mobile,
|
||||
builder: (_, value, ___) {
|
||||
return Consumer(
|
||||
builder: (_, ref, ___) {
|
||||
final isMobileView = ref.watch(viewWidthProvider.notifier).isMobileView;
|
||||
return IconButton(
|
||||
onPressed: _showMoreMenu,
|
||||
icon: value
|
||||
icon: isMobileView
|
||||
? const Icon(
|
||||
Icons.expand_more,
|
||||
)
|
||||
@@ -65,16 +71,9 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
isScrollControlled: false,
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Selector2<AppState, Config, ProxiesSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
final currentGroups = appState.currentGroups;
|
||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
||||
return ProxiesSelectorState(
|
||||
groupNames: groupNames,
|
||||
currentGroupName: config.currentGroupName,
|
||||
);
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
child: Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final state = ref.watch(proxiesSelectorStateProvider);
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
@@ -86,13 +85,13 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
SettingTextCard(
|
||||
groupName,
|
||||
onPressed: () {
|
||||
final index = state.groupNames
|
||||
.indexWhere((item) => item == groupName);
|
||||
final index = state.groupNames.indexWhere(
|
||||
(item) => item == groupName,
|
||||
);
|
||||
if (index == -1) return;
|
||||
_tabController?.animateTo(index);
|
||||
globalState.appController.config.updateCurrentGroupName(
|
||||
groupName,
|
||||
);
|
||||
globalState.appController
|
||||
.updateCurrentGroupName(groupName);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
isSelected: groupName == state.currentGroupName,
|
||||
@@ -108,16 +107,24 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
}
|
||||
|
||||
_tabControllerListener([int? index]) {
|
||||
final appController = globalState.appController;
|
||||
final currentGroups = appController.appState.currentGroups;
|
||||
if (_tabController?.index == null) {
|
||||
int? groupIndex = index;
|
||||
if(groupIndex == -1){
|
||||
return;
|
||||
}
|
||||
final currentGroup = currentGroups[index ?? _tabController!.index];
|
||||
currentProxies = currentGroup.all;
|
||||
currentTestUrl = currentGroup.testUrl;
|
||||
final appController = globalState.appController;
|
||||
if (groupIndex == null) {
|
||||
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((_) {
|
||||
appController.config.updateCurrentGroupName(
|
||||
globalState.appController.updateCurrentGroupName(
|
||||
currentGroup.name,
|
||||
);
|
||||
});
|
||||
@@ -144,122 +151,117 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
||||
_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
|
||||
Widget build(BuildContext context) {
|
||||
return Selector2<AppState, Config, ProxiesSelectorState>(
|
||||
selector: (_, appState, config) {
|
||||
final currentGroups = appState.currentGroups;
|
||||
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(
|
||||
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 children = state.groupNames.map((groupName) {
|
||||
keyMap[groupName] = GlobalObjectKey(groupName);
|
||||
return KeepScope(
|
||||
child: ProxyGroupView(
|
||||
key: keyMap[groupName],
|
||||
groupName: groupName,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
_keyMap = keyMap;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
NotificationListener<ScrollMetricsNotification>(
|
||||
onNotification: (scrollNotification) {
|
||||
_hasMoreButtonNotifier.value =
|
||||
scrollNotification.metrics.maxScrollExtent > 0;
|
||||
return true;
|
||||
},
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _hasMoreButtonNotifier,
|
||||
builder: (_, value, child) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16 + (value ? 16 : 0),
|
||||
),
|
||||
dividerColor: Colors.transparent,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
overlayColor:
|
||||
const WidgetStatePropertyAll(Colors.transparent),
|
||||
tabs: [
|
||||
for (final groupName in state.groupNames)
|
||||
Tab(
|
||||
text: groupName,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (value)
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: child!,
|
||||
final state = ref.watch(groupNamesStateProvider);
|
||||
final groupNames = state.groupNames;
|
||||
if (groupNames.isEmpty) {
|
||||
return NullStatus(
|
||||
label: appLocalizations.nullProxies,
|
||||
);
|
||||
}
|
||||
final GroupNameKeyMap keyMap = {};
|
||||
final children = groupNames.map((groupName) {
|
||||
keyMap[groupName] = GlobalObjectKey(groupName);
|
||||
return KeepScope(
|
||||
child: ProxyGroupView(
|
||||
key: keyMap[groupName],
|
||||
groupName: groupName,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
_keyMap = keyMap;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
NotificationListener<ScrollMetricsNotification>(
|
||||
onNotification: (scrollNotification) {
|
||||
_hasMoreButtonNotifier.value =
|
||||
scrollNotification.metrics.maxScrollExtent > 0;
|
||||
return true;
|
||||
},
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _hasMoreButtonNotifier,
|
||||
builder: (_, value, child) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16 + (value ? 16 : 0),
|
||||
),
|
||||
dividerColor: Colors.transparent,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
overlayColor:
|
||||
const WidgetStatePropertyAll(Colors.transparent),
|
||||
tabs: [
|
||||
for (final groupName in groupNames)
|
||||
Tab(
|
||||
text: groupName,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
context.colorScheme.surface.withOpacity(0.1),
|
||||
context.colorScheme.surface,
|
||||
],
|
||||
stops: const [
|
||||
0.0,
|
||||
0.1
|
||||
]),
|
||||
),
|
||||
child: _buildMoreButton(),
|
||||
),
|
||||
if (value)
|
||||
Positioned(
|
||||
right: 0,
|
||||
child: child!,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
context.colorScheme.surface.withOpacity(0.1),
|
||||
context.colorScheme.surface,
|
||||
],
|
||||
stops: const [
|
||||
0.0,
|
||||
0.1
|
||||
]),
|
||||
),
|
||||
child: _buildMoreButton(),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: children,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: children,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProxyGroupView extends StatefulWidget {
|
||||
class ProxyGroupView extends ConsumerStatefulWidget {
|
||||
final String groupName;
|
||||
|
||||
const ProxyGroupView({
|
||||
@@ -268,25 +270,14 @@ class ProxyGroupView extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProxyGroupView> createState() => ProxyGroupViewState();
|
||||
ConsumerState<ProxyGroupView> createState() => ProxyGroupViewState();
|
||||
}
|
||||
|
||||
class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
var isLock = false;
|
||||
class ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
|
||||
final _controller = ScrollController();
|
||||
|
||||
String get groupName => widget.groupName;
|
||||
|
||||
_delayTest() async {
|
||||
if (isLock) return;
|
||||
isLock = true;
|
||||
await delayTest(
|
||||
currentProxies,
|
||||
currentTestUrl,
|
||||
);
|
||||
isLock = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
@@ -297,9 +288,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
if (_controller.position.maxScrollExtent == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
currentProxies,
|
||||
currentTestUrl,
|
||||
currentTabProxies,
|
||||
currentTabTestUrl,
|
||||
);
|
||||
_controller.animateTo(
|
||||
min(
|
||||
@@ -315,83 +307,45 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
||||
);
|
||||
}
|
||||
|
||||
initFab(bool isCurrent) {
|
||||
if (!isCurrent) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.commonScaffoldState?.floatingActionButton = DelayTestButton(
|
||||
onClick: () async {
|
||||
await _delayTest();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector2<AppState, Config, ProxyGroupSelectorState>(
|
||||
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 columns = state.columns;
|
||||
final proxyCardType = state.proxyCardType;
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
proxies,
|
||||
state.testUrl,
|
||||
);
|
||||
return ActiveBuilder(
|
||||
label: "proxies",
|
||||
builder: (isCurrent, child) {
|
||||
initFab(isCurrent);
|
||||
return child!;
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: GridView.builder(
|
||||
controller: _controller,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 96,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: getItemHeight(proxyCardType),
|
||||
),
|
||||
itemCount: sortedProxies.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return ProxyCard(
|
||||
testUrl: state.testUrl,
|
||||
groupType: state.groupType,
|
||||
type: proxyCardType,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
final state = ref.watch(proxyGroupSelectorStateProvider(groupName));
|
||||
final proxies = state.proxies;
|
||||
final columns = state.columns;
|
||||
final proxyCardType = state.proxyCardType;
|
||||
final sortedProxies = globalState.appController.getSortProxies(
|
||||
proxies,
|
||||
state.testUrl,
|
||||
);
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: GridView.builder(
|
||||
controller: _controller,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 96,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisExtent: getItemHeight(proxyCardType),
|
||||
),
|
||||
itemCount: sortedProxies.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = sortedProxies[index];
|
||||
return ProxyCard(
|
||||
testUrl: state.testUrl,
|
||||
groupType: state.groupType,
|
||||
type: proxyCardType,
|
||||
key: ValueKey('$groupName.${proxy.name}'),
|
||||
proxy: proxy,
|
||||
groupName: groupName,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -414,6 +368,9 @@ class _DelayTestButtonState extends State<DelayTestButton>
|
||||
late Animation<double> _scale;
|
||||
|
||||
_healthcheck() async {
|
||||
if (_controller.isAnimating) {
|
||||
return;
|
||||
}
|
||||
_controller.forward();
|
||||
await widget.onClick();
|
||||
if (mounted) {
|
||||
|
||||
@@ -3,11 +3,12 @@ import 'dart:io';
|
||||
import 'package:fl_clash/clash/clash.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:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path/path.dart' hide context;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@immutable
|
||||
class GeoItem {
|
||||
@@ -84,12 +85,13 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
|
||||
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>(
|
||||
child: UpdateGeoUrlFormDialog(
|
||||
title: geoItem.label,
|
||||
url: url,
|
||||
defaultValue: defaultGeoXMap[geoItem.key],
|
||||
defaultValue: defaultMap[geoItem.key],
|
||||
),
|
||||
);
|
||||
if (newUrl != null && newUrl != url && mounted) {
|
||||
@@ -97,9 +99,13 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
if (!newUrl.isUrl) {
|
||||
throw "Invalid url";
|
||||
}
|
||||
final appController = globalState.appController;
|
||||
appController.clashConfig.geoXUrl =
|
||||
Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl;
|
||||
ref.read(patchClashConfigProvider.notifier).updateState((state) {
|
||||
final map = state.geoXUrl.toJson();
|
||||
map[geoItem.key] = newUrl;
|
||||
return state.copyWith(
|
||||
geoXUrl: GeoXUrl.fromJson(map),
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
globalState.showMessage(
|
||||
title: geoItem.label,
|
||||
@@ -122,63 +128,74 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitle(String url) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
FutureBuilder<FileInfo>(
|
||||
future: _getGeoFileLastModified(geoItem.fileName),
|
||||
builder: (_, snapshot) {
|
||||
return SizedBox(
|
||||
height: 24,
|
||||
child: FadeBox(
|
||||
key: Key("fade_box_${geoItem.label}"),
|
||||
child: snapshot.data == null
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
snapshot.data!.desc,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
url,
|
||||
style: context.textTheme.bodyMedium?.toLight,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 6,
|
||||
spacing: 12,
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.edit),
|
||||
label: appLocalizations.edit,
|
||||
onPressed: () {
|
||||
_updateUrl(url);
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
FutureBuilder<FileInfo>(
|
||||
future: _getGeoFileLastModified(geoItem.fileName),
|
||||
builder: (_, snapshot) {
|
||||
return SizedBox(
|
||||
height: 24,
|
||||
child: FadeBox(
|
||||
key: Key("fade_box_${geoItem.label}"),
|
||||
child: snapshot.data == null
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
snapshot.data!.desc,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.sync),
|
||||
label: appLocalizations.sync,
|
||||
onPressed: () {
|
||||
_handleUpdateGeoDataItem();
|
||||
},
|
||||
Text(
|
||||
url,
|
||||
style: context.textTheme.bodyMedium?.toLight,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Wrap(
|
||||
runSpacing: 6,
|
||||
spacing: 12,
|
||||
children: [
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.edit),
|
||||
label: appLocalizations.edit,
|
||||
onPressed: () {
|
||||
_updateUrl(url, ref);
|
||||
},
|
||||
),
|
||||
CommonChip(
|
||||
avatar: const Icon(Icons.sync),
|
||||
label: appLocalizations.sync,
|
||||
onPressed: () {
|
||||
_handleUpdateGeoDataItem();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,7 +218,6 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
geoType: geoItem.label,
|
||||
),
|
||||
);
|
||||
print(message);
|
||||
if (message.isNotEmpty) throw message;
|
||||
} catch (e) {
|
||||
isUpdating.value = false;
|
||||
@@ -225,12 +241,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
||||
vertical: 4,
|
||||
),
|
||||
title: Text(geoItem.label),
|
||||
subtitle: Selector<ClashConfig, String>(
|
||||
selector: (_, clashConfig) => clashConfig.geoXUrl[geoItem.key]!,
|
||||
builder: (_, value, __) {
|
||||
return _buildSubtitle(value);
|
||||
},
|
||||
),
|
||||
subtitle: _buildSubtitle(),
|
||||
trailing: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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/providers/config.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../widgets/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class ThemeModeItem {
|
||||
final ThemeMode themeMode;
|
||||
@@ -88,99 +86,106 @@ class ItemCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ThemeColorsBox extends StatefulWidget {
|
||||
class ThemeColorsBox extends ConsumerStatefulWidget {
|
||||
const ThemeColorsBox({super.key});
|
||||
|
||||
@override
|
||||
State<ThemeColorsBox> createState() => _ThemeColorsBoxState();
|
||||
ConsumerState<ThemeColorsBox> createState() => _ThemeColorsBoxState();
|
||||
}
|
||||
|
||||
class _ThemeColorsBoxState extends State<ThemeColorsBox> {
|
||||
Widget _themeModeCheckBox({
|
||||
bool? isSelected,
|
||||
required ThemeModeItem themeModeItem,
|
||||
}) {
|
||||
return CommonCard(
|
||||
isSelected: isSelected,
|
||||
onPressed: () {
|
||||
final appController = globalState.appController;
|
||||
appController.config.themeProps =
|
||||
appController.config.themeProps.copyWith(
|
||||
themeMode: themeModeItem.themeMode,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Icon(themeModeItem.iconData),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
themeModeItem.label,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _primaryColorCheckBox({
|
||||
bool? isSelected,
|
||||
Color? color,
|
||||
}) {
|
||||
return ColorSchemeBox(
|
||||
isSelected: isSelected,
|
||||
primaryColor: color,
|
||||
onPressed: () {
|
||||
final appController = globalState.appController;
|
||||
appController.config.themeProps =
|
||||
appController.config.themeProps.copyWith(
|
||||
primaryColor: color?.value,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _fontFamilyCheckBox({
|
||||
bool? isSelected,
|
||||
required FontFamilyItem fontFamilyItem,
|
||||
}) {
|
||||
return CommonCard(
|
||||
isSelected: isSelected,
|
||||
onPressed: () {
|
||||
final appController = globalState.appController;
|
||||
appController.config.themeProps =
|
||||
appController.config.themeProps.copyWith(
|
||||
fontFamily: fontFamilyItem.fontFamily,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
fontFamilyItem.label,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ThemeColorsBoxState extends ConsumerState<ThemeColorsBox> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_FontFamilyItem(),
|
||||
_ThemeModeItem(),
|
||||
_PrimaryColorItem(),
|
||||
_PrueBlackItem(),
|
||||
const SizedBox(
|
||||
height: 64,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FontFamilyItem extends ConsumerWidget {
|
||||
const _FontFamilyItem();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final fontFamily =
|
||||
ref.watch(themeSettingProvider.select((state) => state.fontFamily));
|
||||
List<FontFamilyItem> fontFamilyItems = [
|
||||
FontFamilyItem(
|
||||
label: appLocalizations.systemFont,
|
||||
fontFamily: FontFamily.system,
|
||||
),
|
||||
const FontFamilyItem(
|
||||
label: "MiSans",
|
||||
fontFamily: FontFamily.miSans,
|
||||
),
|
||||
];
|
||||
return ItemCard(
|
||||
info: Info(
|
||||
label: appLocalizations.fontFamily,
|
||||
iconData: Icons.text_fields,
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
),
|
||||
height: 48,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (_, index) {
|
||||
final fontFamilyItem = fontFamilyItems[index];
|
||||
return CommonCard(
|
||||
isSelected: fontFamilyItem.fontFamily == fontFamily,
|
||||
onPressed: () {
|
||||
ref.read(themeSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
fontFamily: fontFamilyItem.fontFamily,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
fontFamilyItem.label,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
width: 16,
|
||||
);
|
||||
},
|
||||
itemCount: fontFamilyItems.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThemeModeItem extends ConsumerWidget {
|
||||
const _ThemeModeItem();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final themeMode =
|
||||
ref.watch(themeSettingProvider.select((state) => state.themeMode));
|
||||
List<ThemeModeItem> themeModeItems = [
|
||||
ThemeModeItem(
|
||||
iconData: Icons.auto_mode,
|
||||
@@ -198,6 +203,68 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
|
||||
themeMode: ThemeMode.dark,
|
||||
),
|
||||
];
|
||||
return ItemCard(
|
||||
info: Info(
|
||||
label: appLocalizations.themeMode,
|
||||
iconData: Icons.brightness_high,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
height: 64,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: themeModeItems.length,
|
||||
itemBuilder: (_, index) {
|
||||
final themeModeItem = themeModeItems[index];
|
||||
return CommonCard(
|
||||
isSelected: themeModeItem.themeMode == themeMode,
|
||||
onPressed: () {
|
||||
ref.read(themeSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
themeMode: themeModeItem.themeMode,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Icon(themeModeItem.iconData),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
themeModeItem.label,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
width: 16,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PrimaryColorItem extends ConsumerWidget {
|
||||
const _PrimaryColorItem();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final primaryColor =
|
||||
ref.watch(themeSettingProvider.select((state) => state.primaryColor));
|
||||
List<Color?> primaryColors = [
|
||||
null,
|
||||
defaultPrimaryColor,
|
||||
@@ -207,147 +274,72 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
|
||||
Colors.yellowAccent,
|
||||
Colors.purple,
|
||||
];
|
||||
List<FontFamilyItem> fontFamilyItems = [
|
||||
FontFamilyItem(
|
||||
label: appLocalizations.systemFont,
|
||||
fontFamily: FontFamily.system,
|
||||
return ItemCard(
|
||||
info: Info(
|
||||
label: appLocalizations.themeColor,
|
||||
iconData: Icons.palette,
|
||||
),
|
||||
const FontFamilyItem(
|
||||
label: "MiSans",
|
||||
fontFamily: FontFamily.miSans,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
),
|
||||
height: 88,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (_, index) {
|
||||
final color = primaryColors[index];
|
||||
return ColorSchemeBox(
|
||||
isSelected: color?.value == primaryColor,
|
||||
primaryColor: color,
|
||||
onPressed: () {
|
||||
ref.read(themeSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
primaryColor: color?.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
width: 16,
|
||||
);
|
||||
},
|
||||
itemCount: primaryColors.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PrueBlackItem extends ConsumerWidget {
|
||||
const _PrueBlackItem();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final prueBlack =
|
||||
ref.watch(themeSettingProvider.select((state) => state.prueBlack));
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: ListItem.switchItem(
|
||||
leading: Icon(
|
||||
Icons.contrast,
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
title: Text(appLocalizations.prueBlackMode),
|
||||
delegate: SwitchDelegate(
|
||||
value: prueBlack,
|
||||
onChanged: (value) {
|
||||
ref.read(themeSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
prueBlack: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
return Column(
|
||||
children: [
|
||||
ItemCard(
|
||||
info: Info(
|
||||
label: appLocalizations.fontFamily,
|
||||
iconData: Icons.text_fields,
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
),
|
||||
height: 48,
|
||||
child: Selector<Config, FontFamily>(
|
||||
selector: (_, config) => config.themeProps.fontFamily,
|
||||
builder: (_, fontFamily, __) {
|
||||
return ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (_, index) {
|
||||
final fontFamilyItem = fontFamilyItems[index];
|
||||
return _fontFamilyCheckBox(
|
||||
isSelected: fontFamily == fontFamilyItem.fontFamily,
|
||||
fontFamilyItem: fontFamilyItem,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
width: 16,
|
||||
);
|
||||
},
|
||||
itemCount: fontFamilyItems.length,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ItemCard(
|
||||
info: Info(
|
||||
label: appLocalizations.themeMode,
|
||||
iconData: Icons.brightness_high,
|
||||
),
|
||||
child: Selector<Config, ThemeMode>(
|
||||
selector: (_, config) => config.themeProps.themeMode,
|
||||
builder: (_, themeMode, __) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
height: 64,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: themeModeItems.length,
|
||||
itemBuilder: (_, index) {
|
||||
final themeModeItem = themeModeItems[index];
|
||||
return _themeModeCheckBox(
|
||||
isSelected: themeMode == themeModeItem.themeMode,
|
||||
themeModeItem: themeModeItem,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
width: 16,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ItemCard(
|
||||
info: Info(
|
||||
label: appLocalizations.themeColor,
|
||||
iconData: Icons.palette,
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
),
|
||||
height: 88,
|
||||
child: Selector<Config, int?>(
|
||||
selector: (_, config) => config.themeProps.primaryColor,
|
||||
builder: (_, currentPrimaryColor, __) {
|
||||
return ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (_, index) {
|
||||
final primaryColor = primaryColors[index];
|
||||
return _primaryColorCheckBox(
|
||||
isSelected: currentPrimaryColor == primaryColor?.value,
|
||||
color: primaryColor,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) {
|
||||
return const SizedBox(
|
||||
width: 16,
|
||||
);
|
||||
},
|
||||
itemCount: primaryColors.length,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Selector<Config, bool>(
|
||||
selector: (_, config) => config.themeProps.prueBlack,
|
||||
builder: (_, value, ___) {
|
||||
return ListItem.switchItem(
|
||||
leading: Icon(
|
||||
Icons.contrast,
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
title: Text(appLocalizations.prueBlackMode),
|
||||
delegate: SwitchDelegate(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
final appController = globalState.appController;
|
||||
appController.config.themeProps =
|
||||
appController.config.themeProps.copyWith(
|
||||
prueBlack: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 64,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:io';
|
||||
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/access.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/l10n/l10n.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'backup_and_recovery.dart';
|
||||
import 'theme.dart';
|
||||
import 'package:path/path.dart' show dirname, join;
|
||||
|
||||
class ToolsFragment extends StatefulWidget {
|
||||
class ToolsFragment extends ConsumerStatefulWidget {
|
||||
const ToolsFragment({super.key});
|
||||
|
||||
@override
|
||||
State<ToolsFragment> createState() => _ToolboxFragmentState();
|
||||
ConsumerState<ToolsFragment> createState() => _ToolboxFragmentState();
|
||||
}
|
||||
|
||||
class _ToolboxFragmentState extends State<ToolsFragment> {
|
||||
class _ToolboxFragmentState extends ConsumerState<ToolsFragment> {
|
||||
_buildNavigationMenuItem(NavigationItem navigationItem) {
|
||||
return ListItem.open(
|
||||
leading: navigationItem.icon,
|
||||
title: Text(Intl.message(navigationItem.label)),
|
||||
title: Text(Intl.message(navigationItem.label.name)),
|
||||
subtitle: navigationItem.description != null
|
||||
? Text(Intl.message(navigationItem.description!))
|
||||
: null,
|
||||
delegate: OpenDelegate(
|
||||
title: Intl.message(navigationItem.label),
|
||||
title: Intl.message(navigationItem.label.name),
|
||||
widget: navigationItem.fragment,
|
||||
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() {
|
||||
return generateSection(
|
||||
title: appLocalizations.other,
|
||||
items: [
|
||||
ListItem(
|
||||
leading: const Icon(Icons.gavel),
|
||||
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(),
|
||||
),
|
||||
),
|
||||
_DisclaimerItem(),
|
||||
_InfoItem(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -91,145 +69,233 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
||||
return generateSection(
|
||||
title: appLocalizations.settings,
|
||||
items: [
|
||||
Selector<Config, String?>(
|
||||
selector: (_, config) => config.appSetting.locale,
|
||||
builder: (_, localeString, __) {
|
||||
final subTitle = localeString ?? appLocalizations.defaultText;
|
||||
final currentLocale = other.getLocaleForString(localeString);
|
||||
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? 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(),
|
||||
),
|
||||
),
|
||||
_LocaleItem(),
|
||||
_ThemeItem(),
|
||||
_BackupItem(),
|
||||
if (system.isDesktop) _HotkeyItem(),
|
||||
if (Platform.isWindows) _LoopbackItem(),
|
||||
if (Platform.isAndroid) _AccessItem(),
|
||||
_OverrideItem(),
|
||||
_SettingItem(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LocaleBuilder(
|
||||
builder: (_) {
|
||||
final items = [
|
||||
Selector<AppState, MoreToolsSelectorState>(
|
||||
selector: (_, appState) {
|
||||
return MoreToolsSelectorState(
|
||||
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(),
|
||||
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||
final items = [
|
||||
Consumer(
|
||||
builder: (_, ref, __) {
|
||||
final state = ref.watch(moreToolsSelectorStateProvider);
|
||||
if (state.navigationItems.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
ListHeader(title: appLocalizations.more),
|
||||
_buildNavigationMenu(state.navigationItems)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
..._getSettingList(),
|
||||
..._getOtherList(),
|
||||
];
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, index) => items[index],
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LocaleItem extends ConsumerWidget {
|
||||
const _LocaleItem();
|
||||
|
||||
String _getLocaleString(Locale? locale) {
|
||||
if (locale == null) return appLocalizations.defaultText;
|
||||
return Intl.message(locale.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final locale =
|
||||
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||
final subTitle = locale ?? appLocalizations.defaultText;
|
||||
final currentLocale = other.getLocaleForString(locale);
|
||||
return ListItem<Locale?>.options(
|
||||
leading: const Icon(Icons.language_outlined),
|
||||
title: Text(appLocalizations.language),
|
||||
subtitle: Text(Intl.message(subTitle)),
|
||||
delegate: OptionsDelegate(
|
||||
title: appLocalizations.language,
|
||||
options: [null, ...AppLocalizations.delegate.supportedLocales],
|
||||
onChanged: (Locale? locale) {
|
||||
ref.read(appSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(locale: locale?.toString()),
|
||||
);
|
||||
},
|
||||
builder: (_, state, __) {
|
||||
if (state.navigationItems.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
ListHeader(title: appLocalizations.more),
|
||||
_buildNavigationMenu(state.navigationItems)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
..._getSettingList(),
|
||||
..._getOtherList(),
|
||||
];
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, index) => items[index],
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
},
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,5 +342,6 @@
|
||||
"copySuccess": "Copy success",
|
||||
"copyLink": "Copy link",
|
||||
"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"
|
||||
}
|
||||
@@ -342,5 +342,6 @@
|
||||
"copySuccess": "复制成功",
|
||||
"copyLink": "复制链接",
|
||||
"exportFile": "导出文件",
|
||||
"cacheCorrupt": "缓存已损坏,是否清空?"
|
||||
"cacheCorrupt": "缓存已损坏,是否清空?",
|
||||
"detectionTip": "依赖第三方程序仅供参考"
|
||||
}
|
||||
|
||||
@@ -180,6 +180,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"desc": MessageLookupByLibrary.simpleMessage(
|
||||
"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"),
|
||||
"disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"),
|
||||
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
|
||||
|
||||
@@ -122,6 +122,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"desc": MessageLookupByLibrary.simpleMessage(
|
||||
"基于ClashMeta的多平台代理客户端,简单易用,开源无广告。",
|
||||
),
|
||||
"detectionTip": MessageLookupByLibrary.simpleMessage("依赖第三方程序仅供参考"),
|
||||
"direct": MessageLookupByLibrary.simpleMessage("直连"),
|
||||
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
|
||||
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
|
||||
|
||||
@@ -2669,6 +2669,16 @@ class AppLocalizations {
|
||||
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> {
|
||||
|
||||
107
lib/main.dart
107
lib/main.dart
@@ -11,53 +11,26 @@ import 'package:fl_clash/plugins/tile.dart';
|
||||
import 'package:fl_clash/plugins/vpn.dart';
|
||||
import 'package:fl_clash/state.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 'clash/core.dart';
|
||||
import 'clash/lib.dart';
|
||||
import 'common/common.dart';
|
||||
import 'l10n/l10n.dart';
|
||||
import 'models/models.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
globalState.isService = false;
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await clashCore.preload();
|
||||
globalState.packageInfo = await PackageInfo.fromPlatform();
|
||||
final version = await system.version;
|
||||
final config = await preferences.getConfig() ?? Config();
|
||||
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
|
||||
await AppLocalizations.load(
|
||||
other.getLocaleForString(config.appSetting.locale) ??
|
||||
WidgetsBinding.instance.platformDispatcher.locale,
|
||||
);
|
||||
await clashCore.preload();
|
||||
await globalState.initApp(version);
|
||||
await android?.init();
|
||||
await window?.init(config.windowProps, 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,
|
||||
);
|
||||
await window?.init(version);
|
||||
HttpOverrides.global = FlClashHttpOverrides();
|
||||
runAppWithPreferences(
|
||||
const Application(),
|
||||
appState: appState,
|
||||
appFlowingState: appFlowingState,
|
||||
config: config,
|
||||
clashConfig: clashConfig,
|
||||
);
|
||||
runApp(ProviderScope(
|
||||
child: const Application(),
|
||||
));
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
@@ -66,11 +39,7 @@ Future<void> _service(List<String> flags) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final quickStart = flags.contains("quick");
|
||||
final clashLibHandler = ClashLibHandler();
|
||||
final config = await preferences.getConfig() ?? Config();
|
||||
await AppLocalizations.load(
|
||||
other.getLocaleForString(config.appSetting.locale) ??
|
||||
WidgetsBinding.instance.platformDispatcher.locale,
|
||||
);
|
||||
await globalState.init();
|
||||
|
||||
tile?.addListener(
|
||||
_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 = () {
|
||||
final traffic = clashLibHandler.getTraffic();
|
||||
@@ -122,17 +64,22 @@ Future<void> _service(List<String> flags) async {
|
||||
vpn?.addListener(
|
||||
_VpnListenerWithService(
|
||||
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) {
|
||||
clashLibHandler.updateDns(dns);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final invokeReceiverPort = ReceivePort();
|
||||
|
||||
clashLibHandler.attachInvokePort(
|
||||
invokeReceiverPort.sendPort.nativePort,
|
||||
);
|
||||
|
||||
invokeReceiverPort.listen(
|
||||
(message) async {
|
||||
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) {
|
||||
|
||||
@@ -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/state.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:flutter/material.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;
|
||||
|
||||
const AndroidManager({
|
||||
@@ -15,47 +13,25 @@ class AndroidManager extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<AndroidManager> createState() => _AndroidContainerState();
|
||||
ConsumerState<AndroidManager> createState() => _AndroidContainerState();
|
||||
}
|
||||
|
||||
class _AndroidContainerState extends State<AndroidManager> {
|
||||
class _AndroidContainerState extends ConsumerState<AndroidManager> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
}
|
||||
|
||||
Widget _updateCoreState(Widget child) {
|
||||
return Selector2<Config, ClashConfig, CoreState>(
|
||||
selector: (_, config, clashConfig) => globalState.getCoreState(
|
||||
config,
|
||||
clashConfig,
|
||||
),
|
||||
builder: (__, state, child) {
|
||||
clashLib?.setState(state);
|
||||
return child!;
|
||||
ref.listenManual(
|
||||
appSettingProvider.select((state) => state.hidden),
|
||||
(prev, next) {
|
||||
app?.updateExcludeFromRecents(next);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _excludeContainer(Widget child) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.hidden,
|
||||
builder: (_, hidden, child) {
|
||||
app?.updateExcludeFromRecents(hidden);
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
fireImmediately: true
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _updateCoreState(
|
||||
_excludeContainer(
|
||||
widget.child,
|
||||
),
|
||||
);
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AppStateManager extends StatefulWidget {
|
||||
final Widget child;
|
||||
@@ -18,48 +16,6 @@ class AppStateManager extends StatefulWidget {
|
||||
|
||||
class _AppStateManagerState extends State<AppStateManager>
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -76,7 +32,7 @@ class _AppStateManagerState extends State<AppStateManager>
|
||||
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.inactive) {
|
||||
globalState.appController.savePreferencesDebounce();
|
||||
globalState.appController.savePreferences();
|
||||
render?.pause();
|
||||
} else {
|
||||
render?.resume();
|
||||
@@ -85,8 +41,9 @@ class _AppStateManagerState extends State<AppStateManager>
|
||||
|
||||
@override
|
||||
void didChangePlatformBrightness() {
|
||||
globalState.appController.appState.brightness =
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness;
|
||||
globalState.appController.updateBrightness(
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -95,11 +52,7 @@ class _AppStateManagerState extends State<AppStateManager>
|
||||
onPointerHover: (_) {
|
||||
render?.resume();
|
||||
},
|
||||
child: _cacheStateChange(
|
||||
_updateNavigationsContainer(
|
||||
widget.child,
|
||||
),
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/providers/state.dart';
|
||||
import 'package:fl_clash/state.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 StatefulWidget {
|
||||
class ClashManager extends ConsumerStatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const ClashManager({
|
||||
@@ -17,78 +18,35 @@ class ClashManager extends StatefulWidget {
|
||||
});
|
||||
|
||||
@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
|
||||
Widget build(BuildContext context) {
|
||||
return _changeProfileContainer(
|
||||
_updateContainer(
|
||||
widget.child,
|
||||
),
|
||||
);
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
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
|
||||
@@ -113,7 +71,7 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
|
||||
|
||||
@override
|
||||
void onLog(Log log) {
|
||||
globalState.appController.appFlowingState.addLog(log);
|
||||
ref.watch(logsProvider.notifier).addLog(log);
|
||||
if (log.logLevel == LogLevel.error) {
|
||||
globalState.showNotifier(log.payload ?? '');
|
||||
}
|
||||
@@ -122,19 +80,18 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
|
||||
|
||||
@override
|
||||
void onRequest(Connection connection) async {
|
||||
globalState.appController.appState.addRequest(connection);
|
||||
ref.watch(requestsProvider.notifier).addRequest(connection);
|
||||
super.onRequest(connection);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLoaded(String providerName) async {
|
||||
final appController = globalState.appController;
|
||||
appController.appState.setProvider(
|
||||
await clashCore.getExternalProvider(
|
||||
providerName,
|
||||
),
|
||||
);
|
||||
await appController.updateGroupsDebounce();
|
||||
ref.watch(providersProvider.notifier).setProvider(
|
||||
await clashCore.getExternalProvider(
|
||||
providerName,
|
||||
),
|
||||
);
|
||||
await globalState.appController.updateGroupsDebounce();
|
||||
super.onLoaded(providerName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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/config.dart';
|
||||
import 'package:fl_clash/providers/config.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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class HotKeyManager extends StatelessWidget {
|
||||
final Widget child;
|
||||
@@ -36,11 +36,11 @@ class HotKeyManager extends StatelessWidget {
|
||||
}) async {
|
||||
await hotKeyManager.unregisterAll();
|
||||
final hotkeyActionHandles = hotKeyActions.where(
|
||||
(hotKeyAction) {
|
||||
(hotKeyAction) {
|
||||
return hotKeyAction.key != null && hotKeyAction.modifiers.isNotEmpty;
|
||||
},
|
||||
).map<Future>(
|
||||
(hotKeyAction) async {
|
||||
(hotKeyAction) async {
|
||||
final modifiers = hotKeyAction.modifiers
|
||||
.map((item) => item.toHotKeyModifier())
|
||||
.toList();
|
||||
@@ -61,14 +61,18 @@ class HotKeyManager extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Config, List<HotKeyAction>>(
|
||||
selector: (_, config) => config.hotKeyActions,
|
||||
shouldRebuild: (prev, next) {
|
||||
return !hotKeyActionListEquality.equals(prev, next);
|
||||
},
|
||||
builder: (_, hotKeyActions, __) {
|
||||
_updateHotKeys(hotKeyActions: hotKeyActions);
|
||||
return child;
|
||||
return Consumer(
|
||||
builder: (_, ref, child) {
|
||||
ref.listenManual(
|
||||
hotKeyActionsProvider,
|
||||
(prev, next) {
|
||||
if (!hotKeyActionListEquality.equals(prev, next)) {
|
||||
_updateHotKeys(hotKeyActions: next);
|
||||
}
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
return child!;
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ export 'clash_manager.dart';
|
||||
export 'tile_manager.dart';
|
||||
export 'app_state_manager.dart';
|
||||
export 'vpn_manager.dart';
|
||||
export 'media_manager.dart';
|
||||
export 'proxy_manager.dart';
|
||||
export 'connectivity_manager.dart';
|
||||
export 'message_manager.dart';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
import 'package:fl_clash/common/proxy.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/state.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;
|
||||
|
||||
const ProxyManager({super.key, required this.child});
|
||||
|
||||
@override
|
||||
ConsumerState createState() => _ProxyManagerState();
|
||||
}
|
||||
|
||||
class _ProxyManagerState extends ConsumerState<ProxyManager> {
|
||||
_updateProxy(ProxyState proxyState) async {
|
||||
final isStart = proxyState.isStart;
|
||||
final systemProxy = proxyState.systemProxy;
|
||||
@@ -20,19 +26,21 @@ class ProxyManager extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector3<AppFlowingState, Config, ClashConfig, ProxyState>(
|
||||
selector: (_, appFlowingState, config, clashConfig) => ProxyState(
|
||||
isStart: appFlowingState.isStart,
|
||||
systemProxy: config.networkProps.systemProxy,
|
||||
port: clashConfig.mixedPort,
|
||||
bassDomain: config.networkProps.bypassDomain,
|
||||
),
|
||||
builder: (_, state, child) {
|
||||
_updateProxy(state);
|
||||
return child!;
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref.listenManual(
|
||||
proxyStateProvider,
|
||||
(prev, next) {
|
||||
if (prev != next) {
|
||||
_updateProxy(next);
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
fireImmediately: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
|
||||
class TrayManager extends StatefulWidget {
|
||||
class TrayManager extends ConsumerStatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const TrayManager({
|
||||
@@ -14,43 +14,27 @@ class TrayManager extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<TrayManager> createState() => _TrayContainerState();
|
||||
ConsumerState<TrayManager> createState() => _TrayContainerState();
|
||||
}
|
||||
|
||||
class _TrayContainerState extends State<TrayManager> with TrayListener {
|
||||
class _TrayContainerState extends ConsumerState<TrayManager> with TrayListener {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
trayManager.addListener(this);
|
||||
ref.listenManual(
|
||||
trayStateProvider,
|
||||
(prev, next) {
|
||||
if (prev != next) {
|
||||
globalState.appController.updateTray();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector4<AppState, AppFlowingState, Config, ClashConfig, TrayState>(
|
||||
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,
|
||||
);
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/providers/state.dart';
|
||||
import 'package:fl_clash/state.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;
|
||||
|
||||
const VpnManager({
|
||||
@@ -14,44 +15,33 @@ class VpnManager extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<VpnManager> createState() => _VpnContainerState();
|
||||
ConsumerState<VpnManager> createState() => _VpnContainerState();
|
||||
}
|
||||
|
||||
class _VpnContainerState extends State<VpnManager> {
|
||||
class _VpnContainerState extends ConsumerState<VpnManager> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref.listenManual(vpnStateProvider, (prev, next) {
|
||||
showTip();
|
||||
});
|
||||
}
|
||||
|
||||
showTip() {
|
||||
debouncer.call(
|
||||
DebounceTag.vpnTip,
|
||||
() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appFlowingState = globalState.appController.appFlowingState;
|
||||
if (appFlowingState.isStart) {
|
||||
globalState.showNotifier(
|
||||
appLocalizations.vpnTip,
|
||||
);
|
||||
}
|
||||
});
|
||||
if (ref.read(runTimeProvider.notifier).isStart) {
|
||||
globalState.showNotifier(
|
||||
appLocalizations.vpnTip,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector2<Config, ClashConfig, VPNState>(
|
||||
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,
|
||||
);
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.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_manager/window_manager.dart';
|
||||
|
||||
class WindowManager extends StatefulWidget {
|
||||
class WindowManager extends ConsumerStatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const WindowManager({
|
||||
@@ -18,17 +19,22 @@ class WindowManager extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<WindowManager> createState() => _WindowContainerState();
|
||||
ConsumerState<WindowManager> createState() => _WindowContainerState();
|
||||
}
|
||||
|
||||
class _WindowContainerState extends State<WindowManager>
|
||||
class _WindowContainerState extends ConsumerState<WindowManager>
|
||||
with WindowListener, WindowExtListener {
|
||||
Function? updateLaunchDebounce;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
_autoLaunchContainer(Widget child) {
|
||||
return Selector<Config, bool>(
|
||||
selector: (_, config) => config.appSetting.autoLaunch,
|
||||
shouldRebuild: (prev, next) {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref.listenManual(
|
||||
appSettingProvider.select((state) => state.autoLaunch),
|
||||
(prev, next) {
|
||||
if (prev != next) {
|
||||
debouncer.call(
|
||||
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);
|
||||
windowManager.addListener(this);
|
||||
}
|
||||
@@ -86,22 +77,24 @@ class _WindowContainerState extends State<WindowManager>
|
||||
Future<void> onWindowMoved() async {
|
||||
super.onWindowMoved();
|
||||
final offset = await windowManager.getPosition();
|
||||
final config = globalState.appController.config;
|
||||
config.windowProps = config.windowProps.copyWith(
|
||||
top: offset.dy,
|
||||
left: offset.dx,
|
||||
);
|
||||
ref.read(windowSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
top: offset.dy,
|
||||
left: offset.dx,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onWindowResized() async {
|
||||
super.onWindowResized();
|
||||
final size = await windowManager.getSize();
|
||||
final config = globalState.appController.config;
|
||||
config.windowProps = config.windowProps.copyWith(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
);
|
||||
ref.read(windowSettingProvider.notifier).updateState(
|
||||
(state) => state.copyWith(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -134,9 +127,9 @@ class WindowHeaderContainer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<AppState, int>(
|
||||
selector: (_, appState) => appState.version,
|
||||
builder: (_, version, child) {
|
||||
return Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final version = ref.watch(versionProvider);
|
||||
if (version <= 10 && Platform.isMacOS) {
|
||||
return child!;
|
||||
}
|
||||
|
||||
@@ -1,364 +1,41 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'common.dart';
|
||||
import 'core.dart';
|
||||
import 'profile.dart';
|
||||
|
||||
part 'generated/app.freezed.dart';
|
||||
|
||||
typedef DelayMap = Map<String, Map<String, int?>>;
|
||||
|
||||
class AppState with ChangeNotifier {
|
||||
List<NavigationItem> _navigationItems;
|
||||
bool _isInit;
|
||||
VersionInfo? _versionInfo;
|
||||
String _currentLabel;
|
||||
SystemColorSchemes _systemColorSchemes;
|
||||
num _sortNum;
|
||||
Mode _mode;
|
||||
DelayMap _delayMap;
|
||||
SelectedMap _selectedMap;
|
||||
List<Group> _groups;
|
||||
double _viewWidth;
|
||||
final FixedList<Connection> _requests;
|
||||
num _checkIpNum;
|
||||
List<ExternalProvider> _providers;
|
||||
List<Package> _packages;
|
||||
Brightness? _brightness;
|
||||
int _version;
|
||||
|
||||
AppState({
|
||||
required Mode mode,
|
||||
required SelectedMap selectedMap,
|
||||
@freezed
|
||||
class AppState with _$AppState {
|
||||
const factory AppState({
|
||||
@Default(false) bool isInit,
|
||||
@Default(PageLabel.dashboard) PageLabel pageLabel,
|
||||
@Default([]) List<Package> packages,
|
||||
@Default(ColorSchemes()) ColorSchemes colorSchemes,
|
||||
@Default(0) int sortNum,
|
||||
required double viewWidth,
|
||||
@Default({}) DelayMap delayMap,
|
||||
@Default([]) List<Group> groups,
|
||||
@Default(0) int checkIpNum,
|
||||
Brightness? brightness,
|
||||
int? runTime,
|
||||
@Default([]) List<ExternalProvider> providers,
|
||||
String? localIp,
|
||||
required FixedList<Connection> requests,
|
||||
required int version,
|
||||
})
|
||||
: _navigationItems = [],
|
||||
_isInit = false,
|
||||
_currentLabel = "dashboard",
|
||||
_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;
|
||||
|
||||
set navigationItems(List<NavigationItem> value) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
required FixedList<Log> logs,
|
||||
required FixedList<Traffic> traffics,
|
||||
required Traffic totalTraffic,
|
||||
}) = _AppState;
|
||||
}
|
||||
|
||||
class AppFlowingState with ChangeNotifier {
|
||||
int? _runTime;
|
||||
final FixedList<Log> _logs;
|
||||
List<Traffic> _traffics;
|
||||
Traffic _totalTraffic;
|
||||
String? _localIp;
|
||||
extension AppStateExt on AppState {
|
||||
ViewMode get viewMode => other.getViewMode(viewWidth);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
bool get isStart => runTime != null;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user