Compare commits

...

11 Commits

Author SHA1 Message Date
chen08209
de9c5ba9cc Optimize dashboard performance
Fix some issues
2025-03-10 18:41:42 +08:00
chen08209
2aae00cf68 Fix unselected proxy group delay issues 2025-03-10 18:41:42 +08:00
chen08209
68be2d34a1 Fix asn url issues 2025-03-08 04:22:32 +08:00
chen08209
7895ccf720 Update changelog 2025-03-07 16:03:59 +00:00
chen08209
e92900dbbd Fix tab delay view issues
Fix tray action issues

Fix get profile redirect client ua issues

Fix proxy card delay view issues

Add Russian, Japanese adaptation

Fix some issues
2025-03-07 23:49:27 +08:00
chen08209
eada271c49 Update changelog 2025-03-05 07:22:18 +00:00
chen08209
5dda2854be Fix list form input view issues
Fix traffic view issues
2025-03-05 15:11:19 +08:00
chen08209
5184ed6fc7 Update changelog 2025-03-05 02:36:31 +00:00
chen08209
4e679f776e Optimize performance
Update core

Optimize core stability

Fix linux tun authority check error

Fix some issues
2025-03-05 10:21:51 +08:00
chen08209
96328f66e9 Fix scroll physics error 2025-02-09 16:51:57 +08:00
chen08209
3eb14ab8a1 Update changelog 2025-02-09 08:36:14 +00:00
152 changed files with 15971 additions and 9059 deletions

View File

@@ -67,7 +67,6 @@ jobs:
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: 3.24.5
channel: stable channel: stable
cache: true cache: true

6
.gitmodules vendored
View File

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

View File

@@ -1,3 +1,55 @@
## v0.8.79
- Fix tab delay view issues
- Fix tray action issues
- Fix get profile redirect client ua issues
- Fix proxy card delay view issues
- Add Russian, Japanese adaptation
- Fix some issues
- Update changelog
## v0.8.78
- Fix list form input view issues
- Fix traffic view issues
- Update changelog
## v0.8.77
- Optimize performance
- Update core
- Optimize core stability
- Fix linux tun authority check error
- Fix some issues
- Fix scroll physics error
- Update changelog
## v0.8.75
- Add windows storage corruption detection
- Fix core crash caused by windows resource manager restart
- Optimize logs, requests, access to pages
- Fix macos bypass domain issues
- Update changelog
## v0.8.74 ## v0.8.74
- Fix some issues - Fix some issues

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

@@ -28,10 +28,18 @@ import (
"sync" "sync"
) )
func splitByComma(s string) interface{} {
parts := strings.Split(s, ",")
if len(parts) > 1 {
return parts
}
return s
}
var ( var (
isRunning = false isRunning = false
runLock sync.Mutex runLock sync.Mutex
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"} ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
) )
@@ -162,7 +170,17 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
func genHosts(hosts, patchHosts map[string]any) { func genHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts { for k, v := range patchHosts {
hosts[k] = v if str, ok := v.(string); ok {
hosts[k] = splitByComma(str)
}
}
}
func modPatchDns(dns *config.RawDNS) {
for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() {
if str, ok := pair.Value.(string); ok {
dns.NameServerPolicy.Set(pair.Key, splitByComma(str))
}
} }
} }
@@ -215,6 +233,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Tun.Device = patchConfig.Tun.Device targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
targetConfig.GeodataLoader = patchConfig.GeodataLoader targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl targetConfig.GeoXUrl = patchConfig.GeoXUrl
@@ -227,6 +246,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
} }
genHosts(targetConfig.Hosts, patchConfig.Hosts) genHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns { if configParams.OverrideDns {
modPatchDns(&patchConfig.DNS)
targetConfig.DNS = patchConfig.DNS targetConfig.DNS = patchConfig.DNS
} else { } else {
if targetConfig.DNS.Enable == false { if targetConfig.DNS.Enable == false {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension ColorExtension on Color { extension ColorExtension on Color {
Color get toLight { Color get toLight {
return withOpacity(0.8); return withOpacity(0.8);
} }
@@ -11,7 +10,7 @@ extension ColorExtension on Color {
} }
Color get toSoft { Color get toSoft {
return withOpacity(0.12); return withOpacity(0.15);
} }
Color get toLittle { Color get toLittle {
@@ -51,7 +50,7 @@ extension ColorExtension on Color {
} }
extension ColorSchemeExtension on ColorScheme { extension ColorSchemeExtension on ColorScheme {
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack ColorScheme toPureBlack(bool isPrueBlack) => isPrueBlack
? copyWith( ? copyWith(
surface: Colors.black, surface: Colors.black,
surfaceContainer: surfaceContainer.darken( surfaceContainer: surfaceContainer.darken(

View File

@@ -35,4 +35,5 @@ export 'tray.dart';
export 'window.dart'; export 'window.dart';
export 'windows.dart'; export 'windows.dart';
export 'render.dart'; export 'render.dart';
export 'view.dart'; export 'mixin.dart';
export 'print.dart';

View File

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

View File

@@ -63,7 +63,23 @@ class Throttler {
} }
} }
Future<T> retry<T>({
required Future<T> Function() task,
int maxAttempts = 3,
required bool Function(T res) retryIf,
Duration delay = Duration.zero,
}) async {
int attempts = 0;
while (attempts < maxAttempts) {
final res = await task();
if (!retryIf(res) || attempts >= maxAttempts) {
return res;
}
attempts++;
}
throw "unknown error";
}
final debouncer = Debouncer(); final debouncer = Debouncer();
final throttler = Throttler(); final throttler = Throttler();

View File

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

View File

@@ -1,26 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import '../state.dart';
import 'constant.dart';
class FlClashHttpOverrides extends HttpOverrides { class FlClashHttpOverrides extends HttpOverrides {
static String handleFindProxy(Uri url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
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";
}
@override @override
HttpClient createHttpClient(SecurityContext? context) { HttpClient createHttpClient(SecurityContext? context) {
final client = super.createHttpClient(context); final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true; client.findProxy = handleFindProxy;
client.findProxy = (url) {
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");
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
};
return client; return client;
} }
} }

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -4,18 +4,19 @@ import 'dart:convert';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'constant.dart'; import 'constant.dart';
class Preferences { class Preferences {
static Preferences? _instance; static Preferences? _instance;
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer(); Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
Future<bool> get isInit async => await sharedPreferencesCompleter.future != null; Future<bool> get isInit async =>
await sharedPreferencesCompleter.future != null;
Preferences._internal() { Preferences._internal() {
SharedPreferences.getInstance().then((value) => sharedPreferencesCompleter.complete(value)) SharedPreferences.getInstance()
.onError((_,__)=>sharedPreferencesCompleter.complete(null)); .then((value) => sharedPreferencesCompleter.complete(value))
.onError((_, __) => sharedPreferencesCompleter.complete(null));
} }
factory Preferences() { factory Preferences() {
@@ -23,7 +24,6 @@ class Preferences {
return _instance!; return _instance!;
} }
Future<ClashConfig?> getClashConfig() async { Future<ClashConfig?> getClashConfig() async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
final clashConfigString = preferences?.getString(clashConfigKey); final clashConfigString = preferences?.getString(clashConfigKey);
@@ -32,29 +32,26 @@ class Preferences {
return ClashConfig.fromJson(clashConfigMap); return ClashConfig.fromJson(clashConfigMap);
} }
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.setString(
clashConfigKey,
json.encode(clashConfig),
);
return true;
}
Future<Config?> getConfig() async { Future<Config?> getConfig() async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
final configString = preferences?.getString(configKey); final configString = preferences?.getString(configKey);
if (configString == null) return null; if (configString == null) return null;
final configMap = json.decode(configString); final configMap = json.decode(configString);
return Config.fromJson(configMap); return Config.compatibleFromJson(configMap);
} }
Future<bool> saveConfig(Config config) async { Future<bool> saveConfig(Config config) async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
return await preferences?.setString( return await preferences?.setString(
configKey, configKey,
json.encode(config), json.encode(config),
) ?? false; ) ??
false;
}
clearClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.remove(clashConfigKey);
} }
clearPreferences() async { clearPreferences() async {

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

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

View File

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

View File

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

View File

@@ -45,6 +45,13 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
} }
class NextClampingScrollPhysics extends ClampingScrollPhysics { class NextClampingScrollPhysics extends ClampingScrollPhysics {
const NextClampingScrollPhysics({super.parent});
@override
NextClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return NextClampingScrollPhysics(parent: buildParent(ancestor));
}
@override @override
Simulation? createBallisticSimulation( Simulation? createBallisticSimulation(
ScrollMetrics position, double velocity) { ScrollMetrics position, double velocity) {
@@ -115,9 +122,7 @@ class ReverseScrollPosition extends ScrollPositionWithSingleContext {
super.keepScrollOffset, super.keepScrollOffset,
super.oldPosition, super.oldPosition,
super.debugLabel, super.debugLabel,
}) : _initialPixels = initialPixels ?? 0; });
final double _initialPixels;
bool _isInit = false; bool _isInit = false;

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,21 +4,23 @@ import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'item.dart'; import 'item.dart';
class ConnectionsFragment extends StatefulWidget { class ConnectionsFragment extends ConsumerStatefulWidget {
const ConnectionsFragment({super.key}); const ConnectionsFragment({super.key});
@override @override
State<ConnectionsFragment> createState() => _ConnectionsFragmentState(); ConsumerState<ConnectionsFragment> createState() =>
_ConnectionsFragmentState();
} }
class _ConnectionsFragmentState extends State<ConnectionsFragment> class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
with ViewMixin { with PageMixin {
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>( final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
const ConnectionsState(), const ConnectionsState(),
); );
@@ -71,15 +73,20 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_updateConnections(); ref.listenManual(
} isCurrentPageProvider(
PageLabel.connections,
_initActions() { handler: (pageLabel, viewMode) =>
WidgetsBinding.instance.addPostFrameCallback( pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
(_) { ),
initViewState(); (prev, next) {
if (prev != next && next == true) {
initPageState();
}
}, },
fireImmediately: true,
); );
_updateConnections();
} }
_handleBlockConnection(String id) async { _handleBlockConnection(String id) async {
@@ -100,56 +107,44 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<AppState, bool?>( return ValueListenableBuilder<ConnectionsState>(
selector: (_, appState) => valueListenable: _connectionsStateNotifier,
appState.currentLabel == 'connections' || builder: (_, state, __) {
appState.viewMode == ViewMode.mobile && final connections = state.list;
appState.currentLabel == "tools", if (connections.isEmpty) {
builder: (_, isCurrent, child) { return NullStatus(
if (isCurrent == null || isCurrent) { label: appLocalizations.nullConnectionsDesc,
_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 CommonScrollBar(
controller: _scrollController,
child: ListView.separated(
controller: _scrollController,
itemBuilder: (_, index) {
final connection = connections[index];
return ConnectionItem(
key: Key(connection.id),
connection: connection,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
trailing: IconButton(
icon: const Icon(Icons.block),
onPressed: () {
_handleBlockConnection(connection.id);
},
),
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(
height: 0,
);
},
itemCount: connections.length,
),
);
},
); );
} }
} }

View File

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

View File

@@ -1,28 +1,25 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'item.dart'; import 'item.dart';
double _preOffset = 0; double _preOffset = 0;
class RequestsFragment extends StatefulWidget { class RequestsFragment extends ConsumerStatefulWidget {
const RequestsFragment({super.key}); const RequestsFragment({super.key});
@override @override
State<RequestsFragment> createState() => _RequestsFragmentState(); ConsumerState<RequestsFragment> createState() => _RequestsFragmentState();
} }
class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin { class _RequestsFragmentState extends ConsumerState<RequestsFragment>
with PageMixin {
final _requestsStateNotifier = final _requestsStateNotifier =
ValueNotifier<ConnectionsState>(const ConnectionsState()); ValueNotifier<ConnectionsState>(const ConnectionsState());
List<Connection> _requests = []; List<Connection> _requests = [];
@@ -51,18 +48,32 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final appController = globalState.appController;
final appState = appController.appState;
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith( _requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
connections: appState.requests, connections: globalState.appState.requests.list,
); );
}
_initActions() { ref.listenManual(
WidgetsBinding.instance.addPostFrameCallback( isCurrentPageProvider(
(_) { PageLabel.requests,
initViewState(); handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
}, },
fireImmediately: true,
);
ref.listenManual(
requestsProvider.select((state) => state.list),
(prev, next) {
if (!connectionListEquality.equals(prev, next)) {
_requests = next;
updateRequestsThrottler();
}
},
fireImmediately: true,
); );
} }
@@ -111,22 +122,6 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
super.dispose(); super.dispose();
} }
Widget _wrapPage(Widget child) {
return Selector<AppState, bool?>(
selector: (_, appState) =>
appState.currentLabel == 'requests' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions();
}
return child!;
},
child: child,
);
}
updateRequestsThrottler() { updateRequestsThrottler() {
throttler.call("request", () { throttler.call("request", () {
final isEquality = connectionListEquality.equals( final isEquality = connectionListEquality.equals(
@@ -144,93 +139,71 @@ class _RequestsFragmentState extends State<RequestsFragment> with ViewMixin {
}, duration: commonDuration); }, duration: commonDuration);
} }
Widget _wrapRequestsUpdate(Widget child) {
return Selector<AppState, List<Connection>>(
selector: (_, appState) => appState.requests,
shouldRebuild: (prev, next) {
final isEquality = connectionListEquality.equals(prev, next);
if (!isEquality) {
_requests = next;
updateRequestsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (_, constraints) { builder: (_, constraints) {
return FindProcessBuilder(builder: (value) { return FindProcessBuilder(builder: (value) {
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0)); _handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
return _wrapPage( return ValueListenableBuilder<ConnectionsState>(
_wrapRequestsUpdate( valueListenable: _requestsStateNotifier,
ValueListenableBuilder<ConnectionsState>( builder: (_, state, __) {
valueListenable: _requestsStateNotifier, final connections = state.list;
builder: (_, state, __) { if (connections.isEmpty) {
final connections = state.list; return NullStatus(
if (connections.isEmpty) { label: appLocalizations.nullRequestsDesc,
return NullStatus( );
label: appLocalizations.nullRequestsDesc, }
); final items = connections
} .map<Widget>(
final items = connections (connection) => ConnectionItem(
.map<Widget>( key: Key(connection.id),
(connection) => ConnectionItem( connection: connection,
key: Key(connection.id), onClick: (value) {
connection: connection, context.commonScaffoldState?.addKeyword(value);
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(
const Divider(
height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
}, },
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index, __) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
),
),
), ),
); )
}, .separated(
), const Divider(
), height: 0,
),
)
.toList();
return Align(
alignment: Alignment.topCenter,
child: NotificationListener<ScrollEndNotification>(
onNotification: (details) {
_preOffset = details.metrics.pixels;
return false;
},
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
reverse: true,
shrinkWrap: true,
physics: NextClampingScrollPhysics(),
controller: _scrollController,
itemExtentBuilder: (index, __) {
final widget = items[index];
if (widget.runtimeType == Divider) {
return 0;
}
final measure = globalState.measure;
final bodyMediumHeight = measure.bodyMediumHeight;
final connection = connections[(index / 2).floor()];
final height = _calcCacheHeight(connection);
return height + bodyMediumHeight + 32;
},
itemBuilder: (_, index) {
return items[index];
},
itemCount: items.length,
),
),
),
);
},
); );
}); });
}, },

View File

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

View File

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

View File

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

View File

@@ -47,6 +47,11 @@ class _MemoryInfoState extends State<MemoryInfo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final darkenLighter = context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1)
.toLighter;
final darken = context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1);
return SizedBox( return SizedBox(
height: getWidgetHeight(2), height: getWidgetHeight(2),
child: CommonCard( child: CommonCard(
@@ -57,12 +62,12 @@ class _MemoryInfoState extends State<MemoryInfo> {
onPressed: () { onPressed: () {
clashCore.requestGc(); clashCore.requestGc();
}, },
child: ValueListenableBuilder( child: Column(
valueListenable: _memoryInfoStateNotifier, children: [
builder: (_, trafficValue, __) { ValueListenableBuilder(
return Column( valueListenable: _memoryInfoStateNotifier,
children: [ builder: (_, trafficValue, __) {
Padding( return Padding(
padding: baseInfoEdgeInsets.copyWith( padding: baseInfoEdgeInsets.copyWith(
bottom: 0, bottom: 0,
top: 12, top: 12,
@@ -82,33 +87,30 @@ class _MemoryInfoState extends State<MemoryInfo> {
) )
], ],
), ),
), );
Flexible( },
child: Stack( ),
children: [ Flexible(
Positioned.fill( child: Stack(
child: WaveView( children: [
waveAmplitude: 12.0, Positioned.fill(
waveFrequency: 0.35, child: WaveView(
waveColor: context.colorScheme.secondaryContainer waveAmplitude: 12.0,
.blendDarken(context, factor: 0.1) waveFrequency: 0.35,
.toLighter, waveColor: darkenLighter,
), ),
),
Positioned.fill(
child: WaveView(
waveAmplitude: 12.0,
waveFrequency: 0.9,
waveColor: context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1),
),
),
],
), ),
) Positioned.fill(
], child: WaveView(
); waveAmplitude: 12.0,
}, waveFrequency: 0.9,
waveColor: darken,
),
),
],
),
),
],
), ),
), ),
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,15 +2,16 @@ import 'dart:math';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
class TrafficUsage extends StatelessWidget { class TrafficUsage extends StatelessWidget {
const TrafficUsage({super.key}); const TrafficUsage({super.key});
Widget getTrafficDataItem( Widget _buildTrafficDataItem(
BuildContext context, BuildContext context,
Icon icon, Icon icon,
TrafficValue trafficValue, TrafficValue trafficValue,
@@ -62,9 +63,9 @@ class TrafficUsage extends StatelessWidget {
iconData: Icons.data_saver_off, iconData: Icons.data_saver_off,
), ),
onPressed: () {}, onPressed: () {},
child: Selector<AppFlowingState, Traffic>( child: Consumer(
selector: (_, appFlowingState) => appFlowingState.totalTraffic, builder: (_, ref, __) {
builder: (_, totalTraffic, __) { final totalTraffic = ref.watch(totalTrafficProvider);
final upTotalTrafficValue = totalTraffic.up; final upTotalTrafficValue = totalTraffic.up;
final downTotalTrafficValue = totalTraffic.down; final downTotalTrafficValue = totalTraffic.down;
return Padding( return Padding(
@@ -188,7 +189,7 @@ class TrafficUsage extends StatelessWidget {
), ),
), ),
), ),
getTrafficDataItem( _buildTrafficDataItem(
context, context,
Icon( Icon(
Icons.arrow_upward, Icons.arrow_upward,
@@ -200,7 +201,7 @@ class TrafficUsage extends StatelessWidget {
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
getTrafficDataItem( _buildTrafficDataItem(
context, context,
Icon( Icon(
Icons.arrow_downward, Icons.arrow_downward,

View File

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

View File

@@ -1,24 +1,23 @@
import 'dart:async';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/models.dart'; import '../models/models.dart';
import '../widgets/widgets.dart'; import '../widgets/widgets.dart';
double _preOffset = 0; double _preOffset = 0;
class LogsFragment extends StatefulWidget { class LogsFragment extends ConsumerStatefulWidget {
const LogsFragment({super.key}); const LogsFragment({super.key});
@override @override
State<LogsFragment> createState() => _LogsFragmentState(); ConsumerState<LogsFragment> createState() => _LogsFragmentState();
} }
class _LogsFragmentState extends State<LogsFragment> with ViewMixin { class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState()); final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
final _scrollController = ScrollController( final _scrollController = ScrollController(
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite, initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
@@ -31,10 +30,34 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final appController = globalState.appController;
final appFlowingState = appController.appFlowingState;
_logsStateNotifier.value = _logsStateNotifier.value.copyWith( _logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: appFlowingState.logs, logs: globalState.appState.logs.list,
);
ref.listenManual(
logsProvider.select((state) => state.list),
(prev, next) {
if (prev != next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
}
},
fireImmediately: true,
);
ref.listenManual(
isCurrentPageProvider(
PageLabel.logs,
handler: (pageLabel, viewMode) =>
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
),
(prev, next) {
if (prev != next && next == true) {
initPageState();
}
},
fireImmediately: true,
); );
} }
@@ -93,12 +116,6 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
); );
} }
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
super.initViewState();
});
}
double _calcCacheHeight(String text) { double _calcCacheHeight(String text) {
final cacheHeight = _cacheDynamicHeightMap.get(text); final cacheHeight = _cacheDynamicHeightMap.get(text);
if (cacheHeight != null) { if (cacheHeight != null) {
@@ -140,98 +157,66 @@ class _LogsFragmentState extends State<LogsFragment> with ViewMixin {
}, duration: commonDuration); }, duration: commonDuration);
} }
Widget _wrapLogsUpdate(Widget child) {
return Selector<AppFlowingState, List<Log>>(
selector: (_, appFlowingState) => appFlowingState.logs,
shouldRebuild: (prev, next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
return !isEquality;
},
builder: (_, next, child) {
return child!;
},
child: child,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (_, constraints) { builder: (_, constraints) {
_handleTryClearCache(constraints.maxWidth - 40); _handleTryClearCache(constraints.maxWidth - 40);
return Selector<AppState, bool?>( return Align(
selector: (_, appState) => alignment: Alignment.topCenter,
appState.currentLabel == 'logs' || child: ValueListenableBuilder<LogsState>(
appState.viewMode == ViewMode.mobile && valueListenable: _logsStateNotifier,
appState.currentLabel == "tools", builder: (_, state, __) {
builder: (_, isCurrent, child) { final logs = state.list;
if (isCurrent == null || isCurrent) { if (logs.isEmpty) {
_initActions(); return NullStatus(
} label: appLocalizations.nullLogsDesc,
return child!; );
}, }
child: _wrapLogsUpdate( final items = logs
Align( .map<Widget>(
alignment: Alignment.topCenter, (log) => LogItem(
child: ValueListenableBuilder<LogsState>( key: Key(log.dateTime.toString()),
valueListenable: _logsStateNotifier, log: log,
builder: (_, state, __) { onClick: (value) {
final logs = state.list; context.commonScaffoldState?.addKeyword(value);
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,
),
), ),
); )
.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,
),
),
);
},
), ),
); );
}, },

View File

@@ -80,9 +80,9 @@ class _EditProfileState extends State<EditProfile> {
); );
} }
} }
appController.setProfile(await profile.saveFile(fileData!)); appController.setProfileAndAutoApply(await profile.saveFile(fileData!));
} else if (!hasUpdate) { } else if (!hasUpdate) {
appController.setProfile(profile); appController.setProfileAndAutoApply(profile);
} else { } else {
globalState.homeScaffoldKey.currentState?.loadingRun( globalState.homeScaffoldKey.currentState?.loadingRun(
() async { () async {

View File

@@ -0,0 +1,87 @@
import 'package:fl_clash/clash/core.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/card.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
class GenProfile extends StatefulWidget {
final String profileId;
const GenProfile({
super.key,
required this.profileId,
});
@override
State<GenProfile> createState() => _GenProfileState();
}
class _GenProfileState extends State<GenProfile> {
final _currentClashConfigNotifier = ValueNotifier<ClashConfigSnippet?>(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 Padding(
padding: EdgeInsets.all(16),
child: CustomScrollView(
slivers: [
SliverGrid.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 100,
mainAxisExtent: 50,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: clashConfig.proxyGroups.length,
itemBuilder: (BuildContext context, int index) {
return CommonCard(
onPressed: () {},
child: Text(
clashConfig.proxyGroups[index].name,
),
);
},
),
SliverList.builder(
itemBuilder: (BuildContext context, int index) {
final rule = clashConfig.rule[index];
return Text(
rule,
);
},
itemCount: clashConfig.rule.length,
)
],
),
);
},
),
title: "自定义",
);
}
}

View File

@@ -4,13 +4,14 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/profiles/edit_profile.dart'; import 'package:fl_clash/fragments/profiles/edit_profile.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart'; import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
import 'add_profile.dart'; import 'add_profile.dart';
import 'gen_profile.dart';
class ProfilesFragment extends StatefulWidget { class ProfilesFragment extends StatefulWidget {
const ProfilesFragment({super.key}); const ProfilesFragment({super.key});
@@ -19,7 +20,7 @@ class ProfilesFragment extends StatefulWidget {
State<ProfilesFragment> createState() => _ProfilesFragmentState(); State<ProfilesFragment> createState() => _ProfilesFragmentState();
} }
class _ProfilesFragmentState extends State<ProfilesFragment> { class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
Function? applyConfigDebounce; Function? applyConfigDebounce;
_handleShowAddExtendPage() { _handleShowAddExtendPage() {
@@ -33,21 +34,19 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
} }
_updateProfiles() async { _updateProfiles() async {
final appController = globalState.appController; final profiles = globalState.config.profiles;
final config = appController.config;
final profiles = appController.config.profiles;
final messages = []; final messages = [];
final updateProfiles = profiles.map<Future>( final updateProfiles = profiles.map<Future>(
(profile) async { (profile) async {
if (profile.type == ProfileType.file) return; if (profile.type == ProfileType.file) return;
config.setProfile( globalState.appController.setProfile(
profile.copyWith(isUpdating: true), profile.copyWith(isUpdating: true),
); );
try { try {
await appController.updateProfile(profile); await globalState.appController.updateProfile(profile);
} catch (e) { } catch (e) {
messages.add("${profile.label ?? profile.id}: $e \n"); messages.add("${profile.label ?? profile.id}: $e \n");
config.setProfile( globalState.appController.setProfile(
profile.copyWith( profile.copyWith(
isUpdating: false, isUpdating: false,
), ),
@@ -70,96 +69,90 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
} }
} }
_initScaffold() { @override
WidgetsBinding.instance.addPostFrameCallback( List<Widget> get actions => [
(_) { IconButton(
if (!mounted) return; onPressed: () {
final commonScaffoldState = context.commonScaffoldState; _updateProfiles();
commonScaffoldState?.actions = [ },
IconButton( icon: const Icon(Icons.sync),
onPressed: () { ),
_updateProfiles(); IconButton(
}, onPressed: () {
icon: const Icon(Icons.sync), final profiles = globalState.config.profiles;
), showSheet(
IconButton( title: appLocalizations.profilesSort,
onPressed: () { context: context,
final profiles = globalState.appController.config.profiles; body: SizedBox(
showSheet( height: 400,
title: appLocalizations.profilesSort, child: ReorderableProfiles(profiles: profiles),
context: context, ),
body: SizedBox( );
height: 400, },
child: ReorderableProfiles(profiles: profiles), icon: const Icon(Icons.sort),
), iconSize: 26,
); ),
}, ];
icon: const Icon(Icons.sort),
iconSize: 26, @override
), Widget? get floatingActionButton => FloatingActionButton(
]; heroTag: null,
commonScaffoldState?.floatingActionButton = FloatingActionButton( onPressed: _handleShowAddExtendPage,
heroTag: null, child: const Icon(
onPressed: _handleShowAddExtendPage, Icons.add,
child: const Icon( ),
Icons.add, );
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActiveBuilder( return Consumer(
label: "profiles", builder: (_, ref, __) {
builder: (isCurrent, child) { ref.listenManual(
if (isCurrent) { isCurrentPageProvider(PageLabel.profiles),
_initScaffold(); (prev, next) {
} if (prev != next && next == true) {
return child!; initPageState();
}, }
child: Selector2<AppState, Config, ProfilesSelectorState>( },
selector: (_, appState, config) => ProfilesSelectorState( fireImmediately: true,
profiles: config.profiles, );
currentProfileId: config.currentProfileId, final profilesSelectorState = ref.watch(profilesSelectorStateProvider);
columns: other.getProfilesColumns(appState.viewWidth), if (profilesSelectorState.profiles.isEmpty) {
), return NullStatus(
builder: (context, state, child) { label: appLocalizations.nullProfileDesc,
if (state.profiles.isEmpty) {
return NullStatus(
label: appLocalizations.nullProfileDesc,
);
}
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: state.columns,
children: [
for (int i = 0; i < state.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(state.profiles[i].id),
profile: state.profiles[i],
groupValue: state.currentProfileId,
onChanged: globalState.appController.changeProfile,
),
),
],
),
),
); );
}, }
), return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 88,
),
child: Grid(
mainAxisSpacing: 16,
crossAxisSpacing: 16,
crossAxisCount: profilesSelectorState.columns,
children: [
for (int i = 0; i < profilesSelectorState.profiles.length; i++)
GridItem(
child: ProfileItem(
key: Key(profilesSelectorState.profiles[i].id),
profile: profilesSelectorState.profiles[i],
groupValue: profilesSelectorState.currentProfileId,
onChanged: (profileId) {
ref.read(currentProfileIdProvider.notifier).value =
profileId;
},
),
),
],
),
),
);
},
); );
} }
} }
@@ -189,24 +182,19 @@ class ProfileItem extends StatelessWidget {
await globalState.appController.deleteProfile(profile.id); await globalState.appController.deleteProfile(profile.id);
} }
_handleUpdateProfile() async {
await globalState.safeRun<void>(updateProfile);
}
Future updateProfile() async { Future updateProfile() async {
final appController = globalState.appController; final appController = globalState.appController;
final config = appController.config;
if (profile.type == ProfileType.file) return; if (profile.type == ProfileType.file) return;
await globalState.safeRun(silence: false, () async { await globalState.safeRun(silence: false, () async {
try { try {
config.setProfile( appController.setProfile(
profile.copyWith( profile.copyWith(
isUpdating: true, isUpdating: true,
), ),
); );
await appController.updateProfile(profile); await appController.updateProfile(profile);
} catch (e) { } catch (e) {
config.setProfile( appController.setProfile(
profile.copyWith( profile.copyWith(
isUpdating: false, isUpdating: false,
), ),
@@ -256,16 +244,16 @@ class ProfileItem extends StatelessWidget {
]; ];
} }
_handleCopyLink(BuildContext context) async { // _handleCopyLink(BuildContext context) async {
await Clipboard.setData( // await Clipboard.setData(
ClipboardData( // ClipboardData(
text: profile.url, // text: profile.url,
), // ),
); // );
if (context.mounted) { // if (context.mounted) {
context.showNotifier(appLocalizations.copySuccess); // context.showNotifier(appLocalizations.copySuccess);
} // }
} // }
_handleExportFile(BuildContext context) async { _handleExportFile(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState; final commonScaffoldState = context.commonScaffoldState;
@@ -286,6 +274,15 @@ class ProfileItem extends StatelessWidget {
} }
} }
_handlePushGenProfilePage(BuildContext context, String id) {
BaseNavigator.push(
context,
GenProfile(
profileId: id,
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final key = GlobalKey<CommonPopupBoxState>(); final key = GlobalKey<CommonPopupBoxState>();
@@ -323,17 +320,24 @@ class ProfileItem extends StatelessWidget {
icon: Icons.sync_alt_sharp, icon: Icons.sync_alt_sharp,
label: appLocalizations.sync, label: appLocalizations.sync,
onPressed: () { onPressed: () {
_handleUpdateProfile(); updateProfile();
},
),
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: () {
// _handlePushGenProfilePage(context, profile.id);
// },
// ),
ActionItemData( ActionItemData(
icon: Icons.file_copy_outlined, icon: Icons.file_copy_outlined,
label: appLocalizations.exportFile, label: appLocalizations.exportFile,
@@ -499,7 +503,7 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
child: FilledButton.tonal( child: FilledButton.tonal(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
globalState.appController.config.profiles = profiles; globalState.appController.setProfiles(profiles);
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all( padding: WidgetStateProperty.all(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,6 +30,8 @@
"other": "Other", "other": "Other",
"about": "About", "about": "About",
"en": "English", "en": "English",
"ja": "Japanese",
"ru": "Russian",
"zh_CN": "Simplified Chinese", "zh_CN": "Simplified Chinese",
"theme": "Theme", "theme": "Theme",
"themeDesc": "Set dark mode,adjust the color", "themeDesc": "Set dark mode,adjust the color",
@@ -215,12 +217,12 @@
"go": "Go", "go": "Go",
"externalLink": "External link", "externalLink": "External link",
"otherContributors": "Other contributors", "otherContributors": "Other contributors",
"autoCloseConnections": "Auto lose connections", "autoCloseConnections": "Auto close connections",
"autoCloseConnectionsDesc": "Auto close connections after change node", "autoCloseConnectionsDesc": "Auto close connections after change node",
"onlyStatisticsProxy": "Only statistics proxy", "onlyStatisticsProxy": "Only statistics proxy",
"onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic", "onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic",
"deleteProfileTip": "Sure you want to delete the current profile?", "deleteProfileTip": "Sure you want to delete the current profile?",
"prueBlackMode": "Prue black mode", "pureBlackMode": "Pure black mode",
"keepAliveIntervalDesc": "Tcp keep alive interval", "keepAliveIntervalDesc": "Tcp keep alive interval",
"entries": " entries", "entries": " entries",
"local": "Local", "local": "Local",
@@ -342,5 +344,7 @@
"copySuccess": "Copy success", "copySuccess": "Copy success",
"copyLink": "Copy link", "copyLink": "Copy link",
"exportFile": "Export file", "exportFile": "Export file",
"cacheCorrupt": "The cache is corrupt. Do you want to clear it?" "cacheCorrupt": "The cache is corrupt. Do you want to clear it?",
"detectionTip": "Relying on third-party api is for reference only",
"listen": "Listen"
} }

350
lib/l10n/arb/intl_ja.arb Normal file
View File

@@ -0,0 +1,350 @@
{
"rule": "ルール",
"global": "グローバル",
"direct": "ダイレクト",
"dashboard": "ダッシュボード",
"proxies": "プロキシ",
"profile": "プロファイル",
"profiles": "プロファイル一覧",
"tools": "ツール",
"logs": "ログ",
"logsDesc": "ログキャプチャ記録",
"resources": "リソース",
"resourcesDesc": "外部リソース関連情報",
"trafficUsage": "トラフィック使用量",
"coreInfo": "コア情報",
"nullCoreInfoDesc": "コア情報を取得できません",
"networkSpeed": "ネットワーク速度",
"outboundMode": "アウトバウンドモード",
"networkDetection": "ネットワーク検出",
"upload": "アップロード",
"download": "ダウンロード",
"noProxy": "プロキシなし",
"noProxyDesc": "プロファイルを作成するか、有効なプロファイルを追加してください",
"nullProfileDesc": "プロファイルがありません。追加してください",
"nullLogsDesc": "ログがありません",
"settings": "設定",
"language": "言語",
"defaultText": "デフォルト",
"more": "詳細",
"other": "その他",
"about": "について",
"en": "英語",
"ja": "日本語",
"ru": "ロシア語",
"zh_CN": "簡体字中国語",
"theme": "テーマ",
"themeDesc": "ダークモードの設定、色の調整",
"override": "上書き",
"overrideDesc": "プロキシ関連設定を上書き",
"allowLan": "LANを許可",
"allowLanDesc": "LAN経由でのプロキシアクセスを許可",
"tun": "TUN",
"tunDesc": "管理者モードでのみ有効",
"minimizeOnExit": "終了時に最小化",
"minimizeOnExitDesc": "システムの終了イベントを変更",
"autoLaunch": "自動起動",
"autoLaunchDesc": "システムの自動起動に従う",
"silentLaunch": "バックグラウンド起動",
"silentLaunchDesc": "バックグラウンドで起動",
"autoRun": "自動実行",
"autoRunDesc": "アプリ起動時に自動実行",
"logcat": "ログキャット",
"logcatDesc": "無効化するとログエントリを非表示",
"autoCheckUpdate": "自動更新チェック",
"autoCheckUpdateDesc": "起動時に更新を自動チェック",
"accessControl": "アクセス制御",
"accessControlDesc": "アプリケーションのプロキシアクセスを設定",
"application": "アプリケーション",
"applicationDesc": "アプリ関連設定を変更",
"edit": "編集",
"confirm": "確認",
"update": "更新",
"add": "追加",
"save": "保存",
"delete": "削除",
"years": "年",
"months": "月",
"hours": "時間",
"days": "日",
"minutes": "分",
"seconds": "秒",
"ago": "前",
"just": "たった今",
"qrcode": "QRコード",
"qrcodeDesc": "QRコードをスキャンしてプロファイルを取得",
"url": "URL",
"urlDesc": "URL経由でプロファイルを取得",
"file": "ファイル",
"fileDesc": "プロファイルを直接アップロード",
"name": "名前",
"profileNameNullValidationDesc": "プロファイル名を入力してください",
"profileUrlNullValidationDesc": "プロファイルURLを入力してください",
"profileUrlInvalidValidationDesc": "有効なプロファイルURLを入力してください",
"autoUpdate": "自動更新",
"autoUpdateInterval": "自動更新間隔(分)",
"profileAutoUpdateIntervalNullValidationDesc": "自動更新間隔を入力してください",
"profileAutoUpdateIntervalInvalidValidationDesc": "有効な間隔形式を入力してください",
"themeMode": "テーマモード",
"themeColor": "テーマカラー",
"preview": "プレビュー",
"auto": "自動",
"light": "ライト",
"dark": "ダーク",
"importFromURL": "URLからインポート",
"submit": "送信",
"doYouWantToPass": "通過させますか?",
"create": "作成",
"defaultSort": "デフォルト順",
"delaySort": "遅延順",
"nameSort": "名前順",
"pleaseUploadFile": "ファイルをアップロードしてください",
"pleaseUploadValidQrcode": "有効なQRコードをアップロードしてください",
"blacklistMode": "ブラックリストモード",
"whitelistMode": "ホワイトリストモード",
"filterSystemApp": "システムアプリを除外",
"cancelFilterSystemApp": "システムアプリの除外を解除",
"selectAll": "すべて選択",
"cancelSelectAll": "全選択解除",
"appAccessControl": "アプリアクセス制御",
"accessControlAllowDesc": "選択したアプリのみVPNを許可",
"accessControlNotAllowDesc": "選択したアプリをVPNから除外",
"selected": "選択済み",
"unableToUpdateCurrentProfileDesc": "現在のプロファイルを更新できません",
"noMoreInfoDesc": "追加情報なし",
"profileParseErrorDesc": "プロファイル解析エラー",
"proxyPort": "プロキシポート",
"proxyPortDesc": "Clashのリスニングポートを設定",
"port": "ポート",
"logLevel": "ログレベル",
"show": "表示",
"exit": "終了",
"systemProxy": "システムプロキシ",
"project": "プロジェクト",
"core": "コア",
"tabAnimation": "タブアニメーション",
"tabAnimationDesc": "有効化するとホームタブに切り替えアニメーションを追加",
"desc": "ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。",
"startVpn": "VPNを開始中...",
"stopVpn": "VPNを停止中...",
"discovery": "新しいバージョンを発見",
"compatible": "互換モード",
"compatibleDesc": "有効化すると一部機能を失いますが、Clashの完全サポートを獲得",
"notSelectedTip": "現在のプロキシグループは選択できません",
"tip": "ヒント",
"backupAndRecovery": "バックアップと復元",
"backupAndRecoveryDesc": "WebDAVまたはファイルでデータを同期",
"account": "アカウント",
"backup": "バックアップ",
"recovery": "復元",
"recoveryProfiles": "プロファイルのみ復元",
"recoveryAll": "全データ復元",
"recoverySuccess": "復元成功",
"backupSuccess": "バックアップ成功",
"noInfo": "情報なし",
"pleaseBindWebDAV": "WebDAVをバインドしてください",
"bind": "バインド",
"connectivity": "接続性:",
"webDAVConfiguration": "WebDAV設定",
"address": "アドレス",
"addressHelp": "WebDAVサーバーアドレス",
"addressTip": "有効なWebDAVアドレスを入力",
"password": "パスワード",
"passwordTip": "パスワードは必須です",
"accountTip": "アカウントは必須です",
"checkUpdate": "更新を確認",
"discoverNewVersion": "新バージョンを発見",
"checkUpdateError": "アプリは最新版です",
"goDownload": "ダウンロードへ",
"unknown": "不明",
"geoData": "地域データ",
"externalResources": "外部リソース",
"checking": "確認中...",
"country": "国",
"checkError": "確認エラー",
"search": "検索",
"allowBypass": "アプリがVPNをバイパスすることを許可",
"allowBypassDesc": "有効化すると一部アプリがVPNをバイパス",
"externalController": "外部コントローラー",
"externalControllerDesc": "有効化するとClashコアをポート9090で制御可能",
"ipv6Desc": "有効化するとIPv6トラフィックを受信可能",
"app": "アプリ",
"general": "一般",
"vpnSystemProxyDesc": "HTTPプロキシをVpnServiceに接続",
"systemProxyDesc": "HTTPプロキシをVpnServiceに接続",
"unifiedDelay": "統一遅延",
"unifiedDelayDesc": "ハンドシェイクなどの余分な遅延を削除",
"tcpConcurrent": "TCP並列処理",
"tcpConcurrentDesc": "TCP並列処理を許可",
"geodataLoader": "Geo低メモリモード",
"geodataLoaderDesc": "有効化するとGeo低メモリローダーを使用",
"requests": "リクエスト",
"requestsDesc": "最近のリクエスト記録を表示",
"findProcessMode": "プロセス検出",
"findProcessModeDesc": "有効化するとフラッシュバックのリスクあり",
"init": "初期化",
"infiniteTime": "長期有効",
"expirationTime": "有効期限",
"connections": "接続",
"connectionsDesc": "現在の接続データを表示",
"nullRequestsDesc": "リクエストなし",
"nullConnectionsDesc": "接続なし",
"intranetIP": "イントラネットIP",
"view": "表示",
"cut": "切り取り",
"copy": "コピー",
"paste": "貼り付け",
"testUrl": "URLテスト",
"sync": "同期",
"exclude": "最近のタスクから非表示",
"excludeDesc": "アプリがバックグラウンド時に最近のタスクから非表示",
"oneColumn": "1列",
"twoColumns": "2列",
"threeColumns": "3列",
"fourColumns": "4列",
"expand": "標準",
"shrink": "縮小",
"min": "最小化",
"tab": "タブ",
"list": "リスト",
"delay": "遅延",
"style": "スタイル",
"size": "サイズ",
"sort": "並び替え",
"columns": "列",
"proxiesSetting": "プロキシ設定",
"proxyGroup": "プロキシグループ",
"go": "移動",
"externalLink": "外部リンク",
"otherContributors": "その他の貢献者",
"autoCloseConnections": "接続を自動閉じる",
"autoCloseConnectionsDesc": "ノード変更後に接続を自動閉じる",
"onlyStatisticsProxy": "プロキシのみ統計",
"onlyStatisticsProxyDesc": "有効化するとプロキシトラフィックのみ統計",
"deleteProfileTip": "現在のプロファイルを削除しますか?",
"pureBlackMode": "純黒モード",
"keepAliveIntervalDesc": "TCPキープアライブ間隔",
"entries": " エントリ",
"local": "ローカル",
"remote": "リモート",
"remoteBackupDesc": "WebDAVにデータをバックアップ",
"remoteRecoveryDesc": "WebDAVからデータを復元",
"localBackupDesc": "ローカルにデータをバックアップ",
"localRecoveryDesc": "ファイルからデータを復元",
"mode": "モード",
"time": "時間",
"source": "ソース",
"allApps": "全アプリ",
"onlyOtherApps": "サードパーティアプリのみ",
"action": "アクション",
"intelligentSelected": "インテリジェント選択",
"clipboardImport": "クリップボードからインポート",
"clipboardExport": "クリップボードにエクスポート",
"layout": "レイアウト",
"tight": "密",
"standard": "標準",
"loose": "疎",
"profilesSort": "プロファイルの並び替え",
"start": "開始",
"stop": "停止",
"appDesc": "アプリ関連設定の処理",
"vpnDesc": "VPN関連設定の変更",
"generalDesc": "一般設定の上書き",
"dnsDesc": "DNS関連設定の更新",
"key": "キー",
"value": "値",
"notEmpty": "空欄不可",
"hostsDesc": "ホストを追加",
"vpnTip": "変更はVPN再起動後に有効",
"vpnEnableDesc": "VpnService経由で全システムトラフィックをルーティング",
"options": "オプション",
"loopback": "ループバック解除ツール",
"loopbackDesc": "UWPループバック解除用",
"providers": "プロバイダー",
"proxyProviders": "プロキシプロバイダー",
"ruleProviders": "ルールプロバイダー",
"overrideDns": "DNS上書き",
"overrideDnsDesc": "有効化するとプロファイルのDNS設定を上書き",
"status": "ステータス",
"statusDesc": "無効時はシステムDNSを使用",
"preferH3Desc": "DOHのHTTP/3を優先使用",
"respectRules": "ルール尊重",
"respectRulesDesc": "DNS接続がルールに従うproxy-server-nameserverの設定が必要",
"dnsMode": "DNSモード",
"fakeipRange": "Fakeip範囲",
"fakeipFilter": "Fakeipフィルター",
"defaultNameserver": "デフォルトネームサーバー",
"defaultNameserverDesc": "DNSサーバーの解決用",
"nameserver": "ネームサーバー",
"nameserverDesc": "ドメイン解決用",
"useHosts": "ホストを使用",
"useSystemHosts": "システムホストを使用",
"nameserverPolicy": "ネームサーバーポリシー",
"nameserverPolicyDesc": "対応するネームサーバーポリシーを指定",
"proxyNameserver": "プロキシネームサーバー",
"proxyNameserverDesc": "プロキシノード解決用ドメイン",
"fallback": "フォールバック",
"fallbackDesc": "通常はオフショアDNSを使用",
"fallbackFilter": "フォールバックフィルター",
"geoipCode": "GeoIPコード",
"ipcidr": "IPCIDR",
"domain": "ドメイン",
"reset": "リセット",
"action_view": "表示/非表示",
"action_start": "開始/停止",
"action_mode": "モード切替",
"action_proxy": "システムプロキシ",
"action_tun": "TUN",
"disclaimer": "免責事項",
"disclaimerDesc": "本ソフトウェアは学習交流や科学研究などの非営利目的でのみ使用されます。商用利用は厳禁です。いかなる商用活動も本ソフトウェアとは無関係です。",
"agree": "同意",
"hotkeyManagement": "ホットキー管理",
"hotkeyManagementDesc": "キーボードでアプリを制御",
"pressKeyboard": "キーボードを押してください",
"inputCorrectHotkey": "正しいホットキーを入力",
"hotkeyConflict": "ホットキー競合",
"remove": "削除",
"noHotKey": "ホットキーなし",
"noNetwork": "ネットワークなし",
"ipv6InboundDesc": "IPv6インバウンドを許可",
"exportLogs": "ログをエクスポート",
"exportSuccess": "エクスポート成功",
"iconStyle": "アイコンスタイル",
"onlyIcon": "アイコンのみ",
"noIcon": "なし",
"stackMode": "スタックモード",
"network": "ネットワーク",
"networkDesc": "ネットワーク関連設定の変更",
"bypassDomain": "バイパスドメイン",
"bypassDomainDesc": "システムプロキシ有効時のみ適用",
"resetTip": "リセットを確定",
"regExp": "正規表現",
"icon": "アイコン",
"iconConfiguration": "アイコン設定",
"noData": "データなし",
"adminAutoLaunch": "管理者自動起動",
"adminAutoLaunchDesc": "管理者モードで起動",
"fontFamily": "フォントファミリー",
"systemFont": "システムフォント",
"toggle": "トグル",
"system": "システム",
"routeMode": "ルートモード",
"routeMode_bypassPrivate": "プライベートルートをバイパス",
"routeMode_config": "設定を使用",
"routeAddress": "ルートアドレス",
"routeAddressDesc": "ルートアドレスを設定",
"pleaseInputAdminPassword": "管理者パスワードを入力",
"copyEnvVar": "環境変数をコピー",
"memoryInfo": "メモリ情報",
"cancel": "キャンセル",
"fileIsUpdate": "ファイルが変更されました。保存しますか?",
"profileHasUpdate": "プロファイルが変更されました。自動更新を無効化しますか?",
"hasCacheChange": "変更をキャッシュしますか?",
"nullProxies": "プロキシなし",
"copySuccess": "コピー成功",
"copyLink": "リンクをコピー",
"exportFile": "ファイルをエクスポート",
"cacheCorrupt": "キャッシュが破損しています。クリアしますか?",
"detectionTip": "サードパーティAPIに依存参考値",
"listen": "リスン"
}

350
lib/l10n/arb/intl_ru.arb Normal file
View File

@@ -0,0 +1,350 @@
{
"rule": "Правило",
"global": "Глобальный",
"direct": "Прямой",
"dashboard": "Панель управления",
"proxies": "Прокси",
"profile": "Профиль",
"profiles": "Профили",
"tools": "Инструменты",
"logs": "Логи",
"logsDesc": "Записи захвата логов",
"resources": "Ресурсы",
"resourcesDesc": "Информация, связанная с внешними ресурсами",
"trafficUsage": "Использование трафика",
"coreInfo": "Информация о ядре",
"nullCoreInfoDesc": "Не удалось получить информацию о ядре",
"networkSpeed": "Скорость сети",
"outboundMode": "Режим исходящего трафика",
"networkDetection": "Обнаружение сети",
"upload": "Загрузка",
"download": "Скачивание",
"noProxy": "Нет прокси",
"noProxyDesc": "Пожалуйста, создайте профиль или добавьте действительный профиль",
"nullProfileDesc": "Нет профиля, пожалуйста, добавьте профиль",
"nullLogsDesc": "Нет логов",
"settings": "Настройки",
"language": "Язык",
"defaultText": "По умолчанию",
"more": "Еще",
"other": "Другое",
"about": "О программе",
"en": "Английский",
"ja": "Японский",
"ru": "Русский",
"zh_CN": "Упрощенный китайский",
"theme": "Тема",
"themeDesc": "Установить темный режим, настроить цвет",
"override": "Переопределить",
"overrideDesc": "Переопределить конфигурацию, связанную с прокси",
"allowLan": "Разрешить LAN",
"allowLanDesc": "Разрешить доступ к прокси через локальную сеть",
"tun": "TUN",
"tunDesc": "действительно только в режиме администратора",
"minimizeOnExit": "Свернуть при выходе",
"minimizeOnExitDesc": "Изменить стандартное событие выхода из системы",
"autoLaunch": "Автозапуск",
"autoLaunchDesc": "Следовать автозапуску системы",
"silentLaunch": "Тихий запуск",
"silentLaunchDesc": "Запуск в фоновом режиме",
"autoRun": "Автозапуск",
"autoRunDesc": "Автоматический запуск при открытии приложения",
"logcat": "Logcat",
"logcatDesc": "Отключение скроет запись логов",
"autoCheckUpdate": "Автопроверка обновлений",
"autoCheckUpdateDesc": "Автоматически проверять обновления при запуске приложения",
"accessControl": "Контроль доступа",
"accessControlDesc": "Настройка доступа приложений к прокси",
"application": "Приложение",
"applicationDesc": "Изменение настроек, связанных с приложением",
"edit": "Редактировать",
"confirm": "Подтвердить",
"update": "Обновить",
"add": "Добавить",
"save": "Сохранить",
"delete": "Удалить",
"years": "Лет",
"months": "Месяцев",
"hours": "Часов",
"days": "Дней",
"minutes": "Минут",
"seconds": "Секунд",
"ago": " назад",
"just": "Только что",
"qrcode": "QR-код",
"qrcodeDesc": "Сканируйте QR-код для получения профиля",
"url": "URL",
"urlDesc": "Получить профиль через URL",
"file": "Файл",
"fileDesc": "Прямая загрузка профиля",
"name": "Имя",
"profileNameNullValidationDesc": "Пожалуйста, введите имя профиля",
"profileUrlNullValidationDesc": "Пожалуйста, введите URL профиля",
"profileUrlInvalidValidationDesc": "Пожалуйста, введите действительный URL профиля",
"autoUpdate": "Автообновление",
"autoUpdateInterval": "Интервал автообновления (минуты)",
"profileAutoUpdateIntervalNullValidationDesc": "Пожалуйста, введите интервал времени для автообновления",
"profileAutoUpdateIntervalInvalidValidationDesc": "Пожалуйста, введите действительный формат интервала времени",
"themeMode": "Режим темы",
"themeColor": "Цвет темы",
"preview": "Предпросмотр",
"auto": "Авто",
"light": "Светлый",
"dark": "Темный",
"importFromURL": "Импорт из URL",
"submit": "Отправить",
"doYouWantToPass": "Вы хотите пропустить",
"create": "Создать",
"defaultSort": "Сортировка по умолчанию",
"delaySort": "Сортировка по задержке",
"nameSort": "Сортировка по имени",
"pleaseUploadFile": "Пожалуйста, загрузите файл",
"pleaseUploadValidQrcode": "Пожалуйста, загрузите действительный QR-код",
"blacklistMode": "Режим черного списка",
"whitelistMode": "Режим белого списка",
"filterSystemApp": "Фильтровать системные приложения",
"cancelFilterSystemApp": "Отменить фильтрацию системных приложений",
"selectAll": "Выбрать все",
"cancelSelectAll": "Отменить выбор всего",
"appAccessControl": "Контроль доступа приложений",
"accessControlAllowDesc": "Разрешить только выбранным приложениям доступ к VPN",
"accessControlNotAllowDesc": "Выбранные приложения будут исключены из VPN",
"selected": "Выбрано",
"unableToUpdateCurrentProfileDesc": "невозможно обновить текущий профиль",
"noMoreInfoDesc": "Нет дополнительной информации",
"profileParseErrorDesc": "ошибка разбора профиля",
"proxyPort": "Порт прокси",
"proxyPortDesc": "Установить порт прослушивания Clash",
"port": "Порт",
"logLevel": "Уровень логов",
"show": "Показать",
"exit": "Выход",
"systemProxy": "Системный прокси",
"project": "Проект",
"core": "Ядро",
"tabAnimation": "Анимация вкладок",
"tabAnimationDesc": "При включении домашняя вкладка добавит анимацию переключения",
"desc": "Многоплатформенный прокси-клиент на основе ClashMeta, простой и удобный в использовании, с открытым исходным кодом и без рекламы.",
"startVpn": "Запуск VPN...",
"stopVpn": "Остановка VPN...",
"discovery": "Обнаружена новая версия",
"compatible": "Режим совместимости",
"compatibleDesc": "Включение приведет к потере части функциональности приложения, но обеспечит полную поддержку Clash.",
"notSelectedTip": "Текущая группа прокси не может быть выбрана.",
"tip": "подсказка",
"backupAndRecovery": "Резервное копирование и восстановление",
"backupAndRecoveryDesc": "Синхронизация данных через WebDAV или файл",
"account": "Аккаунт",
"backup": "Резервное копирование",
"recovery": "Восстановление",
"recoveryProfiles": "Только восстановление профилей",
"recoveryAll": "Восстановить все данные",
"recoverySuccess": "Восстановление успешно",
"backupSuccess": "Резервное копирование успешно",
"noInfo": "Нет информации",
"pleaseBindWebDAV": "Пожалуйста, привяжите WebDAV",
"bind": "Привязать",
"connectivity": "Связь:",
"webDAVConfiguration": "Конфигурация WebDAV",
"address": "Адрес",
"addressHelp": "Адрес сервера WebDAV",
"addressTip": "Пожалуйста, введите действительный адрес WebDAV",
"password": "Пароль",
"passwordTip": "Пароль не может быть пустым",
"accountTip": "Аккаунт не может быть пустым",
"checkUpdate": "Проверить обновления",
"discoverNewVersion": "Обнаружена новая версия",
"checkUpdateError": "Текущее приложение уже является последней версией",
"goDownload": "Перейти к загрузке",
"unknown": "Неизвестно",
"geoData": "Геоданные",
"externalResources": "Внешние ресурсы",
"checking": "Проверка...",
"country": "Страна",
"checkError": "Ошибка проверки",
"search": "Поиск",
"allowBypass": "Разрешить приложениям обходить VPN",
"allowBypassDesc": "Некоторые приложения могут обходить VPN при включении",
"externalController": "Внешний контроллер",
"externalControllerDesc": "При включении ядро Clash можно контролировать на порту 9090",
"ipv6Desc": "При включении будет возможно получать IPv6 трафик",
"app": "Приложение",
"general": "Общие",
"vpnSystemProxyDesc": "Прикрепить HTTP-прокси к VpnService",
"systemProxyDesc": "Прикрепить HTTP-прокси к VpnService",
"unifiedDelay": "Унифицированная задержка",
"unifiedDelayDesc": "Убрать дополнительные задержки, такие как рукопожатие",
"tcpConcurrent": "TCP параллелизм",
"tcpConcurrentDesc": "Включение позволит использовать параллелизм TCP",
"geodataLoader": "Режим низкого потребления памяти для геоданных",
"geodataLoaderDesc": "Включение будет использовать загрузчик геоданных с низким потреблением памяти",
"requests": "Запросы",
"requestsDesc": "Просмотр последних записей запросов",
"findProcessMode": "Режим поиска процесса",
"findProcessModeDesc": "Есть риск сбоя после включения",
"init": "Инициализация",
"infiniteTime": "Долгосрочное действие",
"expirationTime": "Время истечения",
"connections": "Соединения",
"connectionsDesc": "Просмотр текущих данных о соединениях",
"nullRequestsDesc": "Нет запросов",
"nullConnectionsDesc": "Нет соединений",
"intranetIP": "Внутренний IP",
"view": "Просмотр",
"cut": "Вырезать",
"copy": "Копировать",
"paste": "Вставить",
"testUrl": "Тест URL",
"sync": "Синхронизация",
"exclude": "Скрыть из последних задач",
"excludeDesc": "Когда приложение находится в фоновом режиме, оно скрыто из последних задач",
"oneColumn": "Один столбец",
"twoColumns": "Два столбца",
"threeColumns": "Три столбца",
"fourColumns": "Четыре столбца",
"expand": "Стандартный",
"shrink": "Сжать",
"min": "Мин",
"tab": "Вкладка",
"list": "Список",
"delay": "Задержка",
"style": "Стиль",
"size": "Размер",
"sort": "Сортировка",
"columns": "Столбцы",
"proxiesSetting": "Настройка прокси",
"proxyGroup": "Группа прокси",
"go": "Перейти",
"externalLink": "Внешняя ссылка",
"otherContributors": "Другие участники",
"autoCloseConnections": "Автоматическое закрытие соединений",
"autoCloseConnectionsDesc": "Автоматически закрывать соединения после смены узла",
"onlyStatisticsProxy": "Только статистика прокси",
"onlyStatisticsProxyDesc": "При включении будет учитываться только трафик прокси",
"deleteProfileTip": "Вы уверены, что хотите удалить текущий профиль?",
"pureBlackMode": "Чисто черный режим",
"keepAliveIntervalDesc": "Интервал поддержания TCP-соединения",
"entries": " записей",
"local": "Локальный",
"remote": "Удаленный",
"remoteBackupDesc": "Резервное копирование локальных данных на WebDAV",
"remoteRecoveryDesc": "Восстановление данных с WebDAV",
"localBackupDesc": "Резервное копирование локальных данных на локальный диск",
"localRecoveryDesc": "Восстановление данных из файла",
"mode": "Режим",
"time": "Время",
"source": "Источник",
"allApps": "Все приложения",
"onlyOtherApps": "Только сторонние приложения",
"action": "Действие",
"intelligentSelected": "Интеллектуальный выбор",
"clipboardImport": "Импорт из буфера обмена",
"clipboardExport": "Экспорт в буфер обмена",
"layout": "Макет",
"tight": "Плотный",
"standard": "Стандартный",
"loose": "Свободный",
"profilesSort": "Сортировка профилей",
"start": "Старт",
"stop": "Стоп",
"appDesc": "Обработка настроек, связанных с приложением",
"vpnDesc": "Изменение настроек, связанных с VPN",
"generalDesc": "Переопределение общих настроек",
"dnsDesc": "Обновление настроек, связанных с DNS",
"key": "Ключ",
"value": "Значение",
"notEmpty": "Не может быть пустым",
"hostsDesc": "Добавить Hosts",
"vpnTip": "Изменения вступят в силу после перезапуска VPN",
"vpnEnableDesc": "Автоматически направляет весь системный трафик через VpnService",
"options": "Опции",
"loopback": "Инструмент разблокировки Loopback",
"loopbackDesc": "Используется для разблокировки Loopback UWP",
"providers": "Провайдеры",
"proxyProviders": "Провайдеры прокси",
"ruleProviders": "Провайдеры правил",
"overrideDns": "Переопределить DNS",
"overrideDnsDesc": "Включение переопределит настройки DNS в профиле",
"status": "Статус",
"statusDesc": "Системный DNS будет использоваться при выключении",
"preferH3Desc": "Приоритетное использование HTTP/3 для DOH",
"respectRules": "Соблюдение правил",
"respectRulesDesc": "DNS-соединение следует правилам, необходимо настроить proxy-server-nameserver",
"dnsMode": "Режим DNS",
"fakeipRange": "Диапазон Fakeip",
"fakeipFilter": "Фильтр Fakeip",
"defaultNameserver": "Сервер имен по умолчанию",
"defaultNameserverDesc": "Для разрешения DNS-сервера",
"nameserver": "Сервер имен",
"nameserverDesc": "Для разрешения домена",
"useHosts": "Использовать hosts",
"useSystemHosts": "Использовать системные hosts",
"nameserverPolicy": "Политика сервера имен",
"nameserverPolicyDesc": "Указать соответствующую политику сервера имен",
"proxyNameserver": "Прокси-сервер имен",
"proxyNameserverDesc": "Домен для разрешения прокси-узлов",
"fallback": "Резервный",
"fallbackDesc": "Обычно используется оффшорный DNS",
"fallbackFilter": "Фильтр резервного DNS",
"geoipCode": "Код Geoip",
"ipcidr": "IPCIDR",
"domain": "Домен",
"reset": "Сброс",
"action_view": "Показать/Скрыть",
"action_start": "Старт/Стоп",
"action_mode": "Переключить режим",
"action_proxy": "Системный прокси",
"action_tun": "TUN",
"disclaimer": "Отказ от ответственности",
"disclaimerDesc": "Это программное обеспечение используется только в некоммерческих целях, таких как учебные обмены и научные исследования. Запрещено использовать это программное обеспечение в коммерческих целях. Любая коммерческая деятельность, если таковая имеется, не имеет отношения к этому программному обеспечению.",
"agree": "Согласен",
"hotkeyManagement": "Управление горячими клавишами",
"hotkeyManagementDesc": "Использование клавиатуры для управления приложением",
"pressKeyboard": "Пожалуйста, нажмите клавишу.",
"inputCorrectHotkey": "Пожалуйста, введите правильную горячую клавишу",
"hotkeyConflict": "Конфликт горячих клавиш",
"remove": "Удалить",
"noHotKey": "Нет горячей клавиши",
"noNetwork": "Нет сети",
"ipv6InboundDesc": "Разрешить входящий IPv6",
"exportLogs": "Экспорт логов",
"exportSuccess": "Экспорт успешен",
"iconStyle": "Стиль иконки",
"onlyIcon": "Только иконка",
"noIcon": "Нет иконки",
"stackMode": "Режим стека",
"network": "Сеть",
"networkDesc": "Изменение настроек, связанных с сетью",
"bypassDomain": "Обход домена",
"bypassDomainDesc": "Действует только при включенном системном прокси",
"resetTip": "Убедитесь, что хотите сбросить",
"regExp": "Регулярное выражение",
"icon": "Иконка",
"iconConfiguration": "Конфигурация иконки",
"noData": "Нет данных",
"adminAutoLaunch": "Автозапуск с правами администратора",
"adminAutoLaunchDesc": "Запуск с правами администратора при загрузке системы",
"fontFamily": "Семейство шрифтов",
"systemFont": "Системный шрифт",
"toggle": "Переключить",
"system": "Система",
"routeMode": "Режим маршрутизации",
"routeMode_bypassPrivate": "Обход частных адресов маршрутизации",
"routeMode_config": "Использовать конфигурацию",
"routeAddress": "Адрес маршрутизации",
"routeAddressDesc": "Настройка адреса прослушивания маршрутизации",
"pleaseInputAdminPassword": "Пожалуйста, введите пароль администратора",
"copyEnvVar": "Копирование переменных окружения",
"memoryInfo": "Информация о памяти",
"cancel": "Отмена",
"fileIsUpdate": "Файл был изменен. Хотите сохранить изменения?",
"profileHasUpdate": "Профиль был изменен. Хотите отключить автообновление?",
"hasCacheChange": "Хотите сохранить изменения в кэше?",
"nullProxies": "Нет прокси",
"copySuccess": "Копирование успешно",
"copyLink": "Копировать ссылку",
"exportFile": "Экспорт файла",
"cacheCorrupt": "Кэш поврежден. Хотите очистить его?",
"detectionTip": "Опирается на сторонний API, только для справки",
"listen": "Слушать"
}

View File

@@ -30,6 +30,8 @@
"other": "其他", "other": "其他",
"about": "关于", "about": "关于",
"en": "英语", "en": "英语",
"ja": "日语",
"ru": "俄语",
"zh_CN": "中文简体", "zh_CN": "中文简体",
"theme": "主题", "theme": "主题",
"themeDesc": "设置深色模式,调整色彩", "themeDesc": "设置深色模式,调整色彩",
@@ -220,7 +222,7 @@
"onlyStatisticsProxy": "仅统计代理", "onlyStatisticsProxy": "仅统计代理",
"onlyStatisticsProxyDesc": "开启后,将只统计代理流量", "onlyStatisticsProxyDesc": "开启后,将只统计代理流量",
"deleteProfileTip": "确定要删除当前配置吗?", "deleteProfileTip": "确定要删除当前配置吗?",
"prueBlackMode": "纯黑模式", "pureBlackMode": "纯黑模式",
"keepAliveIntervalDesc": "TCP保持活动间隔", "keepAliveIntervalDesc": "TCP保持活动间隔",
"entries": "个条目", "entries": "个条目",
"local": "本地", "local": "本地",
@@ -342,5 +344,7 @@
"copySuccess": "复制成功", "copySuccess": "复制成功",
"copyLink": "复制链接", "copyLink": "复制链接",
"exportFile": "导出文件", "exportFile": "导出文件",
"cacheCorrupt": "缓存已损坏,是否清空?" "cacheCorrupt": "缓存已损坏,是否清空?",
"detectionTip": "依赖第三方api仅供参考",
"listen": "监听"
} }

View File

@@ -17,11 +17,15 @@ import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart'; import 'package:intl/src/intl_helpers.dart';
import 'messages_en.dart' as messages_en; import 'messages_en.dart' as messages_en;
import 'messages_ja.dart' as messages_ja;
import 'messages_ru.dart' as messages_ru;
import 'messages_zh_CN.dart' as messages_zh_cn; import 'messages_zh_CN.dart' as messages_zh_cn;
typedef Future<dynamic> LibraryLoader(); typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = { Map<String, LibraryLoader> _deferredLibraries = {
'en': () => new SynchronousFuture(null), 'en': () => new SynchronousFuture(null),
'ja': () => new SynchronousFuture(null),
'ru': () => new SynchronousFuture(null),
'zh_CN': () => new SynchronousFuture(null), 'zh_CN': () => new SynchronousFuture(null),
}; };
@@ -29,6 +33,10 @@ MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) { switch (localeName) {
case 'en': case 'en':
return messages_en.messages; return messages_en.messages;
case 'ja':
return messages_ja.messages;
case 'ru':
return messages_ru.messages;
case 'zh_CN': case 'zh_CN':
return messages_zh_cn.messages; return messages_zh_cn.messages;
default: default:

View File

@@ -89,7 +89,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Auto check for updates when the app starts", "Auto check for updates when the app starts",
), ),
"autoCloseConnections": MessageLookupByLibrary.simpleMessage( "autoCloseConnections": MessageLookupByLibrary.simpleMessage(
"Auto lose connections", "Auto close connections",
), ),
"autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage( "autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
"Auto close connections after change node", "Auto close connections after change node",
@@ -180,6 +180,9 @@ class MessageLookup extends MessageLookupByLibrary {
"desc": MessageLookupByLibrary.simpleMessage( "desc": MessageLookupByLibrary.simpleMessage(
"A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.", "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.",
), ),
"detectionTip": MessageLookupByLibrary.simpleMessage(
"Relying on third-party api is for reference only",
),
"direct": MessageLookupByLibrary.simpleMessage("Direct"), "direct": MessageLookupByLibrary.simpleMessage("Direct"),
"disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"), "disclaimer": MessageLookupByLibrary.simpleMessage("Disclaimer"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage( "disclaimerDesc": MessageLookupByLibrary.simpleMessage(
@@ -293,6 +296,7 @@ class MessageLookup extends MessageLookupByLibrary {
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage( "ipv6InboundDesc": MessageLookupByLibrary.simpleMessage(
"Allow IPv6 inbound", "Allow IPv6 inbound",
), ),
"ja": MessageLookupByLibrary.simpleMessage("Japanese"),
"just": MessageLookupByLibrary.simpleMessage("Just"), "just": MessageLookupByLibrary.simpleMessage("Just"),
"keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage( "keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage(
"Tcp keep alive interval", "Tcp keep alive interval",
@@ -302,6 +306,7 @@ class MessageLookup extends MessageLookupByLibrary {
"layout": MessageLookupByLibrary.simpleMessage("Layout"), "layout": MessageLookupByLibrary.simpleMessage("Layout"),
"light": MessageLookupByLibrary.simpleMessage("Light"), "light": MessageLookupByLibrary.simpleMessage("Light"),
"list": MessageLookupByLibrary.simpleMessage("List"), "list": MessageLookupByLibrary.simpleMessage("List"),
"listen": MessageLookupByLibrary.simpleMessage("Listen"),
"local": MessageLookupByLibrary.simpleMessage("Local"), "local": MessageLookupByLibrary.simpleMessage("Local"),
"localBackupDesc": MessageLookupByLibrary.simpleMessage( "localBackupDesc": MessageLookupByLibrary.simpleMessage(
"Backup local data to local", "Backup local data to local",
@@ -467,7 +472,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Set the Clash listening port", "Set the Clash listening port",
), ),
"proxyProviders": MessageLookupByLibrary.simpleMessage("Proxy providers"), "proxyProviders": MessageLookupByLibrary.simpleMessage("Proxy providers"),
"prueBlackMode": MessageLookupByLibrary.simpleMessage("Prue black mode"), "pureBlackMode": MessageLookupByLibrary.simpleMessage("Pure black mode"),
"qrcode": MessageLookupByLibrary.simpleMessage("QR code"), "qrcode": MessageLookupByLibrary.simpleMessage("QR code"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage( "qrcodeDesc": MessageLookupByLibrary.simpleMessage(
"Scan QR code to obtain profile", "Scan QR code to obtain profile",
@@ -510,6 +515,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Bypass private route address", "Bypass private route address",
), ),
"routeMode_config": MessageLookupByLibrary.simpleMessage("Use config"), "routeMode_config": MessageLookupByLibrary.simpleMessage("Use config"),
"ru": MessageLookupByLibrary.simpleMessage("Russian"),
"rule": MessageLookupByLibrary.simpleMessage("Rule"), "rule": MessageLookupByLibrary.simpleMessage("Rule"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("Rule providers"), "ruleProviders": MessageLookupByLibrary.simpleMessage("Rule providers"),
"save": MessageLookupByLibrary.simpleMessage("Save"), "save": MessageLookupByLibrary.simpleMessage("Save"),

View File

@@ -0,0 +1,472 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a ja locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'ja';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"about": MessageLookupByLibrary.simpleMessage("について"),
"accessControl": MessageLookupByLibrary.simpleMessage("アクセス制御"),
"accessControlAllowDesc": MessageLookupByLibrary.simpleMessage(
"選択したアプリのみVPNを許可",
),
"accessControlDesc": MessageLookupByLibrary.simpleMessage(
"アプリケーションのプロキシアクセスを設定",
),
"accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage(
"選択したアプリをVPNから除外",
),
"account": MessageLookupByLibrary.simpleMessage("アカウント"),
"accountTip": MessageLookupByLibrary.simpleMessage("アカウントは必須です"),
"action": MessageLookupByLibrary.simpleMessage("アクション"),
"action_mode": MessageLookupByLibrary.simpleMessage("モード切替"),
"action_proxy": MessageLookupByLibrary.simpleMessage("システムプロキシ"),
"action_start": MessageLookupByLibrary.simpleMessage("開始/停止"),
"action_tun": MessageLookupByLibrary.simpleMessage("TUN"),
"action_view": MessageLookupByLibrary.simpleMessage("表示/非表示"),
"add": MessageLookupByLibrary.simpleMessage("追加"),
"address": MessageLookupByLibrary.simpleMessage("アドレス"),
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAVサーバーアドレス"),
"addressTip": MessageLookupByLibrary.simpleMessage("有効なWebDAVアドレスを入力"),
"adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理者自動起動"),
"adminAutoLaunchDesc": MessageLookupByLibrary.simpleMessage("管理者モードで起動"),
"ago": MessageLookupByLibrary.simpleMessage(""),
"agree": MessageLookupByLibrary.simpleMessage("同意"),
"allApps": MessageLookupByLibrary.simpleMessage("全アプリ"),
"allowBypass": MessageLookupByLibrary.simpleMessage("アプリがVPNをバイパスすることを許可"),
"allowBypassDesc": MessageLookupByLibrary.simpleMessage(
"有効化すると一部アプリがVPNをバイパス",
),
"allowLan": MessageLookupByLibrary.simpleMessage("LANを許可"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage("LAN経由でのプロキシアクセスを許可"),
"app": MessageLookupByLibrary.simpleMessage("アプリ"),
"appAccessControl": MessageLookupByLibrary.simpleMessage("アプリアクセス制御"),
"appDesc": MessageLookupByLibrary.simpleMessage("アプリ関連設定の処理"),
"application": MessageLookupByLibrary.simpleMessage("アプリケーション"),
"applicationDesc": MessageLookupByLibrary.simpleMessage("アプリ関連設定を変更"),
"auto": MessageLookupByLibrary.simpleMessage("自動"),
"autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自動更新チェック"),
"autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage(
"起動時に更新を自動チェック",
),
"autoCloseConnections": MessageLookupByLibrary.simpleMessage("接続を自動閉じる"),
"autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
"ノード変更後に接続を自動閉じる",
),
"autoLaunch": MessageLookupByLibrary.simpleMessage("自動起動"),
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage("システムの自動起動に従う"),
"autoRun": MessageLookupByLibrary.simpleMessage("自動実行"),
"autoRunDesc": MessageLookupByLibrary.simpleMessage("アプリ起動時に自動実行"),
"autoUpdate": MessageLookupByLibrary.simpleMessage("自動更新"),
"autoUpdateInterval": MessageLookupByLibrary.simpleMessage("自動更新間隔(分)"),
"backup": MessageLookupByLibrary.simpleMessage("バックアップ"),
"backupAndRecovery": MessageLookupByLibrary.simpleMessage("バックアップと復元"),
"backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage(
"WebDAVまたはファイルでデータを同期",
),
"backupSuccess": MessageLookupByLibrary.simpleMessage("バックアップ成功"),
"bind": MessageLookupByLibrary.simpleMessage("バインド"),
"blacklistMode": MessageLookupByLibrary.simpleMessage("ブラックリストモード"),
"bypassDomain": MessageLookupByLibrary.simpleMessage("バイパスドメイン"),
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage("システムプロキシ有効時のみ適用"),
"cacheCorrupt": MessageLookupByLibrary.simpleMessage(
"キャッシュが破損しています。クリアしますか?",
),
"cancel": MessageLookupByLibrary.simpleMessage("キャンセル"),
"cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage(
"システムアプリの除外を解除",
),
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("全選択解除"),
"checkError": MessageLookupByLibrary.simpleMessage("確認エラー"),
"checkUpdate": MessageLookupByLibrary.simpleMessage("更新を確認"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage("アプリは最新版です"),
"checking": MessageLookupByLibrary.simpleMessage("確認中..."),
"clipboardExport": MessageLookupByLibrary.simpleMessage("クリップボードにエクスポート"),
"clipboardImport": MessageLookupByLibrary.simpleMessage("クリップボードからインポート"),
"columns": MessageLookupByLibrary.simpleMessage(""),
"compatible": MessageLookupByLibrary.simpleMessage("互換モード"),
"compatibleDesc": MessageLookupByLibrary.simpleMessage(
"有効化すると一部機能を失いますが、Clashの完全サポートを獲得",
),
"confirm": MessageLookupByLibrary.simpleMessage("確認"),
"connections": MessageLookupByLibrary.simpleMessage("接続"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("現在の接続データを表示"),
"connectivity": MessageLookupByLibrary.simpleMessage("接続性:"),
"copy": MessageLookupByLibrary.simpleMessage("コピー"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage("環境変数をコピー"),
"copyLink": MessageLookupByLibrary.simpleMessage("リンクをコピー"),
"copySuccess": MessageLookupByLibrary.simpleMessage("コピー成功"),
"core": MessageLookupByLibrary.simpleMessage("コア"),
"coreInfo": MessageLookupByLibrary.simpleMessage("コア情報"),
"country": MessageLookupByLibrary.simpleMessage(""),
"create": MessageLookupByLibrary.simpleMessage("作成"),
"cut": MessageLookupByLibrary.simpleMessage("切り取り"),
"dark": MessageLookupByLibrary.simpleMessage("ダーク"),
"dashboard": MessageLookupByLibrary.simpleMessage("ダッシュボード"),
"days": MessageLookupByLibrary.simpleMessage(""),
"defaultNameserver": MessageLookupByLibrary.simpleMessage("デフォルトネームサーバー"),
"defaultNameserverDesc": MessageLookupByLibrary.simpleMessage(
"DNSサーバーの解決用",
),
"defaultSort": MessageLookupByLibrary.simpleMessage("デフォルト順"),
"defaultText": MessageLookupByLibrary.simpleMessage("デフォルト"),
"delay": MessageLookupByLibrary.simpleMessage("遅延"),
"delaySort": MessageLookupByLibrary.simpleMessage("遅延順"),
"delete": MessageLookupByLibrary.simpleMessage("削除"),
"deleteProfileTip": MessageLookupByLibrary.simpleMessage(
"現在のプロファイルを削除しますか?",
),
"desc": MessageLookupByLibrary.simpleMessage(
"ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。",
),
"detectionTip": MessageLookupByLibrary.simpleMessage("サードパーティAPIに依存参考値"),
"direct": MessageLookupByLibrary.simpleMessage("ダイレクト"),
"disclaimer": MessageLookupByLibrary.simpleMessage("免責事項"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
"本ソフトウェアは学習交流や科学研究などの非営利目的でのみ使用されます。商用利用は厳禁です。いかなる商用活動も本ソフトウェアとは無関係です。",
),
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("新バージョンを発見"),
"discovery": MessageLookupByLibrary.simpleMessage("新しいバージョンを発見"),
"dnsDesc": MessageLookupByLibrary.simpleMessage("DNS関連設定の更新"),
"dnsMode": MessageLookupByLibrary.simpleMessage("DNSモード"),
"doYouWantToPass": MessageLookupByLibrary.simpleMessage("通過させますか?"),
"domain": MessageLookupByLibrary.simpleMessage("ドメイン"),
"download": MessageLookupByLibrary.simpleMessage("ダウンロード"),
"edit": MessageLookupByLibrary.simpleMessage("編集"),
"en": MessageLookupByLibrary.simpleMessage("英語"),
"entries": MessageLookupByLibrary.simpleMessage(" エントリ"),
"exclude": MessageLookupByLibrary.simpleMessage("最近のタスクから非表示"),
"excludeDesc": MessageLookupByLibrary.simpleMessage(
"アプリがバックグラウンド時に最近のタスクから非表示",
),
"exit": MessageLookupByLibrary.simpleMessage("終了"),
"expand": MessageLookupByLibrary.simpleMessage("標準"),
"expirationTime": MessageLookupByLibrary.simpleMessage("有効期限"),
"exportFile": MessageLookupByLibrary.simpleMessage("ファイルをエクスポート"),
"exportLogs": MessageLookupByLibrary.simpleMessage("ログをエクスポート"),
"exportSuccess": MessageLookupByLibrary.simpleMessage("エクスポート成功"),
"externalController": MessageLookupByLibrary.simpleMessage("外部コントローラー"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"有効化するとClashコアをポート9090で制御可能",
),
"externalLink": MessageLookupByLibrary.simpleMessage("外部リンク"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部リソース"),
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeipフィルター"),
"fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip範囲"),
"fallback": MessageLookupByLibrary.simpleMessage("フォールバック"),
"fallbackDesc": MessageLookupByLibrary.simpleMessage("通常はオフショアDNSを使用"),
"fallbackFilter": MessageLookupByLibrary.simpleMessage("フォールバックフィルター"),
"file": MessageLookupByLibrary.simpleMessage("ファイル"),
"fileDesc": MessageLookupByLibrary.simpleMessage("プロファイルを直接アップロード"),
"fileIsUpdate": MessageLookupByLibrary.simpleMessage(
"ファイルが変更されました。保存しますか?",
),
"filterSystemApp": MessageLookupByLibrary.simpleMessage("システムアプリを除外"),
"findProcessMode": MessageLookupByLibrary.simpleMessage("プロセス検出"),
"findProcessModeDesc": MessageLookupByLibrary.simpleMessage(
"有効化するとフラッシュバックのリスクあり",
),
"fontFamily": MessageLookupByLibrary.simpleMessage("フォントファミリー"),
"fourColumns": MessageLookupByLibrary.simpleMessage("4列"),
"general": MessageLookupByLibrary.simpleMessage("一般"),
"generalDesc": MessageLookupByLibrary.simpleMessage("一般設定の上書き"),
"geoData": MessageLookupByLibrary.simpleMessage("地域データ"),
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低メモリモード"),
"geodataLoaderDesc": MessageLookupByLibrary.simpleMessage(
"有効化するとGeo低メモリローダーを使用",
),
"geoipCode": MessageLookupByLibrary.simpleMessage("GeoIPコード"),
"global": MessageLookupByLibrary.simpleMessage("グローバル"),
"go": MessageLookupByLibrary.simpleMessage("移動"),
"goDownload": MessageLookupByLibrary.simpleMessage("ダウンロードへ"),
"hasCacheChange": MessageLookupByLibrary.simpleMessage("変更をキャッシュしますか?"),
"hostsDesc": MessageLookupByLibrary.simpleMessage("ホストを追加"),
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("ホットキー競合"),
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("ホットキー管理"),
"hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage(
"キーボードでアプリを制御",
),
"hours": MessageLookupByLibrary.simpleMessage("時間"),
"icon": MessageLookupByLibrary.simpleMessage("アイコン"),
"iconConfiguration": MessageLookupByLibrary.simpleMessage("アイコン設定"),
"iconStyle": MessageLookupByLibrary.simpleMessage("アイコンスタイル"),
"importFromURL": MessageLookupByLibrary.simpleMessage("URLからインポート"),
"infiniteTime": MessageLookupByLibrary.simpleMessage("長期有効"),
"init": MessageLookupByLibrary.simpleMessage("初期化"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("正しいホットキーを入力"),
"intelligentSelected": MessageLookupByLibrary.simpleMessage("インテリジェント選択"),
"intranetIP": MessageLookupByLibrary.simpleMessage("イントラネットIP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IPCIDR"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("有効化するとIPv6トラフィックを受信可能"),
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("IPv6インバウンドを許可"),
"ja": MessageLookupByLibrary.simpleMessage("日本語"),
"just": MessageLookupByLibrary.simpleMessage("たった今"),
"keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage(
"TCPキープアライブ間隔",
),
"key": MessageLookupByLibrary.simpleMessage("キー"),
"language": MessageLookupByLibrary.simpleMessage("言語"),
"layout": MessageLookupByLibrary.simpleMessage("レイアウト"),
"light": MessageLookupByLibrary.simpleMessage("ライト"),
"list": MessageLookupByLibrary.simpleMessage("リスト"),
"listen": MessageLookupByLibrary.simpleMessage("リスン"),
"local": MessageLookupByLibrary.simpleMessage("ローカル"),
"localBackupDesc": MessageLookupByLibrary.simpleMessage("ローカルにデータをバックアップ"),
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage("ファイルからデータを復元"),
"logLevel": MessageLookupByLibrary.simpleMessage("ログレベル"),
"logcat": MessageLookupByLibrary.simpleMessage("ログキャット"),
"logcatDesc": MessageLookupByLibrary.simpleMessage("無効化するとログエントリを非表示"),
"logs": MessageLookupByLibrary.simpleMessage("ログ"),
"logsDesc": MessageLookupByLibrary.simpleMessage("ログキャプチャ記録"),
"loopback": MessageLookupByLibrary.simpleMessage("ループバック解除ツール"),
"loopbackDesc": MessageLookupByLibrary.simpleMessage("UWPループバック解除用"),
"loose": MessageLookupByLibrary.simpleMessage(""),
"memoryInfo": MessageLookupByLibrary.simpleMessage("メモリ情報"),
"min": MessageLookupByLibrary.simpleMessage("最小化"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("終了時に最小化"),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage(
"システムの終了イベントを変更",
),
"minutes": MessageLookupByLibrary.simpleMessage(""),
"mode": MessageLookupByLibrary.simpleMessage("モード"),
"months": MessageLookupByLibrary.simpleMessage(""),
"more": MessageLookupByLibrary.simpleMessage("詳細"),
"name": MessageLookupByLibrary.simpleMessage("名前"),
"nameSort": MessageLookupByLibrary.simpleMessage("名前順"),
"nameserver": MessageLookupByLibrary.simpleMessage("ネームサーバー"),
"nameserverDesc": MessageLookupByLibrary.simpleMessage("ドメイン解決用"),
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("ネームサーバーポリシー"),
"nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage(
"対応するネームサーバーポリシーを指定",
),
"network": MessageLookupByLibrary.simpleMessage("ネットワーク"),
"networkDesc": MessageLookupByLibrary.simpleMessage("ネットワーク関連設定の変更"),
"networkDetection": MessageLookupByLibrary.simpleMessage("ネットワーク検出"),
"networkSpeed": MessageLookupByLibrary.simpleMessage("ネットワーク速度"),
"noData": MessageLookupByLibrary.simpleMessage("データなし"),
"noHotKey": MessageLookupByLibrary.simpleMessage("ホットキーなし"),
"noIcon": MessageLookupByLibrary.simpleMessage("なし"),
"noInfo": MessageLookupByLibrary.simpleMessage("情報なし"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("追加情報なし"),
"noNetwork": MessageLookupByLibrary.simpleMessage("ネットワークなし"),
"noProxy": MessageLookupByLibrary.simpleMessage("プロキシなし"),
"noProxyDesc": MessageLookupByLibrary.simpleMessage(
"プロファイルを作成するか、有効なプロファイルを追加してください",
),
"notEmpty": MessageLookupByLibrary.simpleMessage("空欄不可"),
"notSelectedTip": MessageLookupByLibrary.simpleMessage(
"現在のプロキシグループは選択できません",
),
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("接続なし"),
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("コア情報を取得できません"),
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("ログがありません"),
"nullProfileDesc": MessageLookupByLibrary.simpleMessage(
"プロファイルがありません。追加してください",
),
"nullProxies": MessageLookupByLibrary.simpleMessage("プロキシなし"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("リクエストなし"),
"oneColumn": MessageLookupByLibrary.simpleMessage("1列"),
"onlyIcon": MessageLookupByLibrary.simpleMessage("アイコンのみ"),
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("サードパーティアプリのみ"),
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("プロキシのみ統計"),
"onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage(
"有効化するとプロキシトラフィックのみ統計",
),
"options": MessageLookupByLibrary.simpleMessage("オプション"),
"other": MessageLookupByLibrary.simpleMessage("その他"),
"otherContributors": MessageLookupByLibrary.simpleMessage("その他の貢献者"),
"outboundMode": MessageLookupByLibrary.simpleMessage("アウトバウンドモード"),
"override": MessageLookupByLibrary.simpleMessage("上書き"),
"overrideDesc": MessageLookupByLibrary.simpleMessage("プロキシ関連設定を上書き"),
"overrideDns": MessageLookupByLibrary.simpleMessage("DNS上書き"),
"overrideDnsDesc": MessageLookupByLibrary.simpleMessage(
"有効化するとプロファイルのDNS設定を上書き",
),
"password": MessageLookupByLibrary.simpleMessage("パスワード"),
"passwordTip": MessageLookupByLibrary.simpleMessage("パスワードは必須です"),
"paste": MessageLookupByLibrary.simpleMessage("貼り付け"),
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage(
"WebDAVをバインドしてください",
),
"pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage(
"管理者パスワードを入力",
),
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage(
"ファイルをアップロードしてください",
),
"pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(
"有効なQRコードをアップロードしてください",
),
"port": MessageLookupByLibrary.simpleMessage("ポート"),
"preferH3Desc": MessageLookupByLibrary.simpleMessage("DOHのHTTP/3を優先使用"),
"pressKeyboard": MessageLookupByLibrary.simpleMessage("キーボードを押してください"),
"preview": MessageLookupByLibrary.simpleMessage("プレビュー"),
"profile": MessageLookupByLibrary.simpleMessage("プロファイル"),
"profileAutoUpdateIntervalInvalidValidationDesc":
MessageLookupByLibrary.simpleMessage("有効な間隔形式を入力してください"),
"profileAutoUpdateIntervalNullValidationDesc":
MessageLookupByLibrary.simpleMessage("自動更新間隔を入力してください"),
"profileHasUpdate": MessageLookupByLibrary.simpleMessage(
"プロファイルが変更されました。自動更新を無効化しますか?",
),
"profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"プロファイル名を入力してください",
),
"profileParseErrorDesc": MessageLookupByLibrary.simpleMessage(
"プロファイル解析エラー",
),
"profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage(
"有効なプロファイルURLを入力してください",
),
"profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"プロファイルURLを入力してください",
),
"profiles": MessageLookupByLibrary.simpleMessage("プロファイル一覧"),
"profilesSort": MessageLookupByLibrary.simpleMessage("プロファイルの並び替え"),
"project": MessageLookupByLibrary.simpleMessage("プロジェクト"),
"providers": MessageLookupByLibrary.simpleMessage("プロバイダー"),
"proxies": MessageLookupByLibrary.simpleMessage("プロキシ"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("プロキシ設定"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("プロキシグループ"),
"proxyNameserver": MessageLookupByLibrary.simpleMessage("プロキシネームサーバー"),
"proxyNameserverDesc": MessageLookupByLibrary.simpleMessage(
"プロキシノード解決用ドメイン",
),
"proxyPort": MessageLookupByLibrary.simpleMessage("プロキシポート"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("Clashのリスニングポートを設定"),
"proxyProviders": MessageLookupByLibrary.simpleMessage("プロキシプロバイダー"),
"pureBlackMode": MessageLookupByLibrary.simpleMessage("純黒モード"),
"qrcode": MessageLookupByLibrary.simpleMessage("QRコード"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("QRコードをスキャンしてプロファイルを取得"),
"recovery": MessageLookupByLibrary.simpleMessage("復元"),
"recoveryAll": MessageLookupByLibrary.simpleMessage("全データ復元"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("プロファイルのみ復元"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("復元成功"),
"regExp": MessageLookupByLibrary.simpleMessage("正規表現"),
"remote": MessageLookupByLibrary.simpleMessage("リモート"),
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage(
"WebDAVにデータをバックアップ",
),
"remoteRecoveryDesc": MessageLookupByLibrary.simpleMessage(
"WebDAVからデータを復元",
),
"remove": MessageLookupByLibrary.simpleMessage("削除"),
"requests": MessageLookupByLibrary.simpleMessage("リクエスト"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("最近のリクエスト記録を表示"),
"reset": MessageLookupByLibrary.simpleMessage("リセット"),
"resetTip": MessageLookupByLibrary.simpleMessage("リセットを確定"),
"resources": MessageLookupByLibrary.simpleMessage("リソース"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部リソース関連情報"),
"respectRules": MessageLookupByLibrary.simpleMessage("ルール尊重"),
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
"DNS接続がルールに従うproxy-server-nameserverの設定が必要",
),
"routeAddress": MessageLookupByLibrary.simpleMessage("ルートアドレス"),
"routeAddressDesc": MessageLookupByLibrary.simpleMessage("ルートアドレスを設定"),
"routeMode": MessageLookupByLibrary.simpleMessage("ルートモード"),
"routeMode_bypassPrivate": MessageLookupByLibrary.simpleMessage(
"プライベートルートをバイパス",
),
"routeMode_config": MessageLookupByLibrary.simpleMessage("設定を使用"),
"ru": MessageLookupByLibrary.simpleMessage("ロシア語"),
"rule": MessageLookupByLibrary.simpleMessage("ルール"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("ルールプロバイダー"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
"search": MessageLookupByLibrary.simpleMessage("検索"),
"seconds": MessageLookupByLibrary.simpleMessage(""),
"selectAll": MessageLookupByLibrary.simpleMessage("すべて選択"),
"selected": MessageLookupByLibrary.simpleMessage("選択済み"),
"settings": MessageLookupByLibrary.simpleMessage("設定"),
"show": MessageLookupByLibrary.simpleMessage("表示"),
"shrink": MessageLookupByLibrary.simpleMessage("縮小"),
"silentLaunch": MessageLookupByLibrary.simpleMessage("バックグラウンド起動"),
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("バックグラウンドで起動"),
"size": MessageLookupByLibrary.simpleMessage("サイズ"),
"sort": MessageLookupByLibrary.simpleMessage("並び替え"),
"source": MessageLookupByLibrary.simpleMessage("ソース"),
"stackMode": MessageLookupByLibrary.simpleMessage("スタックモード"),
"standard": MessageLookupByLibrary.simpleMessage("標準"),
"start": MessageLookupByLibrary.simpleMessage("開始"),
"startVpn": MessageLookupByLibrary.simpleMessage("VPNを開始中..."),
"status": MessageLookupByLibrary.simpleMessage("ステータス"),
"statusDesc": MessageLookupByLibrary.simpleMessage("無効時はシステムDNSを使用"),
"stop": MessageLookupByLibrary.simpleMessage("停止"),
"stopVpn": MessageLookupByLibrary.simpleMessage("VPNを停止中..."),
"style": MessageLookupByLibrary.simpleMessage("スタイル"),
"submit": MessageLookupByLibrary.simpleMessage("送信"),
"sync": MessageLookupByLibrary.simpleMessage("同期"),
"system": MessageLookupByLibrary.simpleMessage("システム"),
"systemFont": MessageLookupByLibrary.simpleMessage("システムフォント"),
"systemProxy": MessageLookupByLibrary.simpleMessage("システムプロキシ"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"HTTPプロキシをVpnServiceに接続",
),
"tab": MessageLookupByLibrary.simpleMessage("タブ"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("タブアニメーション"),
"tabAnimationDesc": MessageLookupByLibrary.simpleMessage(
"有効化するとホームタブに切り替えアニメーションを追加",
),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP並列処理"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("TCP並列処理を許可"),
"testUrl": MessageLookupByLibrary.simpleMessage("URLテスト"),
"theme": MessageLookupByLibrary.simpleMessage("テーマ"),
"themeColor": MessageLookupByLibrary.simpleMessage("テーマカラー"),
"themeDesc": MessageLookupByLibrary.simpleMessage("ダークモードの設定、色の調整"),
"themeMode": MessageLookupByLibrary.simpleMessage("テーマモード"),
"threeColumns": MessageLookupByLibrary.simpleMessage("3列"),
"tight": MessageLookupByLibrary.simpleMessage(""),
"time": MessageLookupByLibrary.simpleMessage("時間"),
"tip": MessageLookupByLibrary.simpleMessage("ヒント"),
"toggle": MessageLookupByLibrary.simpleMessage("トグル"),
"tools": MessageLookupByLibrary.simpleMessage("ツール"),
"trafficUsage": MessageLookupByLibrary.simpleMessage("トラフィック使用量"),
"tun": MessageLookupByLibrary.simpleMessage("TUN"),
"tunDesc": MessageLookupByLibrary.simpleMessage("管理者モードでのみ有効"),
"twoColumns": MessageLookupByLibrary.simpleMessage("2列"),
"unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage(
"現在のプロファイルを更新できません",
),
"unifiedDelay": MessageLookupByLibrary.simpleMessage("統一遅延"),
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage(
"ハンドシェイクなどの余分な遅延を削除",
),
"unknown": MessageLookupByLibrary.simpleMessage("不明"),
"update": MessageLookupByLibrary.simpleMessage("更新"),
"upload": MessageLookupByLibrary.simpleMessage("アップロード"),
"url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": MessageLookupByLibrary.simpleMessage("URL経由でプロファイルを取得"),
"useHosts": MessageLookupByLibrary.simpleMessage("ホストを使用"),
"useSystemHosts": MessageLookupByLibrary.simpleMessage("システムホストを使用"),
"value": MessageLookupByLibrary.simpleMessage(""),
"view": MessageLookupByLibrary.simpleMessage("表示"),
"vpnDesc": MessageLookupByLibrary.simpleMessage("VPN関連設定の変更"),
"vpnEnableDesc": MessageLookupByLibrary.simpleMessage(
"VpnService経由で全システムトラフィックをルーティング",
),
"vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage(
"HTTPプロキシをVpnServiceに接続",
),
"vpnTip": MessageLookupByLibrary.simpleMessage("変更はVPN再起動後に有効"),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV設定"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("ホワイトリストモード"),
"years": MessageLookupByLibrary.simpleMessage(""),
"zh_CN": MessageLookupByLibrary.simpleMessage("簡体字中国語"),
};
}

View File

@@ -0,0 +1,666 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a ru locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'ru';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"about": MessageLookupByLibrary.simpleMessage("О программе"),
"accessControl": MessageLookupByLibrary.simpleMessage("Контроль доступа"),
"accessControlAllowDesc": MessageLookupByLibrary.simpleMessage(
"Разрешить только выбранным приложениям доступ к VPN",
),
"accessControlDesc": MessageLookupByLibrary.simpleMessage(
"Настройка доступа приложений к прокси",
),
"accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage(
"Выбранные приложения будут исключены из VPN",
),
"account": MessageLookupByLibrary.simpleMessage("Аккаунт"),
"accountTip": MessageLookupByLibrary.simpleMessage(
"Аккаунт не может быть пустым",
),
"action": MessageLookupByLibrary.simpleMessage("Действие"),
"action_mode": MessageLookupByLibrary.simpleMessage("Переключить режим"),
"action_proxy": MessageLookupByLibrary.simpleMessage("Системный прокси"),
"action_start": MessageLookupByLibrary.simpleMessage("Старт/Стоп"),
"action_tun": MessageLookupByLibrary.simpleMessage("TUN"),
"action_view": MessageLookupByLibrary.simpleMessage("Показать/Скрыть"),
"add": MessageLookupByLibrary.simpleMessage("Добавить"),
"address": MessageLookupByLibrary.simpleMessage("Адрес"),
"addressHelp": MessageLookupByLibrary.simpleMessage("Адрес сервера WebDAV"),
"addressTip": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, введите действительный адрес WebDAV",
),
"adminAutoLaunch": MessageLookupByLibrary.simpleMessage(
"Автозапуск с правами администратора",
),
"adminAutoLaunchDesc": MessageLookupByLibrary.simpleMessage(
"Запуск с правами администратора при загрузке системы",
),
"ago": MessageLookupByLibrary.simpleMessage(" назад"),
"agree": MessageLookupByLibrary.simpleMessage("Согласен"),
"allApps": MessageLookupByLibrary.simpleMessage("Все приложения"),
"allowBypass": MessageLookupByLibrary.simpleMessage(
"Разрешить приложениям обходить VPN",
),
"allowBypassDesc": MessageLookupByLibrary.simpleMessage(
"Некоторые приложения могут обходить VPN при включении",
),
"allowLan": MessageLookupByLibrary.simpleMessage("Разрешить LAN"),
"allowLanDesc": MessageLookupByLibrary.simpleMessage(
"Разрешить доступ к прокси через локальную сеть",
),
"app": MessageLookupByLibrary.simpleMessage("Приложение"),
"appAccessControl": MessageLookupByLibrary.simpleMessage(
"Контроль доступа приложений",
),
"appDesc": MessageLookupByLibrary.simpleMessage(
"Обработка настроек, связанных с приложением",
),
"application": MessageLookupByLibrary.simpleMessage("Приложение"),
"applicationDesc": MessageLookupByLibrary.simpleMessage(
"Изменение настроек, связанных с приложением",
),
"auto": MessageLookupByLibrary.simpleMessage("Авто"),
"autoCheckUpdate": MessageLookupByLibrary.simpleMessage(
"Автопроверка обновлений",
),
"autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage(
"Автоматически проверять обновления при запуске приложения",
),
"autoCloseConnections": MessageLookupByLibrary.simpleMessage(
"Автоматическое закрытие соединений",
),
"autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
"Автоматически закрывать соединения после смены узла",
),
"autoLaunch": MessageLookupByLibrary.simpleMessage("Автозапуск"),
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage(
"Следовать автозапуску системы",
),
"autoRun": MessageLookupByLibrary.simpleMessage("Автозапуск"),
"autoRunDesc": MessageLookupByLibrary.simpleMessage(
"Автоматический запуск при открытии приложения",
),
"autoUpdate": MessageLookupByLibrary.simpleMessage("Автообновление"),
"autoUpdateInterval": MessageLookupByLibrary.simpleMessage(
"Интервал автообновления (минуты)",
),
"backup": MessageLookupByLibrary.simpleMessage("Резервное копирование"),
"backupAndRecovery": MessageLookupByLibrary.simpleMessage(
"Резервное копирование и восстановление",
),
"backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage(
"Синхронизация данных через WebDAV или файл",
),
"backupSuccess": MessageLookupByLibrary.simpleMessage(
"Резервное копирование успешно",
),
"bind": MessageLookupByLibrary.simpleMessage("Привязать"),
"blacklistMode": MessageLookupByLibrary.simpleMessage(
"Режим черного списка",
),
"bypassDomain": MessageLookupByLibrary.simpleMessage("Обход домена"),
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage(
"Действует только при включенном системном прокси",
),
"cacheCorrupt": MessageLookupByLibrary.simpleMessage(
"Кэш поврежден. Хотите очистить его?",
),
"cancel": MessageLookupByLibrary.simpleMessage("Отмена"),
"cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage(
"Отменить фильтрацию системных приложений",
),
"cancelSelectAll": MessageLookupByLibrary.simpleMessage(
"Отменить выбор всего",
),
"checkError": MessageLookupByLibrary.simpleMessage("Ошибка проверки"),
"checkUpdate": MessageLookupByLibrary.simpleMessage("Проверить обновления"),
"checkUpdateError": MessageLookupByLibrary.simpleMessage(
"Текущее приложение уже является последней версией",
),
"checking": MessageLookupByLibrary.simpleMessage("Проверка..."),
"clipboardExport": MessageLookupByLibrary.simpleMessage(
"Экспорт в буфер обмена",
),
"clipboardImport": MessageLookupByLibrary.simpleMessage(
"Импорт из буфера обмена",
),
"columns": MessageLookupByLibrary.simpleMessage("Столбцы"),
"compatible": MessageLookupByLibrary.simpleMessage("Режим совместимости"),
"compatibleDesc": MessageLookupByLibrary.simpleMessage(
"Включение приведет к потере части функциональности приложения, но обеспечит полную поддержку Clash.",
),
"confirm": MessageLookupByLibrary.simpleMessage("Подтвердить"),
"connections": MessageLookupByLibrary.simpleMessage("Соединения"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage(
"Просмотр текущих данных о соединениях",
),
"connectivity": MessageLookupByLibrary.simpleMessage("Связь:"),
"copy": MessageLookupByLibrary.simpleMessage("Копировать"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage(
"Копирование переменных окружения",
),
"copyLink": MessageLookupByLibrary.simpleMessage("Копировать ссылку"),
"copySuccess": MessageLookupByLibrary.simpleMessage("Копирование успешно"),
"core": MessageLookupByLibrary.simpleMessage("Ядро"),
"coreInfo": MessageLookupByLibrary.simpleMessage("Информация о ядре"),
"country": MessageLookupByLibrary.simpleMessage("Страна"),
"create": MessageLookupByLibrary.simpleMessage("Создать"),
"cut": MessageLookupByLibrary.simpleMessage("Вырезать"),
"dark": MessageLookupByLibrary.simpleMessage("Темный"),
"dashboard": MessageLookupByLibrary.simpleMessage("Панель управления"),
"days": MessageLookupByLibrary.simpleMessage("Дней"),
"defaultNameserver": MessageLookupByLibrary.simpleMessage(
"Сервер имен по умолчанию",
),
"defaultNameserverDesc": MessageLookupByLibrary.simpleMessage(
"Для разрешения DNS-сервера",
),
"defaultSort": MessageLookupByLibrary.simpleMessage(
"Сортировка по умолчанию",
),
"defaultText": MessageLookupByLibrary.simpleMessage("По умолчанию"),
"delay": MessageLookupByLibrary.simpleMessage("Задержка"),
"delaySort": MessageLookupByLibrary.simpleMessage("Сортировка по задержке"),
"delete": MessageLookupByLibrary.simpleMessage("Удалить"),
"deleteProfileTip": MessageLookupByLibrary.simpleMessage(
"Вы уверены, что хотите удалить текущий профиль?",
),
"desc": MessageLookupByLibrary.simpleMessage(
"Многоплатформенный прокси-клиент на основе ClashMeta, простой и удобный в использовании, с открытым исходным кодом и без рекламы.",
),
"detectionTip": MessageLookupByLibrary.simpleMessage(
"Опирается на сторонний API, только для справки",
),
"direct": MessageLookupByLibrary.simpleMessage("Прямой"),
"disclaimer": MessageLookupByLibrary.simpleMessage(
"Отказ от ответственности",
),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
"Это программное обеспечение используется только в некоммерческих целях, таких как учебные обмены и научные исследования. Запрещено использовать это программное обеспечение в коммерческих целях. Любая коммерческая деятельность, если таковая имеется, не имеет отношения к этому программному обеспечению.",
),
"discoverNewVersion": MessageLookupByLibrary.simpleMessage(
"Обнаружена новая версия",
),
"discovery": MessageLookupByLibrary.simpleMessage(
"Обнаружена новая версия",
),
"dnsDesc": MessageLookupByLibrary.simpleMessage(
"Обновление настроек, связанных с DNS",
),
"dnsMode": MessageLookupByLibrary.simpleMessage("Режим DNS"),
"doYouWantToPass": MessageLookupByLibrary.simpleMessage(
"Вы хотите пропустить",
),
"domain": MessageLookupByLibrary.simpleMessage("Домен"),
"download": MessageLookupByLibrary.simpleMessage("Скачивание"),
"edit": MessageLookupByLibrary.simpleMessage("Редактировать"),
"en": MessageLookupByLibrary.simpleMessage("Английский"),
"entries": MessageLookupByLibrary.simpleMessage(" записей"),
"exclude": MessageLookupByLibrary.simpleMessage(
"Скрыть из последних задач",
),
"excludeDesc": MessageLookupByLibrary.simpleMessage(
"Когда приложение находится в фоновом режиме, оно скрыто из последних задач",
),
"exit": MessageLookupByLibrary.simpleMessage("Выход"),
"expand": MessageLookupByLibrary.simpleMessage("Стандартный"),
"expirationTime": MessageLookupByLibrary.simpleMessage("Время истечения"),
"exportFile": MessageLookupByLibrary.simpleMessage("Экспорт файла"),
"exportLogs": MessageLookupByLibrary.simpleMessage("Экспорт логов"),
"exportSuccess": MessageLookupByLibrary.simpleMessage("Экспорт успешен"),
"externalController": MessageLookupByLibrary.simpleMessage(
"Внешний контроллер",
),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"При включении ядро Clash можно контролировать на порту 9090",
),
"externalLink": MessageLookupByLibrary.simpleMessage("Внешняя ссылка"),
"externalResources": MessageLookupByLibrary.simpleMessage(
"Внешние ресурсы",
),
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Фильтр Fakeip"),
"fakeipRange": MessageLookupByLibrary.simpleMessage("Диапазон Fakeip"),
"fallback": MessageLookupByLibrary.simpleMessage("Резервный"),
"fallbackDesc": MessageLookupByLibrary.simpleMessage(
"Обычно используется оффшорный DNS",
),
"fallbackFilter": MessageLookupByLibrary.simpleMessage(
"Фильтр резервного DNS",
),
"file": MessageLookupByLibrary.simpleMessage("Файл"),
"fileDesc": MessageLookupByLibrary.simpleMessage("Прямая загрузка профиля"),
"fileIsUpdate": MessageLookupByLibrary.simpleMessage(
"Файл был изменен. Хотите сохранить изменения?",
),
"filterSystemApp": MessageLookupByLibrary.simpleMessage(
"Фильтровать системные приложения",
),
"findProcessMode": MessageLookupByLibrary.simpleMessage(
"Режим поиска процесса",
),
"findProcessModeDesc": MessageLookupByLibrary.simpleMessage(
"Есть риск сбоя после включения",
),
"fontFamily": MessageLookupByLibrary.simpleMessage("Семейство шрифтов"),
"fourColumns": MessageLookupByLibrary.simpleMessage("Четыре столбца"),
"general": MessageLookupByLibrary.simpleMessage("Общие"),
"generalDesc": MessageLookupByLibrary.simpleMessage(
"Переопределение общих настроек",
),
"geoData": MessageLookupByLibrary.simpleMessage("Геоданные"),
"geodataLoader": MessageLookupByLibrary.simpleMessage(
"Режим низкого потребления памяти для геоданных",
),
"geodataLoaderDesc": MessageLookupByLibrary.simpleMessage(
"Включение будет использовать загрузчик геоданных с низким потреблением памяти",
),
"geoipCode": MessageLookupByLibrary.simpleMessage("Код Geoip"),
"global": MessageLookupByLibrary.simpleMessage("Глобальный"),
"go": MessageLookupByLibrary.simpleMessage("Перейти"),
"goDownload": MessageLookupByLibrary.simpleMessage("Перейти к загрузке"),
"hasCacheChange": MessageLookupByLibrary.simpleMessage(
"Хотите сохранить изменения в кэше?",
),
"hostsDesc": MessageLookupByLibrary.simpleMessage("Добавить Hosts"),
"hotkeyConflict": MessageLookupByLibrary.simpleMessage(
"Конфликт горячих клавиш",
),
"hotkeyManagement": MessageLookupByLibrary.simpleMessage(
"Управление горячими клавишами",
),
"hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage(
"Использование клавиатуры для управления приложением",
),
"hours": MessageLookupByLibrary.simpleMessage("Часов"),
"icon": MessageLookupByLibrary.simpleMessage("Иконка"),
"iconConfiguration": MessageLookupByLibrary.simpleMessage(
"Конфигурация иконки",
),
"iconStyle": MessageLookupByLibrary.simpleMessage("Стиль иконки"),
"importFromURL": MessageLookupByLibrary.simpleMessage("Импорт из URL"),
"infiniteTime": MessageLookupByLibrary.simpleMessage(
"Долгосрочное действие",
),
"init": MessageLookupByLibrary.simpleMessage("Инициализация"),
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, введите правильную горячую клавишу",
),
"intelligentSelected": MessageLookupByLibrary.simpleMessage(
"Интеллектуальный выбор",
),
"intranetIP": MessageLookupByLibrary.simpleMessage("Внутренний IP"),
"ipcidr": MessageLookupByLibrary.simpleMessage("IPCIDR"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage(
"При включении будет возможно получать IPv6 трафик",
),
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage(
"Разрешить входящий IPv6",
),
"ja": MessageLookupByLibrary.simpleMessage("Японский"),
"just": MessageLookupByLibrary.simpleMessage("Только что"),
"keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage(
"Интервал поддержания TCP-соединения",
),
"key": MessageLookupByLibrary.simpleMessage("Ключ"),
"language": MessageLookupByLibrary.simpleMessage("Язык"),
"layout": MessageLookupByLibrary.simpleMessage("Макет"),
"light": MessageLookupByLibrary.simpleMessage("Светлый"),
"list": MessageLookupByLibrary.simpleMessage("Список"),
"listen": MessageLookupByLibrary.simpleMessage("Слушать"),
"local": MessageLookupByLibrary.simpleMessage("Локальный"),
"localBackupDesc": MessageLookupByLibrary.simpleMessage(
"Резервное копирование локальных данных на локальный диск",
),
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage(
"Восстановление данных из файла",
),
"logLevel": MessageLookupByLibrary.simpleMessage("Уровень логов"),
"logcat": MessageLookupByLibrary.simpleMessage("Logcat"),
"logcatDesc": MessageLookupByLibrary.simpleMessage(
"Отключение скроет запись логов",
),
"logs": MessageLookupByLibrary.simpleMessage("Логи"),
"logsDesc": MessageLookupByLibrary.simpleMessage("Записи захвата логов"),
"loopback": MessageLookupByLibrary.simpleMessage(
"Инструмент разблокировки Loopback",
),
"loopbackDesc": MessageLookupByLibrary.simpleMessage(
"Используется для разблокировки Loopback UWP",
),
"loose": MessageLookupByLibrary.simpleMessage("Свободный"),
"memoryInfo": MessageLookupByLibrary.simpleMessage("Информация о памяти"),
"min": MessageLookupByLibrary.simpleMessage("Мин"),
"minimizeOnExit": MessageLookupByLibrary.simpleMessage(
"Свернуть при выходе",
),
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage(
"Изменить стандартное событие выхода из системы",
),
"minutes": MessageLookupByLibrary.simpleMessage("Минут"),
"mode": MessageLookupByLibrary.simpleMessage("Режим"),
"months": MessageLookupByLibrary.simpleMessage("Месяцев"),
"more": MessageLookupByLibrary.simpleMessage("Еще"),
"name": MessageLookupByLibrary.simpleMessage("Имя"),
"nameSort": MessageLookupByLibrary.simpleMessage("Сортировка по имени"),
"nameserver": MessageLookupByLibrary.simpleMessage("Сервер имен"),
"nameserverDesc": MessageLookupByLibrary.simpleMessage(
"Для разрешения домена",
),
"nameserverPolicy": MessageLookupByLibrary.simpleMessage(
"Политика сервера имен",
),
"nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage(
"Указать соответствующую политику сервера имен",
),
"network": MessageLookupByLibrary.simpleMessage("Сеть"),
"networkDesc": MessageLookupByLibrary.simpleMessage(
"Изменение настроек, связанных с сетью",
),
"networkDetection": MessageLookupByLibrary.simpleMessage(
"Обнаружение сети",
),
"networkSpeed": MessageLookupByLibrary.simpleMessage("Скорость сети"),
"noData": MessageLookupByLibrary.simpleMessage("Нет данных"),
"noHotKey": MessageLookupByLibrary.simpleMessage("Нет горячей клавиши"),
"noIcon": MessageLookupByLibrary.simpleMessage("Нет иконки"),
"noInfo": MessageLookupByLibrary.simpleMessage("Нет информации"),
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage(
"Нет дополнительной информации",
),
"noNetwork": MessageLookupByLibrary.simpleMessage("Нет сети"),
"noProxy": MessageLookupByLibrary.simpleMessage("Нет прокси"),
"noProxyDesc": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, создайте профиль или добавьте действительный профиль",
),
"notEmpty": MessageLookupByLibrary.simpleMessage("Не может быть пустым"),
"notSelectedTip": MessageLookupByLibrary.simpleMessage(
"Текущая группа прокси не может быть выбрана.",
),
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage(
"Нет соединений",
),
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage(
"Не удалось получить информацию о ядре",
),
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("Нет логов"),
"nullProfileDesc": MessageLookupByLibrary.simpleMessage(
"Нет профиля, пожалуйста, добавьте профиль",
),
"nullProxies": MessageLookupByLibrary.simpleMessage("Нет прокси"),
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("Нет запросов"),
"oneColumn": MessageLookupByLibrary.simpleMessage("Один столбец"),
"onlyIcon": MessageLookupByLibrary.simpleMessage("Только иконка"),
"onlyOtherApps": MessageLookupByLibrary.simpleMessage(
"Только сторонние приложения",
),
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage(
"Только статистика прокси",
),
"onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage(
"При включении будет учитываться только трафик прокси",
),
"options": MessageLookupByLibrary.simpleMessage("Опции"),
"other": MessageLookupByLibrary.simpleMessage("Другое"),
"otherContributors": MessageLookupByLibrary.simpleMessage(
"Другие участники",
),
"outboundMode": MessageLookupByLibrary.simpleMessage(
"Режим исходящего трафика",
),
"override": MessageLookupByLibrary.simpleMessage("Переопределить"),
"overrideDesc": MessageLookupByLibrary.simpleMessage(
"Переопределить конфигурацию, связанную с прокси",
),
"overrideDns": MessageLookupByLibrary.simpleMessage("Переопределить DNS"),
"overrideDnsDesc": MessageLookupByLibrary.simpleMessage(
"Включение переопределит настройки DNS в профиле",
),
"password": MessageLookupByLibrary.simpleMessage("Пароль"),
"passwordTip": MessageLookupByLibrary.simpleMessage(
"Пароль не может быть пустым",
),
"paste": MessageLookupByLibrary.simpleMessage("Вставить"),
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, привяжите WebDAV",
),
"pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, введите пароль администратора",
),
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, загрузите файл",
),
"pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, загрузите действительный QR-код",
),
"port": MessageLookupByLibrary.simpleMessage("Порт"),
"preferH3Desc": MessageLookupByLibrary.simpleMessage(
"Приоритетное использование HTTP/3 для DOH",
),
"pressKeyboard": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, нажмите клавишу.",
),
"preview": MessageLookupByLibrary.simpleMessage("Предпросмотр"),
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
"profileAutoUpdateIntervalInvalidValidationDesc":
MessageLookupByLibrary.simpleMessage(
"Пожалуйста, введите действительный формат интервала времени",
),
"profileAutoUpdateIntervalNullValidationDesc":
MessageLookupByLibrary.simpleMessage(
"Пожалуйста, введите интервал времени для автообновления",
),
"profileHasUpdate": MessageLookupByLibrary.simpleMessage(
"Профиль был изменен. Хотите отключить автообновление?",
),
"profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, введите имя профиля",
),
"profileParseErrorDesc": MessageLookupByLibrary.simpleMessage(
"ошибка разбора профиля",
),
"profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, введите действительный URL профиля",
),
"profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
"Пожалуйста, введите URL профиля",
),
"profiles": MessageLookupByLibrary.simpleMessage("Профили"),
"profilesSort": MessageLookupByLibrary.simpleMessage("Сортировка профилей"),
"project": MessageLookupByLibrary.simpleMessage("Проект"),
"providers": MessageLookupByLibrary.simpleMessage("Провайдеры"),
"proxies": MessageLookupByLibrary.simpleMessage("Прокси"),
"proxiesSetting": MessageLookupByLibrary.simpleMessage("Настройка прокси"),
"proxyGroup": MessageLookupByLibrary.simpleMessage("Группа прокси"),
"proxyNameserver": MessageLookupByLibrary.simpleMessage(
"Прокси-сервер имен",
),
"proxyNameserverDesc": MessageLookupByLibrary.simpleMessage(
"Домен для разрешения прокси-узлов",
),
"proxyPort": MessageLookupByLibrary.simpleMessage("Порт прокси"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage(
"Установить порт прослушивания Clash",
),
"proxyProviders": MessageLookupByLibrary.simpleMessage("Провайдеры прокси"),
"pureBlackMode": MessageLookupByLibrary.simpleMessage("Чисто черный режим"),
"qrcode": MessageLookupByLibrary.simpleMessage("QR-код"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage(
"Сканируйте QR-код для получения профиля",
),
"recovery": MessageLookupByLibrary.simpleMessage("Восстановление"),
"recoveryAll": MessageLookupByLibrary.simpleMessage(
"Восстановить все данные",
),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage(
"Только восстановление профилей",
),
"recoverySuccess": MessageLookupByLibrary.simpleMessage(
"Восстановление успешно",
),
"regExp": MessageLookupByLibrary.simpleMessage("Регулярное выражение"),
"remote": MessageLookupByLibrary.simpleMessage("Удаленный"),
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage(
"Резервное копирование локальных данных на WebDAV",
),
"remoteRecoveryDesc": MessageLookupByLibrary.simpleMessage(
"Восстановление данных с WebDAV",
),
"remove": MessageLookupByLibrary.simpleMessage("Удалить"),
"requests": MessageLookupByLibrary.simpleMessage("Запросы"),
"requestsDesc": MessageLookupByLibrary.simpleMessage(
"Просмотр последних записей запросов",
),
"reset": MessageLookupByLibrary.simpleMessage("Сброс"),
"resetTip": MessageLookupByLibrary.simpleMessage(
"Убедитесь, что хотите сбросить",
),
"resources": MessageLookupByLibrary.simpleMessage("Ресурсы"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage(
"Информация, связанная с внешними ресурсами",
),
"respectRules": MessageLookupByLibrary.simpleMessage("Соблюдение правил"),
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
"DNS-соединение следует правилам, необходимо настроить proxy-server-nameserver",
),
"routeAddress": MessageLookupByLibrary.simpleMessage("Адрес маршрутизации"),
"routeAddressDesc": MessageLookupByLibrary.simpleMessage(
"Настройка адреса прослушивания маршрутизации",
),
"routeMode": MessageLookupByLibrary.simpleMessage("Режим маршрутизации"),
"routeMode_bypassPrivate": MessageLookupByLibrary.simpleMessage(
"Обход частных адресов маршрутизации",
),
"routeMode_config": MessageLookupByLibrary.simpleMessage(
"Использовать конфигурацию",
),
"ru": MessageLookupByLibrary.simpleMessage("Русский"),
"rule": MessageLookupByLibrary.simpleMessage("Правило"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("Провайдеры правил"),
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
"search": MessageLookupByLibrary.simpleMessage("Поиск"),
"seconds": MessageLookupByLibrary.simpleMessage("Секунд"),
"selectAll": MessageLookupByLibrary.simpleMessage("Выбрать все"),
"selected": MessageLookupByLibrary.simpleMessage("Выбрано"),
"settings": MessageLookupByLibrary.simpleMessage("Настройки"),
"show": MessageLookupByLibrary.simpleMessage("Показать"),
"shrink": MessageLookupByLibrary.simpleMessage("Сжать"),
"silentLaunch": MessageLookupByLibrary.simpleMessage("Тихий запуск"),
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage(
"Запуск в фоновом режиме",
),
"size": MessageLookupByLibrary.simpleMessage("Размер"),
"sort": MessageLookupByLibrary.simpleMessage("Сортировка"),
"source": MessageLookupByLibrary.simpleMessage("Источник"),
"stackMode": MessageLookupByLibrary.simpleMessage("Режим стека"),
"standard": MessageLookupByLibrary.simpleMessage("Стандартный"),
"start": MessageLookupByLibrary.simpleMessage("Старт"),
"startVpn": MessageLookupByLibrary.simpleMessage("Запуск VPN..."),
"status": MessageLookupByLibrary.simpleMessage("Статус"),
"statusDesc": MessageLookupByLibrary.simpleMessage(
"Системный DNS будет использоваться при выключении",
),
"stop": MessageLookupByLibrary.simpleMessage("Стоп"),
"stopVpn": MessageLookupByLibrary.simpleMessage("Остановка VPN..."),
"style": MessageLookupByLibrary.simpleMessage("Стиль"),
"submit": MessageLookupByLibrary.simpleMessage("Отправить"),
"sync": MessageLookupByLibrary.simpleMessage("Синхронизация"),
"system": MessageLookupByLibrary.simpleMessage("Система"),
"systemFont": MessageLookupByLibrary.simpleMessage("Системный шрифт"),
"systemProxy": MessageLookupByLibrary.simpleMessage("Системный прокси"),
"systemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Прикрепить HTTP-прокси к VpnService",
),
"tab": MessageLookupByLibrary.simpleMessage("Вкладка"),
"tabAnimation": MessageLookupByLibrary.simpleMessage("Анимация вкладок"),
"tabAnimationDesc": MessageLookupByLibrary.simpleMessage(
"При включении домашняя вкладка добавит анимацию переключения",
),
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP параллелизм"),
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage(
"Включение позволит использовать параллелизм TCP",
),
"testUrl": MessageLookupByLibrary.simpleMessage("Тест URL"),
"theme": MessageLookupByLibrary.simpleMessage("Тема"),
"themeColor": MessageLookupByLibrary.simpleMessage("Цвет темы"),
"themeDesc": MessageLookupByLibrary.simpleMessage(
"Установить темный режим, настроить цвет",
),
"themeMode": MessageLookupByLibrary.simpleMessage("Режим темы"),
"threeColumns": MessageLookupByLibrary.simpleMessage("Три столбца"),
"tight": MessageLookupByLibrary.simpleMessage("Плотный"),
"time": MessageLookupByLibrary.simpleMessage("Время"),
"tip": MessageLookupByLibrary.simpleMessage("подсказка"),
"toggle": MessageLookupByLibrary.simpleMessage("Переключить"),
"tools": MessageLookupByLibrary.simpleMessage("Инструменты"),
"trafficUsage": MessageLookupByLibrary.simpleMessage(
"Использование трафика",
),
"tun": MessageLookupByLibrary.simpleMessage("TUN"),
"tunDesc": MessageLookupByLibrary.simpleMessage(
"действительно только в режиме администратора",
),
"twoColumns": MessageLookupByLibrary.simpleMessage("Два столбца"),
"unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage(
"невозможно обновить текущий профиль",
),
"unifiedDelay": MessageLookupByLibrary.simpleMessage(
"Унифицированная задержка",
),
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage(
"Убрать дополнительные задержки, такие как рукопожатие",
),
"unknown": MessageLookupByLibrary.simpleMessage("Неизвестно"),
"update": MessageLookupByLibrary.simpleMessage("Обновить"),
"upload": MessageLookupByLibrary.simpleMessage("Загрузка"),
"url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": MessageLookupByLibrary.simpleMessage(
"Получить профиль через URL",
),
"useHosts": MessageLookupByLibrary.simpleMessage("Использовать hosts"),
"useSystemHosts": MessageLookupByLibrary.simpleMessage(
"Использовать системные hosts",
),
"value": MessageLookupByLibrary.simpleMessage("Значение"),
"view": MessageLookupByLibrary.simpleMessage("Просмотр"),
"vpnDesc": MessageLookupByLibrary.simpleMessage(
"Изменение настроек, связанных с VPN",
),
"vpnEnableDesc": MessageLookupByLibrary.simpleMessage(
"Автоматически направляет весь системный трафик через VpnService",
),
"vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage(
"Прикрепить HTTP-прокси к VpnService",
),
"vpnTip": MessageLookupByLibrary.simpleMessage(
"Изменения вступят в силу после перезапуска VPN",
),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage(
"Конфигурация WebDAV",
),
"whitelistMode": MessageLookupByLibrary.simpleMessage(
"Режим белого списка",
),
"years": MessageLookupByLibrary.simpleMessage("Лет"),
"zh_CN": MessageLookupByLibrary.simpleMessage("Упрощенный китайский"),
};
}

View File

@@ -122,6 +122,7 @@ class MessageLookup extends MessageLookupByLibrary {
"desc": MessageLookupByLibrary.simpleMessage( "desc": MessageLookupByLibrary.simpleMessage(
"基于ClashMeta的多平台代理客户端简单易用开源无广告。", "基于ClashMeta的多平台代理客户端简单易用开源无广告。",
), ),
"detectionTip": MessageLookupByLibrary.simpleMessage("依赖第三方api仅供参考"),
"direct": MessageLookupByLibrary.simpleMessage("直连"), "direct": MessageLookupByLibrary.simpleMessage("直连"),
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"), "disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
"disclaimerDesc": MessageLookupByLibrary.simpleMessage( "disclaimerDesc": MessageLookupByLibrary.simpleMessage(
@@ -191,6 +192,7 @@ class MessageLookup extends MessageLookupByLibrary {
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"), "ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"), "ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
"ja": MessageLookupByLibrary.simpleMessage("日语"),
"just": MessageLookupByLibrary.simpleMessage("刚刚"), "just": MessageLookupByLibrary.simpleMessage("刚刚"),
"keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"), "keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
"key": MessageLookupByLibrary.simpleMessage(""), "key": MessageLookupByLibrary.simpleMessage(""),
@@ -198,6 +200,7 @@ class MessageLookup extends MessageLookupByLibrary {
"layout": MessageLookupByLibrary.simpleMessage("布局"), "layout": MessageLookupByLibrary.simpleMessage("布局"),
"light": MessageLookupByLibrary.simpleMessage("浅色"), "light": MessageLookupByLibrary.simpleMessage("浅色"),
"list": MessageLookupByLibrary.simpleMessage("列表"), "list": MessageLookupByLibrary.simpleMessage("列表"),
"listen": MessageLookupByLibrary.simpleMessage("监听"),
"local": MessageLookupByLibrary.simpleMessage("本地"), "local": MessageLookupByLibrary.simpleMessage("本地"),
"localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"), "localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"),
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"), "localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"),
@@ -303,7 +306,7 @@ class MessageLookup extends MessageLookupByLibrary {
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
"proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"), "proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"),
"prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"), "pureBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"),
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
"recovery": MessageLookupByLibrary.simpleMessage("恢复"), "recovery": MessageLookupByLibrary.simpleMessage("恢复"),
@@ -330,6 +333,7 @@ class MessageLookup extends MessageLookupByLibrary {
"routeMode": MessageLookupByLibrary.simpleMessage("路由模式"), "routeMode": MessageLookupByLibrary.simpleMessage("路由模式"),
"routeMode_bypassPrivate": MessageLookupByLibrary.simpleMessage("绕过私有路由地址"), "routeMode_bypassPrivate": MessageLookupByLibrary.simpleMessage("绕过私有路由地址"),
"routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"), "routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"),
"ru": MessageLookupByLibrary.simpleMessage("俄语"),
"rule": MessageLookupByLibrary.simpleMessage("规则"), "rule": MessageLookupByLibrary.simpleMessage("规则"),
"ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"), "ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"),
"save": MessageLookupByLibrary.simpleMessage("保存"), "save": MessageLookupByLibrary.simpleMessage("保存"),

View File

@@ -255,6 +255,16 @@ class AppLocalizations {
return Intl.message('English', name: 'en', desc: '', args: []); return Intl.message('English', name: 'en', desc: '', args: []);
} }
/// `Japanese`
String get ja {
return Intl.message('Japanese', name: 'ja', desc: '', args: []);
}
/// `Russian`
String get ru {
return Intl.message('Russian', name: 'ru', desc: '', args: []);
}
/// `Simplified Chinese` /// `Simplified Chinese`
String get zh_CN { String get zh_CN {
return Intl.message( return Intl.message(
@@ -1670,10 +1680,10 @@ class AppLocalizations {
); );
} }
/// `Auto lose connections` /// `Auto close connections`
String get autoCloseConnections { String get autoCloseConnections {
return Intl.message( return Intl.message(
'Auto lose connections', 'Auto close connections',
name: 'autoCloseConnections', name: 'autoCloseConnections',
desc: '', desc: '',
args: [], args: [],
@@ -1720,11 +1730,11 @@ class AppLocalizations {
); );
} }
/// `Prue black mode` /// `Pure black mode`
String get prueBlackMode { String get pureBlackMode {
return Intl.message( return Intl.message(
'Prue black mode', 'Pure black mode',
name: 'prueBlackMode', name: 'pureBlackMode',
desc: '', desc: '',
args: [], args: [],
); );
@@ -2669,6 +2679,21 @@ class AppLocalizations {
args: [], args: [],
); );
} }
/// `Relying on third-party api is for reference only`
String get detectionTip {
return Intl.message(
'Relying on third-party api is for reference only',
name: 'detectionTip',
desc: '',
args: [],
);
}
/// `Listen`
String get listen {
return Intl.message('Listen', name: 'listen', desc: '', args: []);
}
} }
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> { class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
@@ -2677,6 +2702,8 @@ class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {
List<Locale> get supportedLocales { List<Locale> get supportedLocales {
return const <Locale>[ return const <Locale>[
Locale.fromSubtags(languageCode: 'en'), Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'ja'),
Locale.fromSubtags(languageCode: 'ru'),
Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'), Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'),
]; ];
} }

View File

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

View File

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

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