Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dda2854be | ||
|
|
5184ed6fc7 | ||
|
|
4e679f776e | ||
|
|
96328f66e9 | ||
|
|
3eb14ab8a1 | ||
|
|
c6266b7917 | ||
|
|
6c27f2e2f1 | ||
|
|
e04a0094b1 | ||
|
|
683e6a58ea |
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
52
CHANGELOG.md
52
CHANGELOG.md
@@ -1,3 +1,55 @@
|
|||||||
|
## v0.8.77
|
||||||
|
|
||||||
|
- Optimize performance
|
||||||
|
|
||||||
|
- Update core
|
||||||
|
|
||||||
|
- Optimize core stability
|
||||||
|
|
||||||
|
- Fix linux tun authority check error
|
||||||
|
|
||||||
|
- Fix some issues
|
||||||
|
|
||||||
|
- Fix scroll physics error
|
||||||
|
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
|
## v0.8.75
|
||||||
|
|
||||||
|
- Add windows storage corruption detection
|
||||||
|
|
||||||
|
- Fix core crash caused by windows resource manager restart
|
||||||
|
|
||||||
|
- Optimize logs, requests, access to pages
|
||||||
|
|
||||||
|
- Fix macos bypass domain issues
|
||||||
|
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
|
## v0.8.74
|
||||||
|
|
||||||
|
- Fix some issues
|
||||||
|
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
|
## v0.8.73
|
||||||
|
|
||||||
|
- Update popup menu
|
||||||
|
|
||||||
|
- Add file editor
|
||||||
|
|
||||||
|
- Fix android service issues
|
||||||
|
|
||||||
|
- Optimize desktop background performance
|
||||||
|
|
||||||
|
- Optimize android main process performance
|
||||||
|
|
||||||
|
- Optimize delay test
|
||||||
|
|
||||||
|
- Optimize vpn protect
|
||||||
|
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
## v0.8.72
|
## v0.8.72
|
||||||
|
|
||||||
- Update core
|
- Update core
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ data class Package(
|
|||||||
val packageName: String,
|
val packageName: String,
|
||||||
val label: String,
|
val label: String,
|
||||||
val isSystem: Boolean,
|
val isSystem: Boolean,
|
||||||
val firstInstallTime: Long,
|
val lastUpdateTime: Long,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
@@ -302,7 +301,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
|||||||
packageName = it.packageName,
|
packageName = it.packageName,
|
||||||
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
||||||
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
|
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
|
||||||
firstInstallTime = it.firstInstallTime
|
lastUpdateTime = it.lastUpdateTime
|
||||||
)
|
)
|
||||||
}?.let { packages.addAll(it) }
|
}?.let { packages.addAll(it) }
|
||||||
return packages
|
return packages
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Submodule core/Clash.Meta updated: 0c03d8e4b4...76b0d7e8bc
@@ -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), ¶ms)
|
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||||
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), ¶ms)
|
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
isRunning = false
|
isRunning = false
|
||||||
runLock sync.Mutex
|
runLock sync.Mutex
|
||||||
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"}
|
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
|
||||||
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -215,6 +215,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
|||||||
targetConfig.Tun.Device = patchConfig.Tun.Device
|
targetConfig.Tun.Device = patchConfig.Tun.Device
|
||||||
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
||||||
targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
||||||
|
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
|
||||||
targetConfig.GeodataLoader = patchConfig.GeodataLoader
|
targetConfig.GeodataLoader = patchConfig.GeodataLoader
|
||||||
targetConfig.Profile.StoreSelected = false
|
targetConfig.Profile.StoreSelected = false
|
||||||
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
41
core/go.mod
41
core/go.mod
@@ -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
|
||||||
|
|||||||
88
core/go.sum
88
core/go.sum
@@ -24,12 +24,12 @@ github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.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=
|
||||||
|
|||||||
25
core/hub.go
25
core/hub.go
@@ -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{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -52,18 +52,6 @@ func NewInvokeManager() *InvokeManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *InvokeManager) load(id string) string {
|
|
||||||
res, ok := m.invokeMap.Load(id)
|
|
||||||
if ok {
|
|
||||||
return res.(string)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *InvokeManager) delete(id string) {
|
|
||||||
m.invokeMap.Delete(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *InvokeManager) completer(id string, value string) {
|
func (m *InvokeManager) completer(id string, value string) {
|
||||||
m.invokeMap.Store(id, value)
|
m.invokeMap.Store(id, value)
|
||||||
m.chanLock.Lock()
|
m.chanLock.Lock()
|
||||||
@@ -74,7 +62,7 @@ func (m *InvokeManager) completer(id string, value string) {
|
|||||||
m.chanLock.Unlock()
|
m.chanLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *InvokeManager) await(id string) {
|
func (m *InvokeManager) await(id string) string {
|
||||||
m.chanLock.Lock()
|
m.chanLock.Lock()
|
||||||
if _, ok := m.chanMap[id]; !ok {
|
if _, ok := m.chanMap[id]; !ok {
|
||||||
m.chanMap[id] = make(chan struct{})
|
m.chanMap[id] = make(chan struct{})
|
||||||
@@ -85,12 +73,17 @@ func (m *InvokeManager) await(id string) {
|
|||||||
timeout := time.After(500 * time.Millisecond)
|
timeout := time.After(500 * time.Millisecond)
|
||||||
select {
|
select {
|
||||||
case <-ch:
|
case <-ch:
|
||||||
return
|
res, ok := m.invokeMap.Load(id)
|
||||||
|
m.invokeMap.Delete(id)
|
||||||
|
if ok {
|
||||||
|
return res.(string)
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
case <-timeout:
|
case <-timeout:
|
||||||
m.completer(id, "")
|
m.completer(id, "")
|
||||||
return
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -195,7 +188,6 @@ func initSocketHook() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
fdInvokeMap.await(id)
|
fdInvokeMap.await(id)
|
||||||
fdInvokeMap.delete(id)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,10 +206,7 @@ func init() {
|
|||||||
Id: id,
|
Id: id,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
})
|
})
|
||||||
processInvokeMap.await(id)
|
return processInvokeMap.await(id), nil
|
||||||
res := processInvokeMap.load(id)
|
|
||||||
processInvokeMap.delete(id)
|
|
||||||
return res, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,14 +214,14 @@ func handleGetAndroidVpnOptions() string {
|
|||||||
tunLock.Lock()
|
tunLock.Lock()
|
||||||
defer tunLock.Unlock()
|
defer tunLock.Unlock()
|
||||||
options := state.AndroidVpnOptions{
|
options := state.AndroidVpnOptions{
|
||||||
Enable: state.CurrentState.Enable,
|
Enable: state.CurrentState.VpnProps.Enable,
|
||||||
Port: currentConfig.General.MixedPort,
|
Port: currentConfig.General.MixedPort,
|
||||||
Ipv4Address: state.DefaultIpv4Address,
|
Ipv4Address: state.DefaultIpv4Address,
|
||||||
Ipv6Address: state.GetIpv6Address(),
|
Ipv6Address: state.GetIpv6Address(),
|
||||||
AccessControl: state.CurrentState.AccessControl,
|
AccessControl: state.CurrentState.VpnProps.AccessControl,
|
||||||
SystemProxy: state.CurrentState.SystemProxy,
|
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
|
||||||
AllowBypass: state.CurrentState.AllowBypass,
|
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
|
||||||
RouteAddress: state.CurrentState.RouteAddress,
|
RouteAddress: currentConfig.General.Tun.RouteAddress,
|
||||||
BypassDomain: state.CurrentState.BypassDomain,
|
BypassDomain: state.CurrentState.BypassDomain,
|
||||||
DnsServerAddress: state.GetDnsServerAddress(),
|
DnsServerAddress: state.GetDnsServerAddress(),
|
||||||
}
|
}
|
||||||
@@ -244,10 +233,6 @@ func handleGetAndroidVpnOptions() string {
|
|||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSetState(params string) {
|
|
||||||
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleUpdateDns(value string) {
|
func handleUpdateDns(value string) {
|
||||||
go func() {
|
go func() {
|
||||||
log.Infoln("[DNS] updateDns %s", value)
|
log.Infoln("[DNS] updateDns %s", value)
|
||||||
@@ -263,46 +248,41 @@ func handleGetCurrentProfileName() string {
|
|||||||
return state.CurrentState.CurrentProfileName
|
return state.CurrentState.CurrentProfileName
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextHandle(action *Action, send func([]byte)) bool {
|
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||||
switch action.Method {
|
switch action.Method {
|
||||||
case startTunMethod:
|
case startTunMethod:
|
||||||
data := action.Data.(string)
|
data := action.Data.(string)
|
||||||
var fd int
|
var fd int
|
||||||
_ = json.Unmarshal([]byte(data), &fd)
|
_ = json.Unmarshal([]byte(data), &fd)
|
||||||
send(action.wrapMessage(handleStartTun(fd)))
|
result(handleStartTun(fd))
|
||||||
return true
|
return true
|
||||||
case stopTunMethod:
|
case stopTunMethod:
|
||||||
handleStopTun()
|
handleStopTun()
|
||||||
send(action.wrapMessage(true))
|
result(true)
|
||||||
return true
|
|
||||||
case setStateMethod:
|
|
||||||
data := action.Data.(string)
|
|
||||||
handleSetState(data)
|
|
||||||
send(action.wrapMessage(true))
|
|
||||||
return true
|
return true
|
||||||
case getAndroidVpnOptionsMethod:
|
case getAndroidVpnOptionsMethod:
|
||||||
send(action.wrapMessage(handleGetAndroidVpnOptions()))
|
result(handleGetAndroidVpnOptions())
|
||||||
return true
|
return true
|
||||||
case updateDnsMethod:
|
case updateDnsMethod:
|
||||||
data := action.Data.(string)
|
data := action.Data.(string)
|
||||||
handleUpdateDns(data)
|
handleUpdateDns(data)
|
||||||
send(action.wrapMessage(true))
|
result(true)
|
||||||
return true
|
return true
|
||||||
case setFdMapMethod:
|
case setFdMapMethod:
|
||||||
fdId := action.Data.(string)
|
fdId := action.Data.(string)
|
||||||
handleSetFdMap(fdId)
|
handleSetFdMap(fdId)
|
||||||
send(action.wrapMessage(true))
|
result(true)
|
||||||
return true
|
return true
|
||||||
case setProcessMapMethod:
|
case setProcessMapMethod:
|
||||||
data := action.Data.(string)
|
data := action.Data.(string)
|
||||||
handleSetProcessMap(data)
|
handleSetProcessMap(data)
|
||||||
send(action.wrapMessage(true))
|
result(true)
|
||||||
return true
|
return true
|
||||||
case getRunTimeMethod:
|
case getRunTimeMethod:
|
||||||
send(action.wrapMessage(handleGetRunTime()))
|
result(handleGetRunTime())
|
||||||
return true
|
return true
|
||||||
case getCurrentProfileNameMethod:
|
case getCurrentProfileNameMethod:
|
||||||
send(action.wrapMessage(handleGetCurrentProfileName()))
|
result(handleGetCurrentProfileName())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -315,7 +295,10 @@ func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, po
|
|||||||
bytes := []byte(C.GoString(paramsChar))
|
bytes := []byte(C.GoString(paramsChar))
|
||||||
stateParams := C.GoString(stateParamsChar)
|
stateParams := C.GoString(stateParamsChar)
|
||||||
go func() {
|
go func() {
|
||||||
handleInitClash(dir)
|
res := handleInitClash(dir)
|
||||||
|
if res == false {
|
||||||
|
bridge.SendToPort(i, "init error")
|
||||||
|
}
|
||||||
handleSetState(stateParams)
|
handleSetState(stateParams)
|
||||||
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -7,57 +7,27 @@ import 'package:fl_clash/l10n/l10n.dart';
|
|||||||
import 'package:fl_clash/manager/hotkey_manager.dart';
|
import 'package:fl_clash/manager/hotkey_manager.dart';
|
||||||
import 'package:fl_clash/manager/manager.dart';
|
import 'package:fl_clash/manager/manager.dart';
|
||||||
import 'package:fl_clash/plugins/app.dart';
|
import 'package:fl_clash/plugins/app.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'controller.dart';
|
import 'controller.dart';
|
||||||
import 'models/models.dart';
|
import 'models/models.dart';
|
||||||
import 'pages/pages.dart';
|
import 'pages/pages.dart';
|
||||||
|
|
||||||
runAppWithPreferences(
|
class Application extends ConsumerStatefulWidget {
|
||||||
Widget child, {
|
|
||||||
required AppState appState,
|
|
||||||
required Config config,
|
|
||||||
required AppFlowingState appFlowingState,
|
|
||||||
required ClashConfig clashConfig,
|
|
||||||
}) {
|
|
||||||
runApp(MultiProvider(
|
|
||||||
providers: [
|
|
||||||
ChangeNotifierProvider<ClashConfig>(
|
|
||||||
create: (_) => clashConfig,
|
|
||||||
),
|
|
||||||
ChangeNotifierProvider<Config>(
|
|
||||||
create: (_) => config,
|
|
||||||
),
|
|
||||||
ChangeNotifierProvider<AppFlowingState>(
|
|
||||||
create: (_) => appFlowingState,
|
|
||||||
),
|
|
||||||
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
|
|
||||||
create: (_) => appState,
|
|
||||||
update: (_, config, clashConfig, appState) {
|
|
||||||
appState?.mode = clashConfig.mode;
|
|
||||||
appState?.selectedMap = config.currentSelectedMap;
|
|
||||||
return appState!;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
child: child,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
class Application extends StatefulWidget {
|
|
||||||
const Application({
|
const Application({
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<Application> createState() => ApplicationState();
|
ConsumerState<Application> createState() => ApplicationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApplicationState extends State<Application> {
|
class ApplicationState extends ConsumerState<Application> {
|
||||||
late SystemColorSchemes systemColorSchemes;
|
late ColorSchemes systemColorSchemes;
|
||||||
Timer? _autoUpdateGroupTaskTimer;
|
Timer? _autoUpdateGroupTaskTimer;
|
||||||
Timer? _autoUpdateProfilesTaskTimer;
|
Timer? _autoUpdateProfilesTaskTimer;
|
||||||
|
|
||||||
@@ -73,7 +43,7 @@ class ApplicationState extends State<Application> {
|
|||||||
ColorScheme _getAppColorScheme({
|
ColorScheme _getAppColorScheme({
|
||||||
required Brightness brightness,
|
required Brightness brightness,
|
||||||
int? primaryColor,
|
int? primaryColor,
|
||||||
required SystemColorSchemes systemColorSchemes,
|
required ColorSchemes systemColorSchemes,
|
||||||
}) {
|
}) {
|
||||||
if (primaryColor != null) {
|
if (primaryColor != null) {
|
||||||
return ColorScheme.fromSeed(
|
return ColorScheme.fromSeed(
|
||||||
@@ -81,7 +51,7 @@ class ApplicationState extends State<Application> {
|
|||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness);
|
return systemColorSchemes.getColorSchemeForBrightness(brightness);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +60,21 @@ class ApplicationState extends State<Application> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_autoUpdateGroupTask();
|
_autoUpdateGroupTask();
|
||||||
_autoUpdateProfilesTask();
|
_autoUpdateProfilesTask();
|
||||||
globalState.appController = AppController(context);
|
globalState.appController = AppController(context, ref);
|
||||||
globalState.measure = Measure.of(context);
|
globalState.measure = Measure.of(context);
|
||||||
|
ref.listenManual(themeSettingProvider.select((state) => state.fontFamily),
|
||||||
|
(prev, next) {
|
||||||
|
if (prev != next) {
|
||||||
|
globalState.measure = Measure.of(
|
||||||
|
context,
|
||||||
|
fontFamily: next.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, fireImmediately: true);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||||
final currentContext = globalState.navigatorKey.currentContext;
|
final currentContext = globalState.navigatorKey.currentContext;
|
||||||
if (currentContext != null) {
|
if (currentContext != null) {
|
||||||
globalState.appController = AppController(currentContext);
|
globalState.appController = AppController(currentContext, ref);
|
||||||
}
|
}
|
||||||
await globalState.appController.init();
|
await globalState.appController.init();
|
||||||
globalState.appController.initLink();
|
globalState.appController.initLink();
|
||||||
@@ -167,7 +146,7 @@ class ApplicationState extends State<Application> {
|
|||||||
ColorScheme? lightDynamic,
|
ColorScheme? lightDynamic,
|
||||||
ColorScheme? darkDynamic,
|
ColorScheme? darkDynamic,
|
||||||
) {
|
) {
|
||||||
systemColorSchemes = SystemColorSchemes(
|
systemColorSchemes = ColorSchemes(
|
||||||
lightColorScheme: lightDynamic,
|
lightColorScheme: lightDynamic,
|
||||||
darkColorScheme: darkDynamic,
|
darkColorScheme: darkDynamic,
|
||||||
);
|
);
|
||||||
@@ -180,15 +159,11 @@ class ApplicationState extends State<Application> {
|
|||||||
Widget build(context) {
|
Widget build(context) {
|
||||||
return _buildPlatformWrap(
|
return _buildPlatformWrap(
|
||||||
_buildWrap(
|
_buildWrap(
|
||||||
Selector2<AppState, Config, ApplicationSelectorState>(
|
Consumer(
|
||||||
selector: (_, appState, config) => ApplicationSelectorState(
|
builder: (_, ref, child) {
|
||||||
locale: config.appSetting.locale,
|
final locale =
|
||||||
themeMode: config.themeProps.themeMode,
|
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||||
primaryColor: config.themeProps.primaryColor,
|
final themeProps = ref.watch(themeSettingProvider);
|
||||||
prueBlack: config.themeProps.prueBlack,
|
|
||||||
fontFamily: config.themeProps.fontFamily,
|
|
||||||
),
|
|
||||||
builder: (_, state, child) {
|
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) {
|
builder: (lightDynamic, darkDynamic) {
|
||||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||||
@@ -204,11 +179,9 @@ class ApplicationState extends State<Application> {
|
|||||||
return MessageManager(
|
return MessageManager(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (_, container) {
|
builder: (_, container) {
|
||||||
final appController = globalState.appController;
|
globalState.appController.updateViewWidth(
|
||||||
final maxWidth = container.maxWidth;
|
container.maxWidth,
|
||||||
if (appController.appState.viewWidth != maxWidth) {
|
);
|
||||||
globalState.appController.updateViewWidth(maxWidth);
|
|
||||||
}
|
|
||||||
return _buildPage(child!);
|
return _buildPage(child!);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -216,28 +189,28 @@ class ApplicationState extends State<Application> {
|
|||||||
},
|
},
|
||||||
scrollBehavior: BaseScrollBehavior(),
|
scrollBehavior: BaseScrollBehavior(),
|
||||||
title: appName,
|
title: appName,
|
||||||
locale: other.getLocaleForString(state.locale),
|
locale: other.getLocaleForString(locale),
|
||||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||||
themeMode: state.themeMode,
|
themeMode: themeProps.themeMode,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
fontFamily: state.fontFamily.value,
|
fontFamily: themeProps.fontFamily.value,
|
||||||
pageTransitionsTheme: _pageTransitionsTheme,
|
pageTransitionsTheme: _pageTransitionsTheme,
|
||||||
colorScheme: _getAppColorScheme(
|
colorScheme: _getAppColorScheme(
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
systemColorSchemes: systemColorSchemes,
|
systemColorSchemes: systemColorSchemes,
|
||||||
primaryColor: state.primaryColor,
|
primaryColor: themeProps.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
fontFamily: state.fontFamily.value,
|
fontFamily: themeProps.fontFamily.value,
|
||||||
pageTransitionsTheme: _pageTransitionsTheme,
|
pageTransitionsTheme: _pageTransitionsTheme,
|
||||||
colorScheme: _getAppColorScheme(
|
colorScheme: _getAppColorScheme(
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
systemColorSchemes: systemColorSchemes,
|
systemColorSchemes: systemColorSchemes,
|
||||||
primaryColor: state.primaryColor,
|
primaryColor: themeProps.primaryColor,
|
||||||
).toPrueBlack(state.prueBlack),
|
).toPrueBlack(themeProps.prueBlack),
|
||||||
),
|
),
|
||||||
home: child,
|
home: child,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ClashCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> initGeo() async {
|
static Future<void> initGeo() async {
|
||||||
final homePath = await appPath.getHomeDirPath();
|
final homePath = await appPath.homeDirPath;
|
||||||
final homeDir = Directory(homePath);
|
final homeDir = Directory(homePath);
|
||||||
final isExists = await homeDir.exists();
|
final isExists = await homeDir.exists();
|
||||||
if (!isExists) {
|
if (!isExists) {
|
||||||
@@ -63,15 +63,16 @@ class ClashCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> init({
|
Future<bool> init() async {
|
||||||
required ClashConfig clashConfig,
|
|
||||||
required Config config,
|
|
||||||
}) async {
|
|
||||||
await initGeo();
|
await initGeo();
|
||||||
final homeDirPath = await appPath.getHomeDirPath();
|
final homeDirPath = await appPath.homeDirPath;
|
||||||
return await clashInterface.init(homeDirPath);
|
return await clashInterface.init(homeDirPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> setState(CoreState state) async {
|
||||||
|
return await clashInterface.setState(state);
|
||||||
|
}
|
||||||
|
|
||||||
shutdown() async {
|
shutdown() async {
|
||||||
await clashInterface.shutdown();
|
await clashInterface.shutdown();
|
||||||
}
|
}
|
||||||
@@ -234,6 +235,14 @@ class ClashCore {
|
|||||||
return int.parse(value);
|
return int.parse(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ClashConfig?> getProfile(String id) async {
|
||||||
|
final res = await clashInterface.getProfile(id);
|
||||||
|
if (res.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ClashConfig.fromJson(json.decode(res));
|
||||||
|
}
|
||||||
|
|
||||||
resetTraffic() {
|
resetTraffic() {
|
||||||
clashInterface.resetTraffic();
|
clashInterface.resetTraffic();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:fl_clash/clash/message.dart';
|
import 'package:fl_clash/clash/message.dart';
|
||||||
import 'package:fl_clash/common/constant.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/common/future.dart';
|
|
||||||
import 'package:fl_clash/common/other.dart';
|
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:flutter/material.dart' hide Action;
|
|
||||||
|
|
||||||
mixin ClashInterface {
|
mixin ClashInterface {
|
||||||
Future<bool> init(String homeDir);
|
Future<bool> init(String homeDir);
|
||||||
@@ -66,6 +63,10 @@ mixin ClashInterface {
|
|||||||
FutureOr<bool> closeConnection(String id);
|
FutureOr<bool> closeConnection(String id);
|
||||||
|
|
||||||
FutureOr<bool> closeConnections();
|
FutureOr<bool> closeConnections();
|
||||||
|
|
||||||
|
FutureOr<String> getProfile(String id);
|
||||||
|
|
||||||
|
Future<bool> setState(CoreState state);
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin AndroidClashInterface {
|
mixin AndroidClashInterface {
|
||||||
@@ -73,8 +74,6 @@ mixin AndroidClashInterface {
|
|||||||
|
|
||||||
Future<bool> setProcessMap(ProcessMapItem item);
|
Future<bool> setProcessMap(ProcessMapItem item);
|
||||||
|
|
||||||
Future<bool> setState(CoreState state);
|
|
||||||
|
|
||||||
Future<bool> stopTun();
|
Future<bool> stopTun();
|
||||||
|
|
||||||
Future<bool> updateDns(String value);
|
Future<bool> updateDns(String value);
|
||||||
@@ -106,6 +105,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
|||||||
case ActionMethod.closeConnections:
|
case ActionMethod.closeConnections:
|
||||||
case ActionMethod.closeConnection:
|
case ActionMethod.closeConnection:
|
||||||
case ActionMethod.stopListener:
|
case ActionMethod.stopListener:
|
||||||
|
case ActionMethod.setState:
|
||||||
completer?.complete(result.data as bool);
|
completer?.complete(result.data as bool);
|
||||||
return;
|
return;
|
||||||
case ActionMethod.changeProxy:
|
case ActionMethod.changeProxy:
|
||||||
@@ -137,7 +137,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
|||||||
completer?.complete(result.data);
|
completer?.complete(result.data);
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
debugPrint(result.id);
|
commonPrint.log(result.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +198,14 @@ abstract class ClashHandlerInterface with ClashInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> setState(CoreState state) {
|
||||||
|
return invoke<bool>(
|
||||||
|
method: ActionMethod.setState,
|
||||||
|
data: json.encode(state),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
shutdown() async {
|
shutdown() async {
|
||||||
return await invoke<bool>(
|
return await invoke<bool>(
|
||||||
@@ -232,6 +240,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
|||||||
return await invoke<String>(
|
return await invoke<String>(
|
||||||
method: ActionMethod.updateConfig,
|
method: ActionMethod.updateConfig,
|
||||||
data: json.encode(updateConfigParams),
|
data: json.encode(updateConfigParams),
|
||||||
|
timeout: Duration(minutes: 2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +248,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
|||||||
Future<String> getProxies() {
|
Future<String> getProxies() {
|
||||||
return invoke<String>(
|
return invoke<String>(
|
||||||
method: ActionMethod.getProxies,
|
method: ActionMethod.getProxies,
|
||||||
|
timeout: Duration(seconds: 5),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,9 +278,9 @@ abstract class ClashHandlerInterface with ClashInterface {
|
|||||||
@override
|
@override
|
||||||
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||||
return invoke<String>(
|
return invoke<String>(
|
||||||
method: ActionMethod.updateGeoData,
|
method: ActionMethod.updateGeoData,
|
||||||
data: json.encode(params),
|
data: json.encode(params),
|
||||||
);
|
timeout: Duration(minutes: 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -292,6 +302,7 @@ abstract class ClashHandlerInterface with ClashInterface {
|
|||||||
return invoke<String>(
|
return invoke<String>(
|
||||||
method: ActionMethod.updateExternalProvider,
|
method: ActionMethod.updateExternalProvider,
|
||||||
data: providerName,
|
data: providerName,
|
||||||
|
timeout: Duration(minutes: 1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +328,14 @@ abstract class ClashHandlerInterface with ClashInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> getProfile(String id) {
|
||||||
|
return invoke<String>(
|
||||||
|
method: ActionMethod.getProfile,
|
||||||
|
data: id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<String> getTotalTraffic() {
|
FutureOr<String> getTotalTraffic() {
|
||||||
return invoke<String>(
|
return invoke<String>(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ extension ColorExtension on Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Color get toSoft {
|
Color get toSoft {
|
||||||
return withOpacity(0.12);
|
return withOpacity(0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color get toLittle {
|
Color get toLittle {
|
||||||
|
|||||||
@@ -34,4 +34,6 @@ export 'text.dart';
|
|||||||
export 'tray.dart';
|
export 'tray.dart';
|
||||||
export 'window.dart';
|
export 'window.dart';
|
||||||
export 'windows.dart';
|
export 'windows.dart';
|
||||||
export 'render.dart';
|
export 'render.dart';
|
||||||
|
export 'mixin.dart';
|
||||||
|
export 'print.dart';
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -22,4 +22,23 @@ extension BuildContextExtension on BuildContext {
|
|||||||
ColorScheme get colorScheme => Theme.of(this).colorScheme;
|
ColorScheme get colorScheme => Theme.of(this).colorScheme;
|
||||||
|
|
||||||
TextTheme get textTheme => Theme.of(this).textTheme;
|
TextTheme get textTheme => Theme.of(this).textTheme;
|
||||||
|
|
||||||
|
T? findLastStateOfType<T extends State>() {
|
||||||
|
T? state;
|
||||||
|
|
||||||
|
visitor(Element element) {
|
||||||
|
if(!element.mounted){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(element is StatefulElement){
|
||||||
|
if (element.state is T) {
|
||||||
|
state = element.state as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.visitChildren(visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
visitor(this as Element);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
class Debouncer {
|
class Debouncer {
|
||||||
Map<dynamic, Timer> operators = {};
|
final Map<dynamic, Timer> _operations = {};
|
||||||
|
|
||||||
call(
|
call(
|
||||||
dynamic tag,
|
dynamic tag,
|
||||||
@@ -9,14 +9,15 @@ class Debouncer {
|
|||||||
List<dynamic>? args,
|
List<dynamic>? args,
|
||||||
Duration duration = const Duration(milliseconds: 600),
|
Duration duration = const Duration(milliseconds: 600),
|
||||||
}) {
|
}) {
|
||||||
final timer = operators[tag];
|
final timer = _operations[tag];
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
}
|
}
|
||||||
operators[tag] = Timer(
|
_operations[tag] = Timer(
|
||||||
duration,
|
duration,
|
||||||
() {
|
() {
|
||||||
operators.remove(tag);
|
_operations[tag]?.cancel();
|
||||||
|
_operations.remove(tag);
|
||||||
Function.apply(
|
Function.apply(
|
||||||
func,
|
func,
|
||||||
args,
|
args,
|
||||||
@@ -26,8 +27,59 @@ class Debouncer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancel(dynamic tag) {
|
cancel(dynamic tag) {
|
||||||
operators[tag]?.cancel();
|
_operations[tag]?.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Throttler {
|
||||||
|
final Map<dynamic, Timer> _operations = {};
|
||||||
|
|
||||||
|
call(
|
||||||
|
String tag,
|
||||||
|
Function func, {
|
||||||
|
List<dynamic>? args,
|
||||||
|
Duration duration = const Duration(milliseconds: 600),
|
||||||
|
}) {
|
||||||
|
final timer = _operations[tag];
|
||||||
|
if (timer != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_operations[tag] = Timer(
|
||||||
|
duration,
|
||||||
|
() {
|
||||||
|
_operations[tag]?.cancel();
|
||||||
|
_operations.remove(tag);
|
||||||
|
Function.apply(
|
||||||
|
func,
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(dynamic tag) {
|
||||||
|
_operations[tag]?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> retry<T>({
|
||||||
|
required Future<T> Function() task,
|
||||||
|
int maxAttempts = 3,
|
||||||
|
required bool Function(T res) retryIf,
|
||||||
|
Duration delay = Duration.zero,
|
||||||
|
}) async {
|
||||||
|
int attempts = 0;
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
final res = await task();
|
||||||
|
if (!retryIf(res) || attempts >= maxAttempts) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
throw "unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
final debouncer = Debouncer();
|
final debouncer = Debouncer();
|
||||||
|
|
||||||
|
final throttler = Throttler();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'constant.dart';
|
|
||||||
|
|
||||||
class FlClashHttpOverrides extends HttpOverrides {
|
class FlClashHttpOverrides extends HttpOverrides {
|
||||||
@override
|
@override
|
||||||
@@ -14,10 +13,9 @@ class FlClashHttpOverrides extends HttpOverrides {
|
|||||||
if ([localhost].contains(url.host)) {
|
if ([localhost].contains(url.host)) {
|
||||||
return "DIRECT";
|
return "DIRECT";
|
||||||
}
|
}
|
||||||
final appController = globalState.appController;
|
final port = globalState.config.patchClashConfig.mixedPort;
|
||||||
final port = appController.clashConfig.mixedPort;
|
final isStart = globalState.appState.runTime != null;
|
||||||
final isStart = appController.appFlowingState.isStart;
|
commonPrint.log("find $url proxy:$isStart");
|
||||||
debugPrint("find $url proxy:$isStart");
|
|
||||||
if (!isStart) return "DIRECT";
|
if (!isStart) return "DIRECT";
|
||||||
return "PROXY localhost:$port";
|
return "PROXY localhost:$port";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|||||||
@@ -1,3 +1,66 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
class FixedList<T> {
|
||||||
|
final int maxLength;
|
||||||
|
final List<T> _list;
|
||||||
|
|
||||||
|
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
|
||||||
|
|
||||||
|
add(T item) {
|
||||||
|
if (_list.length == maxLength) {
|
||||||
|
_list.removeAt(0);
|
||||||
|
}
|
||||||
|
_list.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
_list.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<T> get list => List.unmodifiable(_list);
|
||||||
|
|
||||||
|
int get length => _list.length;
|
||||||
|
|
||||||
|
T operator [](int index) => _list[index];
|
||||||
|
|
||||||
|
FixedList<T> copyWith() {
|
||||||
|
return FixedList(
|
||||||
|
maxLength,
|
||||||
|
list: _list,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FixedMap<K, V> {
|
||||||
|
final int maxSize;
|
||||||
|
final Map<K, V> _map = {};
|
||||||
|
final Queue<K> _queue = Queue<K>();
|
||||||
|
|
||||||
|
FixedMap(this.maxSize);
|
||||||
|
|
||||||
|
put(K key, V value) {
|
||||||
|
if (_map.length == maxSize) {
|
||||||
|
final oldestKey = _queue.removeFirst();
|
||||||
|
_map.remove(oldestKey);
|
||||||
|
}
|
||||||
|
_map[key] = value;
|
||||||
|
_queue.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
_map.clear();
|
||||||
|
_queue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
V? get(K key) => _map[key];
|
||||||
|
|
||||||
|
bool containsKey(K key) => _map.containsKey(key);
|
||||||
|
|
||||||
|
int get length => _map.length;
|
||||||
|
|
||||||
|
Map<K, V> get map => Map.unmodifiable(_map);
|
||||||
|
}
|
||||||
|
|
||||||
extension ListExtension<T> on List<T> {
|
extension ListExtension<T> on List<T> {
|
||||||
List<T> intersection(List<T> list) {
|
List<T> intersection(List<T> list) {
|
||||||
return where((item) => list.contains(item)).toList();
|
return where((item) => list.contains(item)).toList();
|
||||||
@@ -17,8 +80,8 @@ extension ListExtension<T> on List<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<T> safeSublist(int start) {
|
List<T> safeSublist(int start) {
|
||||||
if(start <= 0) return this;
|
if (start <= 0) return this;
|
||||||
if(start > length) return [];
|
if (start > length) return [];
|
||||||
return sublist(start);
|
return sublist(start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class SingleInstanceLock {
|
|||||||
|
|
||||||
Future<bool> acquire() async {
|
Future<bool> acquire() async {
|
||||||
try {
|
try {
|
||||||
final lockFilePath = await appPath.getLockFilePath();
|
final lockFilePath = await appPath.lockFilePath;
|
||||||
final lockFile = File(lockFilePath);
|
final lockFile = File(lockFilePath);
|
||||||
await lockFile.create();
|
await lockFile.create();
|
||||||
_accessFile = await lockFile.open(mode: FileMode.write);
|
_accessFile = await lockFile.open(mode: FileMode.write);
|
||||||
|
|||||||
@@ -4,20 +4,32 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class Measure {
|
class Measure {
|
||||||
final TextScaler _textScale;
|
final TextScaler _textScale;
|
||||||
late BuildContext context;
|
final BuildContext context;
|
||||||
|
final String? _fontFamily;
|
||||||
|
|
||||||
Measure.of(this.context)
|
Measure.of(this.context, {String? fontFamily})
|
||||||
: _textScale = TextScaler.linear(
|
: _textScale = TextScaler.linear(
|
||||||
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
|
||||||
);
|
),
|
||||||
|
_fontFamily = fontFamily ?? "";
|
||||||
|
|
||||||
Size computeTextSize(Text text) {
|
Size computeTextSize(
|
||||||
|
Text text, {
|
||||||
|
double maxWidth = double.infinity,
|
||||||
|
}) {
|
||||||
final textPainter = TextPainter(
|
final textPainter = TextPainter(
|
||||||
text: TextSpan(text: text.data, style: text.style),
|
text: TextSpan(
|
||||||
|
text: text.data,
|
||||||
|
style: text.style?.copyWith(
|
||||||
|
fontFamily: _fontFamily,
|
||||||
|
),
|
||||||
|
),
|
||||||
maxLines: text.maxLines,
|
maxLines: text.maxLines,
|
||||||
textScaler: _textScale,
|
textScaler: _textScale,
|
||||||
textDirection: text.textDirection ?? TextDirection.ltr,
|
textDirection: text.textDirection ?? TextDirection.ltr,
|
||||||
)..layout();
|
)..layout(
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
);
|
||||||
return textPainter.size;
|
return textPainter.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
46
lib/common/mixin.dart
Normal file
46
lib/common/mixin.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'context.dart';
|
||||||
|
|
||||||
|
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
|
||||||
|
set value(T value) {
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(previous, next) {
|
||||||
|
final res = super.updateShouldNotify(previous, next);
|
||||||
|
if (res) {
|
||||||
|
onUpdate(next);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(T value) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin PageMixin<T extends StatefulWidget> on State<T> {
|
||||||
|
void onPageShow() {
|
||||||
|
initPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
initPageState() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final commonScaffoldState = context.commonScaffoldState;
|
||||||
|
commonScaffoldState?.actions = actions;
|
||||||
|
commonScaffoldState?.floatingActionButton = floatingActionButton;
|
||||||
|
commonScaffoldState?.onSearch = onSearch;
|
||||||
|
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPageHidden() {}
|
||||||
|
|
||||||
|
List<Widget> get actions => [];
|
||||||
|
|
||||||
|
Widget? get floatingActionButton => null;
|
||||||
|
|
||||||
|
Function(String)? get onSearch => null;
|
||||||
|
|
||||||
|
Function(List<String>)? get onKeywordsUpdate => null;
|
||||||
|
}
|
||||||
@@ -6,55 +6,81 @@ import 'package:flutter/material.dart';
|
|||||||
class Navigation {
|
class Navigation {
|
||||||
static Navigation? _instance;
|
static Navigation? _instance;
|
||||||
|
|
||||||
getItems({
|
List<NavigationItem> getItems({
|
||||||
bool openLogs = false,
|
bool openLogs = false,
|
||||||
bool hasProxies = false,
|
bool hasProxies = false,
|
||||||
}) {
|
}) {
|
||||||
return [
|
return [
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.space_dashboard),
|
icon: Icon(Icons.space_dashboard),
|
||||||
label: "dashboard",
|
label: PageLabel.dashboard,
|
||||||
fragment: DashboardFragment(),
|
fragment: DashboardFragment(
|
||||||
|
key: GlobalObjectKey(PageLabel.dashboard),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
NavigationItem(
|
NavigationItem(
|
||||||
icon: const Icon(Icons.rocket),
|
icon: const Icon(Icons.article),
|
||||||
label: "proxies",
|
label: PageLabel.proxies,
|
||||||
fragment: const ProxiesFragment(),
|
fragment: const ProxiesFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.proxies,
|
||||||
|
),
|
||||||
|
),
|
||||||
modes: hasProxies
|
modes: hasProxies
|
||||||
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
|
? [NavigationItemMode.mobile, NavigationItemMode.desktop]
|
||||||
: [],
|
: [],
|
||||||
),
|
),
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.folder),
|
icon: Icon(Icons.folder),
|
||||||
label: "profiles",
|
label: PageLabel.profiles,
|
||||||
fragment: ProfilesFragment(),
|
fragment: ProfilesFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.profiles,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.view_timeline),
|
icon: Icon(Icons.view_timeline),
|
||||||
label: "requests",
|
label: PageLabel.requests,
|
||||||
fragment: RequestsFragment(),
|
fragment: RequestsFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.requests,
|
||||||
|
),
|
||||||
|
),
|
||||||
description: "requestsDesc",
|
description: "requestsDesc",
|
||||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||||
),
|
),
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.ballot),
|
icon: Icon(Icons.ballot),
|
||||||
label: "connections",
|
label: PageLabel.connections,
|
||||||
fragment: ConnectionsFragment(),
|
fragment: ConnectionsFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.connections,
|
||||||
|
),
|
||||||
|
),
|
||||||
description: "connectionsDesc",
|
description: "connectionsDesc",
|
||||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
||||||
),
|
),
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.storage),
|
icon: Icon(Icons.storage),
|
||||||
label: "resources",
|
label: PageLabel.resources,
|
||||||
description: "resourcesDesc",
|
description: "resourcesDesc",
|
||||||
keep: false,
|
keep: false,
|
||||||
fragment: Resources(),
|
fragment: Resources(
|
||||||
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.resources,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
modes: [NavigationItemMode.more],
|
||||||
),
|
),
|
||||||
NavigationItem(
|
NavigationItem(
|
||||||
icon: const Icon(Icons.adb),
|
icon: const Icon(Icons.adb),
|
||||||
label: "logs",
|
label: PageLabel.logs,
|
||||||
fragment: const LogsFragment(),
|
fragment: const LogsFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.logs,
|
||||||
|
),
|
||||||
|
),
|
||||||
description: "logsDesc",
|
description: "logsDesc",
|
||||||
modes: openLogs
|
modes: openLogs
|
||||||
? [NavigationItemMode.desktop, NavigationItemMode.more]
|
? [NavigationItemMode.desktop, NavigationItemMode.more]
|
||||||
@@ -62,8 +88,12 @@ class Navigation {
|
|||||||
),
|
),
|
||||||
const NavigationItem(
|
const NavigationItem(
|
||||||
icon: Icon(Icons.construction),
|
icon: Icon(Icons.construction),
|
||||||
label: "tools",
|
label: PageLabel.tools,
|
||||||
fragment: ToolsFragment(),
|
fragment: ToolsFragment(
|
||||||
|
key: GlobalObjectKey(
|
||||||
|
PageLabel.tools,
|
||||||
|
),
|
||||||
|
),
|
||||||
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
|
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image/image.dart' as img;
|
|
||||||
import 'package:lpinyin/lpinyin.dart';
|
import 'package:lpinyin/lpinyin.dart';
|
||||||
import 'package:zxing2/qrcode.dart';
|
|
||||||
|
|
||||||
class Other {
|
class Other {
|
||||||
Color? getDelayColor(int? delay) {
|
Color? getDelayColor(int? delay) {
|
||||||
@@ -34,6 +30,26 @@ class Other {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String generateRandomString({int minLength = 10, int maxLength = 100}) {
|
||||||
|
const latinChars =
|
||||||
|
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
final random = Random();
|
||||||
|
|
||||||
|
int length = minLength + random.nextInt(maxLength - minLength + 1);
|
||||||
|
|
||||||
|
String result = '';
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
if (random.nextBool()) {
|
||||||
|
result +=
|
||||||
|
String.fromCharCode(0x4E00 + random.nextInt(0x9FA5 - 0x4E00 + 1));
|
||||||
|
} else {
|
||||||
|
result += latinChars[random.nextInt(latinChars.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
String get uuidV4 {
|
String get uuidV4 {
|
||||||
final Random random = Random();
|
final Random random = Random();
|
||||||
final bytes = List.generate(16, (_) => random.nextInt(256));
|
final bytes = List.generate(16, (_) => random.nextInt(256));
|
||||||
@@ -165,30 +181,6 @@ class Other {
|
|||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> parseQRCode(Uint8List? bytes) {
|
|
||||||
return Isolate.run<String?>(() {
|
|
||||||
if (bytes == null) return null;
|
|
||||||
img.Image? image = img.decodeImage(bytes);
|
|
||||||
LuminanceSource source = RGBLuminanceSource(
|
|
||||||
image!.width,
|
|
||||||
image.height,
|
|
||||||
image
|
|
||||||
.convert(numChannels: 4)
|
|
||||||
.getBytes(order: img.ChannelOrder.abgr)
|
|
||||||
.buffer
|
|
||||||
.asInt32List(),
|
|
||||||
);
|
|
||||||
final bitmap = BinaryBitmap(GlobalHistogramBinarizer(source));
|
|
||||||
final reader = QRCodeReader();
|
|
||||||
try {
|
|
||||||
final result = reader.decode(bitmap);
|
|
||||||
return result.text;
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
String? getFileNameForDisposition(String? disposition) {
|
String? getFileNameForDisposition(String? disposition) {
|
||||||
if (disposition == null) return null;
|
if (disposition == null) return null;
|
||||||
final parseValue = HeaderValue.parse(disposition);
|
final parseValue = HeaderValue.parse(disposition);
|
||||||
|
|||||||
@@ -48,35 +48,40 @@ class AppPath {
|
|||||||
return join(executableDirPath, "$appHelperService$executableExtension");
|
return join(executableDirPath, "$appHelperService$executableExtension");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getDownloadDirPath() async {
|
Future<String> get downloadDirPath async {
|
||||||
final directory = await downloadDir.future;
|
final directory = await downloadDir.future;
|
||||||
return directory.path;
|
return directory.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getHomeDirPath() async {
|
Future<String> get homeDirPath async {
|
||||||
final directory = await dataDir.future;
|
final directory = await dataDir.future;
|
||||||
return directory.path;
|
return directory.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getLockFilePath() async {
|
Future<String> get lockFilePath async {
|
||||||
final directory = await dataDir.future;
|
final directory = await dataDir.future;
|
||||||
return join(directory.path, "FlClash.lock");
|
return join(directory.path, "FlClash.lock");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getProfilesPath() async {
|
Future<String> get sharedPreferencesPath async {
|
||||||
|
final directory = await dataDir.future;
|
||||||
|
return join(directory.path, "shared_preferences.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> get profilesPath async {
|
||||||
final directory = await dataDir.future;
|
final directory = await dataDir.future;
|
||||||
return join(directory.path, profilesDirectoryName);
|
return join(directory.path, profilesDirectoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getProfilePath(String? id) async {
|
Future<String?> getProfilePath(String? id) async {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
final directory = await getProfilesPath();
|
final directory = await profilesPath;
|
||||||
return join(directory, "$id.yaml");
|
return join(directory, "$id.yaml");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getProvidersPath(String? id) async {
|
Future<String?> getProvidersPath(String? id) async {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
final directory = await getProfilesPath();
|
final directory = await profilesPath;
|
||||||
return join(directory, "providers", id);
|
return join(directory, "providers", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import 'dart:typed_data';
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
|
|
||||||
class Picker {
|
class Picker {
|
||||||
Future<PlatformFile?> pickerFile() async {
|
Future<PlatformFile?> pickerFile() async {
|
||||||
final filePickerResult = await FilePicker.platform.pickFiles(
|
final filePickerResult = await FilePicker.platform.pickFiles(
|
||||||
withData: true,
|
withData: true,
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
initialDirectory: await appPath.getDownloadDirPath(),
|
initialDirectory: await appPath.downloadDirPath,
|
||||||
);
|
);
|
||||||
return filePickerResult?.files.first;
|
return filePickerResult?.files.first;
|
||||||
}
|
}
|
||||||
@@ -18,7 +19,7 @@ class Picker {
|
|||||||
Future<String?> saveFile(String fileName, Uint8List bytes) async {
|
Future<String?> saveFile(String fileName, Uint8List bytes) async {
|
||||||
final path = await FilePicker.platform.saveFile(
|
final path = await FilePicker.platform.saveFile(
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
initialDirectory: await appPath.getDownloadDirPath(),
|
initialDirectory: await appPath.downloadDirPath,
|
||||||
bytes: Platform.isAndroid ? bytes : null,
|
bytes: Platform.isAndroid ? bytes : null,
|
||||||
);
|
);
|
||||||
if (!Platform.isAndroid && path != null) {
|
if (!Platform.isAndroid && path != null) {
|
||||||
@@ -30,9 +31,14 @@ class Picker {
|
|||||||
|
|
||||||
Future<String?> pickerConfigQRCode() async {
|
Future<String?> pickerConfigQRCode() async {
|
||||||
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
|
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
|
||||||
final bytes = await xFile?.readAsBytes();
|
if (xFile == null) {
|
||||||
if (bytes == null) return null;
|
return null;
|
||||||
final result = await other.parseQRCode(bytes);
|
}
|
||||||
|
final controller = MobileScannerController();
|
||||||
|
final capture = await controller.analyzeImage(xFile.path, formats: [
|
||||||
|
BarcodeFormat.qrCode,
|
||||||
|
]);
|
||||||
|
final result = capture?.barcodes.first.rawValue;
|
||||||
if (result == null || !result.isUrl) {
|
if (result == null || !result.isUrl) {
|
||||||
throw appLocalizations.pleaseUploadValidQrcode;
|
throw appLocalizations.pleaseUploadValidQrcode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../models/models.dart';
|
|
||||||
import 'constant.dart';
|
import 'constant.dart';
|
||||||
|
|
||||||
class Preferences {
|
class Preferences {
|
||||||
static Preferences? _instance;
|
static Preferences? _instance;
|
||||||
Completer<SharedPreferences> sharedPreferencesCompleter = Completer();
|
Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
|
||||||
|
|
||||||
|
Future<bool> get isInit async =>
|
||||||
|
await sharedPreferencesCompleter.future != null;
|
||||||
|
|
||||||
Preferences._internal() {
|
Preferences._internal() {
|
||||||
SharedPreferences.getInstance()
|
SharedPreferences.getInstance()
|
||||||
.then((value) => sharedPreferencesCompleter.complete(value));
|
.then((value) => sharedPreferencesCompleter.complete(value))
|
||||||
|
.onError((_, __) => sharedPreferencesCompleter.complete(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Preferences() {
|
factory Preferences() {
|
||||||
@@ -23,50 +26,38 @@ class Preferences {
|
|||||||
|
|
||||||
Future<ClashConfig?> getClashConfig() async {
|
Future<ClashConfig?> getClashConfig() async {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await sharedPreferencesCompleter.future;
|
||||||
final clashConfigString = preferences.getString(clashConfigKey);
|
final clashConfigString = preferences?.getString(clashConfigKey);
|
||||||
if (clashConfigString == null) return null;
|
if (clashConfigString == null) return null;
|
||||||
final clashConfigMap = json.decode(clashConfigString);
|
final clashConfigMap = json.decode(clashConfigString);
|
||||||
try {
|
return ClashConfig.fromJson(clashConfigMap);
|
||||||
return ClashConfig.fromJson(clashConfigMap);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint(e.toString());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
|
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
|
||||||
return preferences.setString(
|
|
||||||
clashConfigKey,
|
|
||||||
json.encode(clashConfig),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Config?> getConfig() async {
|
Future<Config?> getConfig() async {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await sharedPreferencesCompleter.future;
|
||||||
final configString = preferences.getString(configKey);
|
final configString = preferences?.getString(configKey);
|
||||||
if (configString == null) return null;
|
if (configString == null) return null;
|
||||||
final configMap = json.decode(configString);
|
final configMap = json.decode(configString);
|
||||||
try {
|
return Config.compatibleFromJson(configMap);
|
||||||
return Config.fromJson(configMap);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint(e.toString());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> saveConfig(Config config) async {
|
Future<bool> saveConfig(Config config) async {
|
||||||
final preferences = await sharedPreferencesCompleter.future;
|
final preferences = await sharedPreferencesCompleter.future;
|
||||||
return preferences.setString(
|
return await preferences?.setString(
|
||||||
configKey,
|
configKey,
|
||||||
json.encode(config),
|
json.encode(config),
|
||||||
);
|
) ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearClashConfig() async {
|
||||||
|
final preferences = await sharedPreferencesCompleter.future;
|
||||||
|
preferences?.remove(clashConfigKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPreferences() async {
|
clearPreferences() async {
|
||||||
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
final sharedPreferencesIns = await sharedPreferencesCompleter.future;
|
||||||
sharedPreferencesIns.clear();
|
sharedPreferencesIns?.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final preferences = Preferences();
|
final preferences = Preferences();
|
||||||
|
|||||||
31
lib/common/print.dart
Normal file
31
lib/common/print.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class CommonPrint {
|
||||||
|
static CommonPrint? _instance;
|
||||||
|
|
||||||
|
CommonPrint._internal();
|
||||||
|
|
||||||
|
factory CommonPrint() {
|
||||||
|
_instance ??= CommonPrint._internal();
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(String? text) {
|
||||||
|
final payload = "[FlClash] $text";
|
||||||
|
debugPrint(payload);
|
||||||
|
if (globalState.isService) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
globalState.appController.addLog(
|
||||||
|
Log(
|
||||||
|
logLevel: LogLevel.info,
|
||||||
|
payload: payload,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final commonPrint = CommonPrint();
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
class Render {
|
class Render {
|
||||||
@@ -23,14 +23,14 @@ class Render {
|
|||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
debouncer.call(
|
debouncer.call(
|
||||||
"render_pause",
|
DebounceTag.renderPause,
|
||||||
_pause,
|
_pause,
|
||||||
duration: Duration(seconds: 5),
|
duration: Duration(seconds: 15),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
resume() {
|
resume() {
|
||||||
debouncer.cancel("render_pause");
|
debouncer.cancel(DebounceTag.renderPause);
|
||||||
_resume();
|
_resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ class Render {
|
|||||||
_drawFrame = _dispatcher.onDrawFrame;
|
_drawFrame = _dispatcher.onDrawFrame;
|
||||||
_dispatcher.onBeginFrame = null;
|
_dispatcher.onBeginFrame = null;
|
||||||
_dispatcher.onDrawFrame = null;
|
_dispatcher.onDrawFrame = null;
|
||||||
debugPrint("[App] pause");
|
commonPrint.log("pause");
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resume() {
|
void _resume() {
|
||||||
@@ -49,7 +49,8 @@ class Render {
|
|||||||
_isPaused = false;
|
_isPaused = false;
|
||||||
_dispatcher.onBeginFrame = _beginFrame;
|
_dispatcher.onBeginFrame = _beginFrame;
|
||||||
_dispatcher.onDrawFrame = _drawFrame;
|
_dispatcher.onDrawFrame = _drawFrame;
|
||||||
debugPrint("[App] resume");
|
_dispatcher.scheduleFrame();
|
||||||
|
commonPrint.log("resume");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
@@ -14,11 +13,10 @@ class Request {
|
|||||||
String? userAgent;
|
String? userAgent;
|
||||||
|
|
||||||
Request() {
|
Request() {
|
||||||
_dio = Dio();
|
_dio = Dio(
|
||||||
_dio.interceptors.add(
|
BaseOptions(
|
||||||
InterceptorsWrapper(
|
headers: {
|
||||||
onRequest: (options, handler) {
|
"User-Agent": browserUa,
|
||||||
return handler.next(options); // 继续请求
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -30,7 +28,7 @@ class Request {
|
|||||||
url,
|
url,
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": globalState.appController.clashConfig.globalUa
|
"User-Agent": globalState.ua,
|
||||||
},
|
},
|
||||||
responseType: ResponseType.bytes,
|
responseType: ResponseType.bytes,
|
||||||
),
|
),
|
||||||
@@ -71,31 +69,32 @@ class Request {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<String> _ipInfoSources = [
|
final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
|
||||||
"https://ipwho.is/?fields=ip&output=csv",
|
"https://ipwho.is/": IpInfo.fromIpwhoIsJson,
|
||||||
"https://ipinfo.io/ip",
|
"https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
|
||||||
"https://ifconfig.me/ip/",
|
"https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
|
||||||
];
|
"https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
|
||||||
|
};
|
||||||
|
|
||||||
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
|
||||||
for (final source in _ipInfoSources) {
|
for (final source in _ipInfoSources.entries) {
|
||||||
try {
|
try {
|
||||||
final response = await _dio
|
final response = await _dio.get<Map<String, dynamic>>(
|
||||||
.get<String>(
|
source.key,
|
||||||
source,
|
cancelToken: cancelToken,
|
||||||
cancelToken: cancelToken,
|
options: Options(
|
||||||
)
|
responseType: ResponseType.json,
|
||||||
.timeout(httpTimeoutDuration);
|
),
|
||||||
|
);
|
||||||
if (response.statusCode != 200 || response.data == null) {
|
if (response.statusCode != 200 || response.data == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final ipInfo = await clashCore.getCountryCode(response.data!);
|
if (response.data == null) {
|
||||||
if (ipInfo == null && source != _ipInfoSources.last) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return ipInfo;
|
return source.value(response.data!);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("checkIp error ===> $e");
|
commonPrint.log("checkIp error ===> $e");
|
||||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||||
throw "cancelled";
|
throw "cancelled";
|
||||||
}
|
}
|
||||||
@@ -110,6 +109,9 @@ class Request {
|
|||||||
.get(
|
.get(
|
||||||
"http://$localhost:$helperPort/ping",
|
"http://$localhost:$helperPort/ping",
|
||||||
options: Options(
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
"User-Agent": browserUa,
|
||||||
|
},
|
||||||
responseType: ResponseType.plain,
|
responseType: ResponseType.plain,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
@@ -15,6 +16,8 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BaseScrollBehavior2 extends ScrollBehavior {}
|
||||||
|
|
||||||
class HiddenBarScrollBehavior extends BaseScrollBehavior {
|
class HiddenBarScrollBehavior extends BaseScrollBehavior {
|
||||||
@override
|
@override
|
||||||
Widget buildScrollbar(
|
Widget buildScrollbar(
|
||||||
@@ -40,3 +43,95 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NextClampingScrollPhysics extends ClampingScrollPhysics {
|
||||||
|
const NextClampingScrollPhysics({super.parent});
|
||||||
|
|
||||||
|
@override
|
||||||
|
NextClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||||
|
return NextClampingScrollPhysics(parent: buildParent(ancestor));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Simulation? createBallisticSimulation(
|
||||||
|
ScrollMetrics position, double velocity) {
|
||||||
|
final Tolerance tolerance = toleranceFor(position);
|
||||||
|
if (position.outOfRange) {
|
||||||
|
double? end;
|
||||||
|
if (position.pixels > position.maxScrollExtent) {
|
||||||
|
end = position.maxScrollExtent;
|
||||||
|
}
|
||||||
|
if (position.pixels < position.minScrollExtent) {
|
||||||
|
end = position.minScrollExtent;
|
||||||
|
}
|
||||||
|
assert(end != null);
|
||||||
|
return ScrollSpringSimulation(
|
||||||
|
spring,
|
||||||
|
end!,
|
||||||
|
end,
|
||||||
|
min(0.0, velocity),
|
||||||
|
tolerance: tolerance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (velocity.abs() < tolerance.velocity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (velocity < 0.0 && position.pixels <= position.minScrollExtent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ClampingScrollSimulation(
|
||||||
|
position: position.pixels,
|
||||||
|
velocity: velocity,
|
||||||
|
tolerance: tolerance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReverseScrollController extends ScrollController {
|
||||||
|
ReverseScrollController({
|
||||||
|
super.initialScrollOffset,
|
||||||
|
super.keepScrollOffset,
|
||||||
|
super.debugLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ScrollPosition createScrollPosition(
|
||||||
|
ScrollPhysics physics,
|
||||||
|
ScrollContext context,
|
||||||
|
ScrollPosition? oldPosition,
|
||||||
|
) {
|
||||||
|
return ReverseScrollPosition(
|
||||||
|
physics: physics,
|
||||||
|
context: context,
|
||||||
|
initialPixels: initialScrollOffset,
|
||||||
|
keepScrollOffset: keepScrollOffset,
|
||||||
|
oldPosition: oldPosition,
|
||||||
|
debugLabel: debugLabel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReverseScrollPosition extends ScrollPositionWithSingleContext {
|
||||||
|
ReverseScrollPosition({
|
||||||
|
required super.physics,
|
||||||
|
required super.context,
|
||||||
|
super.initialPixels = 0.0,
|
||||||
|
super.keepScrollOffset,
|
||||||
|
super.oldPosition,
|
||||||
|
super.debugLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool _isInit = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
|
||||||
|
if (!_isInit) {
|
||||||
|
correctPixels(maxScrollExtent);
|
||||||
|
_isInit = true;
|
||||||
|
}
|
||||||
|
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
0
lib/common/state.dart
Normal file
0
lib/common/state.dart
Normal 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!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -8,28 +8,24 @@ import 'package:archive/archive.dart';
|
|||||||
import 'package:fl_clash/clash/clash.dart';
|
import 'package:fl_clash/clash/clash.dart';
|
||||||
import 'package:fl_clash/common/archive.dart';
|
import 'package:fl_clash/common/archive.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'common/common.dart';
|
import 'common/common.dart';
|
||||||
import 'models/models.dart';
|
import 'models/models.dart';
|
||||||
|
|
||||||
class AppController {
|
class AppController {
|
||||||
final BuildContext context;
|
bool lastTunEnable = false;
|
||||||
late AppState appState;
|
int? lastProfileModified;
|
||||||
late AppFlowingState appFlowingState;
|
|
||||||
late Config config;
|
|
||||||
late ClashConfig clashConfig;
|
|
||||||
|
|
||||||
AppController(this.context) {
|
final BuildContext context;
|
||||||
appState = context.read<AppState>();
|
final WidgetRef _ref;
|
||||||
config = context.read<Config>();
|
|
||||||
clashConfig = context.read<ClashConfig>();
|
AppController(this.context, WidgetRef ref) : _ref = ref;
|
||||||
appFlowingState = context.read<AppFlowingState>();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateClashConfigDebounce() {
|
updateClashConfigDebounce() {
|
||||||
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
|
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
|
||||||
@@ -41,13 +37,13 @@ class AppController {
|
|||||||
|
|
||||||
addCheckIpNumDebounce() {
|
addCheckIpNumDebounce() {
|
||||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
debouncer.call(DebounceTag.addCheckIpNum, () {
|
||||||
appState.checkIpNum++;
|
_ref.read(checkIpNumProvider.notifier).add();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
applyProfileDebounce() {
|
applyProfileDebounce() {
|
||||||
debouncer.call(DebounceTag.addCheckIpNum, () {
|
debouncer.call(DebounceTag.addCheckIpNum, () {
|
||||||
applyProfile(isPrue: true);
|
applyProfile();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,11 +63,12 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restartCore() async {
|
restartCore() async {
|
||||||
await globalState.restartCore(
|
await clashService?.reStart();
|
||||||
appState: appState,
|
await initCore();
|
||||||
clashConfig: clashConfig,
|
|
||||||
config: config,
|
if (_ref.read(runTimeProvider.notifier).isStart) {
|
||||||
);
|
await globalState.handleStart();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(bool isStart) async {
|
updateStatus(bool isStart) async {
|
||||||
@@ -81,13 +78,12 @@ class AppController {
|
|||||||
updateTraffic,
|
updateTraffic,
|
||||||
]);
|
]);
|
||||||
final currentLastModified =
|
final currentLastModified =
|
||||||
await config.getCurrentProfile()?.profileLastModified;
|
await _ref.read(currentProfileProvider)?.profileLastModified;
|
||||||
if (currentLastModified == null ||
|
if (currentLastModified == null || lastProfileModified == null) {
|
||||||
globalState.lastProfileModified == null) {
|
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) {
|
if (currentLastModified <= (lastProfileModified ?? 0)) {
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -95,9 +91,10 @@ class AppController {
|
|||||||
} else {
|
} else {
|
||||||
await globalState.handleStop();
|
await globalState.handleStop();
|
||||||
await clashCore.resetTraffic();
|
await clashCore.resetTraffic();
|
||||||
appFlowingState.traffics = [];
|
_ref.read(trafficsProvider.notifier).clear();
|
||||||
appFlowingState.totalTraffic = Traffic();
|
_ref.read(totalTrafficProvider.notifier).value = Traffic();
|
||||||
appFlowingState.runTime = null;
|
_ref.read(runTimeProvider.notifier).value = null;
|
||||||
|
// tray.updateTrayTitle(null);
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,107 +104,214 @@ class AppController {
|
|||||||
if (startTime != null) {
|
if (startTime != null) {
|
||||||
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
final startTimeStamp = startTime.millisecondsSinceEpoch;
|
||||||
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
final nowTimeStamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
appFlowingState.runTime = nowTimeStamp - startTimeStamp;
|
_ref.read(runTimeProvider.notifier).value = nowTimeStamp - startTimeStamp;
|
||||||
} else {
|
} else {
|
||||||
appFlowingState.runTime = null;
|
_ref.read(runTimeProvider.notifier).value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTraffic() {
|
updateTraffic() async {
|
||||||
globalState.updateTraffic(
|
final traffic = await clashCore.getTraffic();
|
||||||
config: config,
|
_ref.read(trafficsProvider.notifier).addTraffic(traffic);
|
||||||
appFlowingState: appFlowingState,
|
_ref.read(totalTrafficProvider.notifier).value =
|
||||||
);
|
await clashCore.getTotalTraffic();
|
||||||
}
|
}
|
||||||
|
|
||||||
addProfile(Profile profile) async {
|
addProfile(Profile profile) async {
|
||||||
config.setProfile(profile);
|
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||||
if (config.currentProfileId != null) return;
|
if (_ref.read(currentProfileIdProvider) != null) return;
|
||||||
await changeProfile(profile.id);
|
_ref.read(currentProfileIdProvider.notifier).value = profile.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteProfile(String id) async {
|
deleteProfile(String id) async {
|
||||||
config.deleteProfileById(id);
|
_ref.read(profilesProvider.notifier).deleteProfileById(id);
|
||||||
clearEffect(id);
|
clearEffect(id);
|
||||||
if (config.currentProfileId == id) {
|
if (globalState.config.currentProfileId == id) {
|
||||||
if (config.profiles.isNotEmpty) {
|
final profiles = globalState.config.profiles;
|
||||||
final updateId = config.profiles.first.id;
|
final currentProfileId = _ref.read(currentProfileIdProvider.notifier);
|
||||||
changeProfile(updateId);
|
if (profiles.isNotEmpty) {
|
||||||
|
final updateId = profiles.first.id;
|
||||||
|
currentProfileId.value = updateId;
|
||||||
} else {
|
} else {
|
||||||
changeProfile(null);
|
currentProfileId.value = null;
|
||||||
updateStatus(false);
|
updateStatus(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProviders() async {
|
updateProviders() async {
|
||||||
await globalState.updateProviders(appState);
|
_ref.read(providersProvider.notifier).value =
|
||||||
|
await clashCore.getExternalProviders();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLocalIp() async {
|
updateLocalIp() async {
|
||||||
appFlowingState.localIp = null;
|
_ref.read(localIpProvider.notifier).value = null;
|
||||||
await Future.delayed(commonDuration);
|
await Future.delayed(commonDuration);
|
||||||
appFlowingState.localIp = await other.getLocalIpAddress();
|
_ref.read(localIpProvider.notifier).value = await other.getLocalIpAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateProfile(Profile profile) async {
|
Future<void> updateProfile(Profile profile) async {
|
||||||
final newProfile = await profile.update();
|
final newProfile = await profile.update();
|
||||||
config.setProfile(
|
_ref
|
||||||
newProfile.copyWith(isUpdating: false),
|
.read(profilesProvider.notifier)
|
||||||
);
|
.setProfile(newProfile.copyWith(isUpdating: false));
|
||||||
if (profile.id == config.currentProfile?.id) {
|
if (profile.id == _ref.read(currentProfileIdProvider)) {
|
||||||
applyProfileDebounce();
|
applyProfileDebounce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setProfile(Profile profile) {
|
||||||
|
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
setProfile(Profile profile) {
|
setProfile(Profile profile) {
|
||||||
config.setProfile(profile);
|
_setProfile(profile);
|
||||||
if (profile.id == config.currentProfile?.id) {
|
if (profile.id == _ref.read(currentProfileIdProvider)) {
|
||||||
applyProfileDebounce();
|
applyProfileDebounce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateClashConfig({bool isPatch = true}) async {
|
setProfiles(List<Profile> profiles) {
|
||||||
|
_ref.read(profilesProvider.notifier).value = profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
addLog(Log log) {
|
||||||
|
_ref.read(logsProvider).add(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOrAddHotKeyAction(HotKeyAction hotKeyAction) {
|
||||||
|
final hotKeyActions = _ref.read(hotKeyActionsProvider);
|
||||||
|
final index =
|
||||||
|
hotKeyActions.indexWhere((item) => item.action == hotKeyAction.action);
|
||||||
|
if (index == -1) {
|
||||||
|
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
|
||||||
|
..add(hotKeyAction);
|
||||||
|
} else {
|
||||||
|
_ref.read(hotKeyActionsProvider.notifier).value = List.from(hotKeyActions)
|
||||||
|
..[index] = hotKeyAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ref.read(hotKeyActionsProvider.notifier).value = index == -1
|
||||||
|
? (List.from(hotKeyActions)..add(hotKeyAction))
|
||||||
|
: (List.from(hotKeyActions)..[index] = hotKeyAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Group> getCurrentGroups() {
|
||||||
|
return _ref.read(currentGroupsStateProvider.select((state) => state.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
String getRealTestUrl(String? url) {
|
||||||
|
return _ref.read(getRealTestUrlProvider(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
int getProxiesColumns() {
|
||||||
|
return _ref.read(getProxiesColumnsProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSortNum() {
|
||||||
|
return _ref.read(sortNumProvider.notifier).add();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentGroupName() {
|
||||||
|
final currentGroupName = _ref.read(currentProfileProvider.select(
|
||||||
|
(state) => state?.currentGroupName,
|
||||||
|
));
|
||||||
|
return currentGroupName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRealProxyName(proxyName) {
|
||||||
|
return _ref.read(getRealTestUrlProvider(proxyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedProxyName(groupName) {
|
||||||
|
return _ref.read(getSelectedProxyNameProvider(groupName));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentGroupName(String groupName) {
|
||||||
|
final profile = _ref.read(currentProfileProvider);
|
||||||
|
if (profile == null || profile.currentGroupName == groupName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_setProfile(
|
||||||
|
profile.copyWith(currentGroupName: groupName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateClashConfig([bool? isPatch]) async {
|
||||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||||
if (commonScaffoldState?.mounted != true) return;
|
if (commonScaffoldState?.mounted != true) return;
|
||||||
await commonScaffoldState?.loadingRun(() async {
|
await commonScaffoldState?.loadingRun(() async {
|
||||||
await globalState.updateClashConfig(
|
await _updateClashConfig(
|
||||||
appState: appState,
|
isPatch,
|
||||||
clashConfig: clashConfig,
|
|
||||||
config: config,
|
|
||||||
isPatch: isPatch,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future applyProfile({bool isPrue = false}) async {
|
Future<void> _updateClashConfig([bool? isPatch]) async {
|
||||||
if (isPrue) {
|
final profile = _ref.watch(currentProfileProvider);
|
||||||
await globalState.applyProfile(
|
await _ref.read(currentProfileProvider)?.checkAndUpdate();
|
||||||
appState: appState,
|
final patchConfig = _ref.read(patchClashConfigProvider);
|
||||||
config: config,
|
final appSetting = _ref.read(appSettingProvider);
|
||||||
clashConfig: clashConfig,
|
bool enableTun = patchConfig.tun.enable;
|
||||||
);
|
if (enableTun != lastTunEnable &&
|
||||||
|
lastTunEnable == false &&
|
||||||
|
!Platform.isAndroid) {
|
||||||
|
final code = await system.authorizeCore();
|
||||||
|
switch (code) {
|
||||||
|
case AuthorizeCode.none:
|
||||||
|
break;
|
||||||
|
case AuthorizeCode.success:
|
||||||
|
lastTunEnable = enableTun;
|
||||||
|
await restartCore();
|
||||||
|
return;
|
||||||
|
case AuthorizeCode.error:
|
||||||
|
enableTun = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (appSetting.openLogs) {
|
||||||
|
clashCore.startLog();
|
||||||
|
} else {
|
||||||
|
clashCore.stopLog();
|
||||||
|
}
|
||||||
|
final res = await clashCore.updateConfig(
|
||||||
|
globalState.getUpdateConfigParams(isPatch),
|
||||||
|
);
|
||||||
|
if (res.isNotEmpty) throw res;
|
||||||
|
lastTunEnable = enableTun;
|
||||||
|
lastProfileModified = await profile?.profileLastModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _applyProfile() async {
|
||||||
|
await clashCore.requestGc();
|
||||||
|
await updateClashConfig();
|
||||||
|
await updateGroups();
|
||||||
|
await updateProviders();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future applyProfile({bool silence = false}) async {
|
||||||
|
if (silence) {
|
||||||
|
await _applyProfile();
|
||||||
} else {
|
} else {
|
||||||
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
|
||||||
if (commonScaffoldState?.mounted != true) return;
|
if (commonScaffoldState?.mounted != true) return;
|
||||||
await commonScaffoldState?.loadingRun(() async {
|
await commonScaffoldState?.loadingRun(() async {
|
||||||
await globalState.applyProfile(
|
await _applyProfile();
|
||||||
appState: appState,
|
|
||||||
config: config,
|
|
||||||
clashConfig: clashConfig,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
}
|
}
|
||||||
|
|
||||||
changeProfile(String? value) async {
|
handleChangeProfile() {
|
||||||
if (value == config.currentProfileId) return;
|
_ref.read(delayDataSourceProvider.notifier).value = {};
|
||||||
config.currentProfileId = value;
|
applyProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBrightness(Brightness brightness) {
|
||||||
|
_ref.read(appBrightnessProvider.notifier).value = brightness;
|
||||||
}
|
}
|
||||||
|
|
||||||
autoUpdateProfiles() async {
|
autoUpdateProfiles() async {
|
||||||
for (final profile in config.profiles) {
|
for (final profile in _ref.read(profilesProvider)) {
|
||||||
if (!profile.autoUpdate) continue;
|
if (!profile.autoUpdate) continue;
|
||||||
final isNotNeedUpdate = profile.lastUpdateDate
|
final isNotNeedUpdate = profile.lastUpdateDate
|
||||||
?.add(
|
?.add(
|
||||||
@@ -218,20 +322,29 @@ class AppController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
updateProfile(profile);
|
await updateProfile(profile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appFlowingState.addLog(
|
_ref.read(logsProvider.notifier).addLog(
|
||||||
Log(
|
Log(
|
||||||
logLevel: LogLevel.info,
|
logLevel: LogLevel.info,
|
||||||
payload: e.toString(),
|
payload: e.toString(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateGroups() async {
|
||||||
|
_ref.read(groupsProvider.notifier).value = await retry(
|
||||||
|
task: () async {
|
||||||
|
return await clashCore.getProxiesGroups();
|
||||||
|
},
|
||||||
|
retryIf: (res) => res.isEmpty,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
updateProfiles() async {
|
updateProfiles() async {
|
||||||
for (final profile in config.profiles) {
|
for (final profile in _ref.read(profilesProvider)) {
|
||||||
if (profile.type == ProfileType.file) {
|
if (profile.type == ProfileType.file) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -239,34 +352,33 @@ class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateGroups() async {
|
updateSystemColorSchemes(ColorSchemes colorSchemes) {
|
||||||
await globalState.updateGroups(appState);
|
_ref.read(appSchemesProvider.notifier).value = colorSchemes;
|
||||||
}
|
|
||||||
|
|
||||||
updateSystemColorSchemes(SystemColorSchemes systemColorSchemes) {
|
|
||||||
appState.systemColorSchemes = systemColorSchemes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
savePreferences() async {
|
savePreferences() async {
|
||||||
debugPrint("[APP] savePreferences");
|
commonPrint.log("save preferences");
|
||||||
await preferences.saveConfig(config);
|
await preferences.saveConfig(globalState.config);
|
||||||
await preferences.saveClashConfig(clashConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeProxy({
|
changeProxy({
|
||||||
required String groupName,
|
required String groupName,
|
||||||
required String proxyName,
|
required String proxyName,
|
||||||
}) async {
|
}) async {
|
||||||
await globalState.changeProxy(
|
await clashCore.changeProxy(
|
||||||
config: config,
|
ChangeProxyParams(
|
||||||
groupName: groupName,
|
groupName: groupName,
|
||||||
proxyName: proxyName,
|
proxyName: proxyName,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
if (_ref.read(appSettingProvider).closeConnections) {
|
||||||
|
clashCore.closeConnections();
|
||||||
|
}
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBackOrExit() async {
|
handleBackOrExit() async {
|
||||||
if (config.appSetting.minimizeOnExit) {
|
if (_ref.read(appSettingProvider).minimizeOnExit) {
|
||||||
if (system.isDesktop) {
|
if (system.isDesktop) {
|
||||||
await savePreferencesDebounce();
|
await savePreferencesDebounce();
|
||||||
}
|
}
|
||||||
@@ -289,7 +401,7 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
autoCheckUpdate() async {
|
autoCheckUpdate() async {
|
||||||
if (!config.appSetting.autoCheckUpdate) return;
|
if (!_ref.read(appSettingProvider).autoCheckUpdate) return;
|
||||||
final res = await request.checkForUpdate();
|
final res = await request.checkForUpdate();
|
||||||
checkUpdateResultHandle(data: res);
|
checkUpdateResultHandle(data: res);
|
||||||
}
|
}
|
||||||
@@ -338,35 +450,61 @@ class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init() async {
|
_handlePreference() async {
|
||||||
final isDisclaimerAccepted = await handlerDisclaimer();
|
if (await preferences.isInit) {
|
||||||
if (!isDisclaimerAccepted) {
|
return;
|
||||||
handleExit();
|
|
||||||
}
|
}
|
||||||
await globalState.initCore(
|
final res = await globalState.showMessage(
|
||||||
appState: appState,
|
title: appLocalizations.tip,
|
||||||
clashConfig: clashConfig,
|
message: TextSpan(text: appLocalizations.cacheCorrupt),
|
||||||
config: config,
|
|
||||||
);
|
);
|
||||||
|
if (res == true) {
|
||||||
|
final file = File(await appPath.sharedPreferencesPath);
|
||||||
|
final isExists = await file.exists();
|
||||||
|
if (isExists) {
|
||||||
|
await file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await handleExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initCore() async {
|
||||||
|
final isInit = await clashCore.isInit;
|
||||||
|
if (!isInit) {
|
||||||
|
await clashCore.setState(
|
||||||
|
globalState.getCoreState(),
|
||||||
|
);
|
||||||
|
await clashCore.init();
|
||||||
|
}
|
||||||
|
await applyProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() async {
|
||||||
|
await _handlePreference();
|
||||||
|
await _handlerDisclaimer();
|
||||||
|
await initCore();
|
||||||
await _initStatus();
|
await _initStatus();
|
||||||
|
updateTray(true);
|
||||||
autoLaunch?.updateStatus(
|
autoLaunch?.updateStatus(
|
||||||
config.appSetting.autoLaunch,
|
_ref.read(appSettingProvider).autoLaunch,
|
||||||
);
|
);
|
||||||
autoUpdateProfiles();
|
autoUpdateProfiles();
|
||||||
autoCheckUpdate();
|
autoCheckUpdate();
|
||||||
if (!config.appSetting.silentLaunch) {
|
if (!_ref.read(appSettingProvider).silentLaunch) {
|
||||||
window?.show();
|
window?.show();
|
||||||
} else {
|
} else {
|
||||||
window?.hide();
|
window?.hide();
|
||||||
}
|
}
|
||||||
|
_ref.read(initProvider.notifier).value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_initStatus() async {
|
_initStatus() async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
await globalState.updateStartTime();
|
await globalState.updateStartTime();
|
||||||
}
|
}
|
||||||
final status =
|
final status = globalState.isStart == true
|
||||||
globalState.isStart == true ? true : config.appSetting.autoRun;
|
? true
|
||||||
|
: _ref.read(appSettingProvider).autoRun;
|
||||||
|
|
||||||
await updateStatus(status);
|
await updateStatus(status);
|
||||||
if (!status) {
|
if (!status) {
|
||||||
@@ -375,18 +513,23 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDelay(Delay delay) {
|
setDelay(Delay delay) {
|
||||||
appState.setDelay(delay);
|
_ref.read(delayDataSourceProvider.notifier).setDelay(delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
toPage(
|
toPage(
|
||||||
int index, {
|
int index, {
|
||||||
bool hasAnimate = false,
|
bool hasAnimate = false,
|
||||||
}) {
|
}) {
|
||||||
if (index > appState.currentNavigationItems.length - 1) {
|
final navigations = _ref.read(currentNavigationsStateProvider).value;
|
||||||
|
if (index > navigations.length - 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
appState.currentLabel = appState.currentNavigationItems[index].label;
|
_ref.read(currentPageLabelProvider.notifier).value =
|
||||||
if ((config.appSetting.isAnimateToPage || hasAnimate)) {
|
navigations[index].label;
|
||||||
|
final isAnimateToPage = _ref.read(appSettingProvider).isAnimateToPage;
|
||||||
|
final isMobile =
|
||||||
|
_ref.read(viewWidthProvider.notifier).viewMode == ViewMode.mobile;
|
||||||
|
if (isAnimateToPage && isMobile || hasAnimate) {
|
||||||
globalState.pageController?.animateToPage(
|
globalState.pageController?.animateToPage(
|
||||||
index,
|
index,
|
||||||
duration: kTabScrollDuration,
|
duration: kTabScrollDuration,
|
||||||
@@ -398,9 +541,9 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toProfiles() {
|
toProfiles() {
|
||||||
final index = appState.currentNavigationItems.indexWhere(
|
final index = _ref.read(currentNavigationsStateProvider).value.indexWhere(
|
||||||
(element) => element.label == "profiles",
|
(element) => element.label == PageLabel.profiles,
|
||||||
);
|
);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
toPage(index);
|
toPage(index);
|
||||||
}
|
}
|
||||||
@@ -460,9 +603,9 @@ class AppController {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
config.appSetting = config.appSetting.copyWith(
|
_ref.read(appSettingProvider.notifier).updateState(
|
||||||
disclaimerAccepted: true,
|
(state) => state.copyWith(disclaimerAccepted: true),
|
||||||
);
|
);
|
||||||
Navigator.of(context).pop<bool>(true);
|
Navigator.of(context).pop<bool>(true);
|
||||||
},
|
},
|
||||||
child: Text(appLocalizations.agree),
|
child: Text(appLocalizations.agree),
|
||||||
@@ -473,11 +616,15 @@ class AppController {
|
|||||||
false;
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> handlerDisclaimer() async {
|
_handlerDisclaimer() async {
|
||||||
if (config.appSetting.disclaimerAccepted) {
|
if (_ref.read(appSettingProvider).disclaimerAccepted) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
return showDisclaimer();
|
final isDisclaimerAccepted = await showDisclaimer();
|
||||||
|
if (!isDisclaimerAccepted) {
|
||||||
|
await handleExit();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addProfileFormURL(String url) async {
|
addProfileFormURL(String url) async {
|
||||||
@@ -531,20 +678,12 @@ class AppController {
|
|||||||
|
|
||||||
updateViewWidth(double width) {
|
updateViewWidth(double width) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
appState.viewWidth = width;
|
_ref.read(viewWidthProvider.notifier).value = width;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
int? getDelay(String proxyName, [String? url]) {
|
setProvider(ExternalProvider? provider) {
|
||||||
final currentDelayMap = appState.delayMap[getRealTestUrl(url)];
|
_ref.read(providersProvider.notifier).setProvider(provider);
|
||||||
return currentDelayMap?[appState.getRealProxyName(proxyName)];
|
|
||||||
}
|
|
||||||
|
|
||||||
String getRealTestUrl(String? url) {
|
|
||||||
if (url == null || url.isEmpty) {
|
|
||||||
return config.appSetting.testUrl;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
List<Proxy> _sortOfName(List<Proxy> proxies) {
|
||||||
@@ -557,12 +696,17 @@ class AppController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Proxy> _sortOfDelay(String url, List<Proxy> proxies) {
|
List<Proxy> _sortOfDelay({
|
||||||
|
required List<Proxy> proxies,
|
||||||
|
String? testUrl,
|
||||||
|
}) {
|
||||||
return List.of(proxies)
|
return List.of(proxies)
|
||||||
..sort(
|
..sort(
|
||||||
(a, b) {
|
(a, b) {
|
||||||
final aDelay = getDelay(a.name, url);
|
final aDelay =
|
||||||
final bDelay = getDelay(b.name, url);
|
_ref.read(getDelayProvider(proxyName: a.name, testUrl: testUrl));
|
||||||
|
final bDelay =
|
||||||
|
_ref.read(getDelayProvider(proxyName: b.name, testUrl: testUrl));
|
||||||
if (aDelay == null && bDelay == null) {
|
if (aDelay == null && bDelay == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -578,20 +722,16 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
|
List<Proxy> getSortProxies(List<Proxy> proxies, [String? url]) {
|
||||||
return switch (config.proxiesStyle.sortType) {
|
return switch (_ref.read(proxiesStyleSettingProvider).sortType) {
|
||||||
ProxiesSortType.none => proxies,
|
ProxiesSortType.none => proxies,
|
||||||
ProxiesSortType.delay => _sortOfDelay(getRealTestUrl(url), proxies),
|
ProxiesSortType.delay => _sortOfDelay(
|
||||||
|
proxies: proxies,
|
||||||
|
testUrl: url,
|
||||||
|
),
|
||||||
ProxiesSortType.name => _sortOfName(proxies),
|
ProxiesSortType.name => _sortOfName(proxies),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
String getCurrentSelectedName(String groupName) {
|
|
||||||
final group = appState.getGroupWithName(groupName);
|
|
||||||
return group?.getCurrentSelectedName(
|
|
||||||
config.currentSelectedMap[groupName] ?? '') ??
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
|
|
||||||
clearEffect(String profileId) async {
|
clearEffect(String profileId) async {
|
||||||
final profilePath = await appPath.getProfilePath(profileId);
|
final profilePath = await appPath.getProfilePath(profileId);
|
||||||
final providersPath = await appPath.getProvidersPath(profileId);
|
final providersPath = await appPath.getProvidersPath(profileId);
|
||||||
@@ -605,38 +745,67 @@ class AppController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isMobileView {
|
|
||||||
return appState.viewMode == ViewMode.mobile;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTun() {
|
updateTun() {
|
||||||
clashConfig.tun = clashConfig.tun.copyWith(
|
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
enable: !clashConfig.tun.enable,
|
(state) => state.copyWith.tun(enable: !state.tun.enable),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSystemProxy() {
|
updateSystemProxy() {
|
||||||
config.networkProps = config.networkProps.copyWith(
|
_ref.read(networkSettingProvider.notifier).updateState(
|
||||||
systemProxy: !config.networkProps.systemProxy,
|
(state) => state.copyWith(
|
||||||
);
|
systemProxy: state.systemProxy,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStart() {
|
updateStart() {
|
||||||
updateStatus(!appFlowingState.isStart);
|
updateStatus(_ref.read(runTimeProvider.notifier).isStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentSelectedMap(String groupName, String proxyName) {
|
||||||
|
final currentProfile = _ref.read(currentProfileProvider);
|
||||||
|
if (currentProfile != null &&
|
||||||
|
currentProfile.selectedMap[groupName] != proxyName) {
|
||||||
|
final SelectedMap selectedMap = Map.from(
|
||||||
|
currentProfile.selectedMap,
|
||||||
|
)..[groupName] = proxyName;
|
||||||
|
_ref.read(profilesProvider.notifier).setProfile(
|
||||||
|
currentProfile.copyWith(
|
||||||
|
selectedMap: selectedMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentUnfoldSet(Set<String> value) {
|
||||||
|
final currentProfile = _ref.read(currentProfileProvider);
|
||||||
|
if (currentProfile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ref.read(profilesProvider.notifier).setProfile(
|
||||||
|
currentProfile.copyWith(
|
||||||
|
unfoldSet: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeMode(Mode mode) {
|
changeMode(Mode mode) {
|
||||||
clashConfig.mode = mode;
|
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(mode: mode),
|
||||||
|
);
|
||||||
if (mode == Mode.global) {
|
if (mode == Mode.global) {
|
||||||
config.updateCurrentGroupName(GroupName.GLOBAL.name);
|
updateCurrentGroupName(GroupName.GLOBAL.name);
|
||||||
}
|
}
|
||||||
addCheckIpNumDebounce();
|
addCheckIpNumDebounce();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAutoLaunch() {
|
updateAutoLaunch() {
|
||||||
config.appSetting = config.appSetting.copyWith(
|
_ref.read(appSettingProvider.notifier).updateState(
|
||||||
autoLaunch: !config.appSetting.autoLaunch,
|
(state) => state.copyWith(
|
||||||
);
|
autoLaunch: !state.autoLaunch,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVisible() async {
|
updateVisible() async {
|
||||||
@@ -649,18 +818,24 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateMode() {
|
updateMode() {
|
||||||
final index = Mode.values.indexWhere((item) => item == clashConfig.mode);
|
_ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
if (index == -1) {
|
(state) {
|
||||||
return;
|
final index = Mode.values.indexWhere((item) => item == state.mode);
|
||||||
}
|
if (index == -1) {
|
||||||
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
return null;
|
||||||
clashConfig.mode = Mode.values[nextIndex];
|
}
|
||||||
|
final nextIndex = index + 1 > Mode.values.length - 1 ? 0 : index + 1;
|
||||||
|
return state.copyWith(
|
||||||
|
mode: Mode.values[nextIndex],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> exportLogs() async {
|
Future<bool> exportLogs() async {
|
||||||
final logsRaw = appFlowingState.logs.map(
|
final logsRaw = _ref.read(logsProvider).list.map(
|
||||||
(item) => item.toString(),
|
(item) => item.toString(),
|
||||||
);
|
);
|
||||||
final data = await Isolate.run<List<int>>(() async {
|
final data = await Isolate.run<List<int>>(() async {
|
||||||
final logsRawString = logsRaw.join("\n");
|
final logsRawString = logsRaw.join("\n");
|
||||||
return utf8.encode(logsRawString);
|
return utf8.encode(logsRawString);
|
||||||
@@ -673,14 +848,12 @@ class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<int>> backupData() async {
|
Future<List<int>> backupData() async {
|
||||||
final homeDirPath = await appPath.getHomeDirPath();
|
final homeDirPath = await appPath.homeDirPath;
|
||||||
final profilesPath = await appPath.getProfilesPath();
|
final profilesPath = await appPath.profilesPath;
|
||||||
final configJson = config.toJson();
|
final configJson = globalState.config.toJson();
|
||||||
final clashConfigJson = clashConfig.toJson();
|
|
||||||
return Isolate.run<List<int>>(() async {
|
return Isolate.run<List<int>>(() async {
|
||||||
final archive = Archive();
|
final archive = Archive();
|
||||||
archive.add("config.json", configJson);
|
archive.add("config.json", configJson);
|
||||||
archive.add("clashConfig.json", clashConfigJson);
|
|
||||||
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
await archive.addDirectoryToArchive(profilesPath, homeDirPath);
|
||||||
final zipEncoder = ZipEncoder();
|
final zipEncoder = ZipEncoder();
|
||||||
return zipEncoder.encode(archive) ?? [];
|
return zipEncoder.encode(archive) ?? [];
|
||||||
@@ -689,11 +862,7 @@ class AppController {
|
|||||||
|
|
||||||
updateTray([bool focus = false]) async {
|
updateTray([bool focus = false]) async {
|
||||||
tray.update(
|
tray.update(
|
||||||
appState: appState,
|
trayState: _ref.read(trayStateProvider),
|
||||||
appFlowingState: appFlowingState,
|
|
||||||
config: config,
|
|
||||||
clashConfig: clashConfig,
|
|
||||||
focus: focus,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,39 +874,71 @@ class AppController {
|
|||||||
final zipDecoder = ZipDecoder();
|
final zipDecoder = ZipDecoder();
|
||||||
return zipDecoder.decodeBytes(data);
|
return zipDecoder.decodeBytes(data);
|
||||||
});
|
});
|
||||||
final homeDirPath = await appPath.getHomeDirPath();
|
final homeDirPath = await appPath.homeDirPath;
|
||||||
final configs =
|
final configs =
|
||||||
archive.files.where((item) => item.name.endsWith(".json")).toList();
|
archive.files.where((item) => item.name.endsWith(".json")).toList();
|
||||||
final profiles =
|
final profiles =
|
||||||
archive.files.where((item) => !item.name.endsWith(".json"));
|
archive.files.where((item) => !item.name.endsWith(".json"));
|
||||||
final configIndex =
|
final configIndex =
|
||||||
configs.indexWhere((config) => config.name == "config.json");
|
configs.indexWhere((config) => config.name == "config.json");
|
||||||
final clashConfigIndex =
|
if (configIndex == -1) throw "invalid backup file";
|
||||||
configs.indexWhere((config) => config.name == "clashConfig.json");
|
|
||||||
if (configIndex == -1 || clashConfigIndex == -1) throw "invalid backup.zip";
|
|
||||||
final configFile = configs[configIndex];
|
final configFile = configs[configIndex];
|
||||||
final clashConfigFile = configs[clashConfigIndex];
|
var tempConfig = Config.compatibleFromJson(
|
||||||
final tempConfig = Config.fromJson(
|
|
||||||
json.decode(
|
json.decode(
|
||||||
utf8.decode(configFile.content),
|
utf8.decode(configFile.content),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final tempClashConfig = ClashConfig.fromJson(
|
|
||||||
json.decode(
|
|
||||||
utf8.decode(clashConfigFile.content),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
for (final profile in profiles) {
|
for (final profile in profiles) {
|
||||||
final filePath = join(homeDirPath, profile.name);
|
final filePath = join(homeDirPath, profile.name);
|
||||||
final file = File(filePath);
|
final file = File(filePath);
|
||||||
await file.create(recursive: true);
|
await file.create(recursive: true);
|
||||||
await file.writeAsBytes(profile.content);
|
await file.writeAsBytes(profile.content);
|
||||||
}
|
}
|
||||||
if (recoveryOption == RecoveryOption.onlyProfiles) {
|
final clashConfigIndex =
|
||||||
config.update(tempConfig, RecoveryOption.onlyProfiles);
|
configs.indexWhere((config) => config.name == "clashConfig.json");
|
||||||
} else {
|
if (clashConfigIndex != -1) {
|
||||||
config.update(tempConfig, RecoveryOption.all);
|
final clashConfigFile = configs[clashConfigIndex];
|
||||||
clashConfig.update(tempClashConfig);
|
tempConfig = tempConfig.copyWith(
|
||||||
|
patchClashConfig: ClashConfig.fromJson(
|
||||||
|
json.decode(
|
||||||
|
utf8.decode(
|
||||||
|
clashConfigFile.content,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
_recovery(
|
||||||
|
tempConfig,
|
||||||
|
recoveryOption,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_recovery(Config config, RecoveryOption recoveryOption) {
|
||||||
|
final profiles = config.profiles;
|
||||||
|
for (final profile in profiles) {
|
||||||
|
_ref.read(profilesProvider.notifier).setProfile(profile);
|
||||||
|
}
|
||||||
|
final onlyProfiles = recoveryOption == RecoveryOption.onlyProfiles;
|
||||||
|
if (onlyProfiles) {
|
||||||
|
final currentProfile = _ref.read(currentProfileProvider);
|
||||||
|
if (currentProfile != null) {
|
||||||
|
_ref.read(currentProfileIdProvider.notifier).value = profiles.first.id;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ref.read(patchClashConfigProvider.notifier).value =
|
||||||
|
config.patchClashConfig;
|
||||||
|
_ref.read(appSettingProvider.notifier).value = config.appSetting;
|
||||||
|
_ref.read(currentProfileIdProvider.notifier).value =
|
||||||
|
config.currentProfileId;
|
||||||
|
_ref.read(appDAVSettingProvider.notifier).value = config.dav;
|
||||||
|
_ref.read(themeSettingProvider.notifier).value = config.themeProps;
|
||||||
|
_ref.read(windowSettingProvider.notifier).value = config.windowProps;
|
||||||
|
_ref.read(vpnSettingProvider.notifier).value = config.vpnProps;
|
||||||
|
_ref.read(proxiesStyleSettingProvider.notifier).value = config.proxiesStyle;
|
||||||
|
_ref.read(overrideDnsProvider.notifier).value = config.overrideDns;
|
||||||
|
_ref.read(networkSettingProvider.notifier).value = config.networkProps;
|
||||||
|
_ref.read(hotKeyActionsProvider.notifier).value = config.hotKeyActions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,24 @@ const desktopPlatforms = [
|
|||||||
SupportPlatform.Windows,
|
SupportPlatform.Windows,
|
||||||
];
|
];
|
||||||
|
|
||||||
enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay }
|
enum GroupType {
|
||||||
|
Selector,
|
||||||
|
URLTest,
|
||||||
|
Fallback,
|
||||||
|
LoadBalance,
|
||||||
|
Relay;
|
||||||
|
|
||||||
|
static GroupType parseProfileType(String type) {
|
||||||
|
return switch (type) {
|
||||||
|
"url-test" => URLTest,
|
||||||
|
"select" => Selector,
|
||||||
|
"fallback" => Fallback,
|
||||||
|
"load-balance" => LoadBalance,
|
||||||
|
"relay" => Relay,
|
||||||
|
String() => throw UnimplementedError(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
enum GroupName { GLOBAL, Proxy, Auto, Fallback }
|
||||||
|
|
||||||
@@ -45,7 +62,7 @@ extension GroupTypeExtension on GroupType {
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
bool get isURLTestOrFallback {
|
bool get isComputedSelected {
|
||||||
return [GroupType.URLTest, GroupType.Fallback].contains(this);
|
return [GroupType.URLTest, GroupType.Fallback].contains(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +155,13 @@ enum DnsMode {
|
|||||||
hosts
|
hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ExternalControllerStatus {
|
||||||
|
@JsonValue("")
|
||||||
|
close,
|
||||||
|
@JsonValue("127.0.0.1:9090")
|
||||||
|
open
|
||||||
|
}
|
||||||
|
|
||||||
enum KeyboardModifier {
|
enum KeyboardModifier {
|
||||||
alt([
|
alt([
|
||||||
PhysicalKeyboardKey.altLeft,
|
PhysicalKeyboardKey.altLeft,
|
||||||
@@ -238,6 +262,7 @@ enum ActionMethod {
|
|||||||
stopListener,
|
stopListener,
|
||||||
getCountryCode,
|
getCountryCode,
|
||||||
getMemory,
|
getMemory,
|
||||||
|
getProfile,
|
||||||
|
|
||||||
///Android,
|
///Android,
|
||||||
setFdMap,
|
setFdMap,
|
||||||
@@ -270,7 +295,11 @@ enum DebounceTag {
|
|||||||
handleWill,
|
handleWill,
|
||||||
updateDelay,
|
updateDelay,
|
||||||
vpnTip,
|
vpnTip,
|
||||||
autoLaunch
|
autoLaunch,
|
||||||
|
renderPause,
|
||||||
|
updatePageIndex,
|
||||||
|
pageChange,
|
||||||
|
proxiesTabChange,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DashboardWidget {
|
enum DashboardWidget {
|
||||||
@@ -341,3 +370,19 @@ enum DashboardWidget {
|
|||||||
return dashboardWidgets[index];
|
return dashboardWidgets[index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum GeodataLoader {
|
||||||
|
standard,
|
||||||
|
memconservative,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PageLabel {
|
||||||
|
dashboard,
|
||||||
|
proxies,
|
||||||
|
profiles,
|
||||||
|
tools,
|
||||||
|
logs,
|
||||||
|
requests,
|
||||||
|
resources,
|
||||||
|
connections,
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,41 +4,50 @@ import 'package:fl_clash/enum/enum.dart';
|
|||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/plugins/app.dart';
|
import 'package:fl_clash/plugins/app.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class AccessFragment extends StatefulWidget {
|
class AccessFragment extends ConsumerStatefulWidget {
|
||||||
const AccessFragment({super.key});
|
const AccessFragment({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AccessFragment> createState() => _AccessFragmentState();
|
ConsumerState<AccessFragment> createState() => _AccessFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AccessFragmentState extends State<AccessFragment> {
|
class _AccessFragmentState extends ConsumerState<AccessFragment> {
|
||||||
List<String> acceptList = [];
|
List<String> acceptList = [];
|
||||||
List<String> rejectList = [];
|
List<String> rejectList = [];
|
||||||
|
late ScrollController _controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_updateInitList();
|
_updateInitList();
|
||||||
|
_controller = ScrollController();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final appState = globalState.appController.appState;
|
final appState = globalState.appState;
|
||||||
if (appState.packages.isEmpty) {
|
if (appState.packages.isEmpty) {
|
||||||
Future.delayed(const Duration(milliseconds: 300), () async {
|
Future.delayed(const Duration(milliseconds: 300), () async {
|
||||||
appState.packages = await app?.getPackages() ?? [];
|
ref.read(packagesProvider.notifier).value =
|
||||||
|
await app?.getPackages() ?? [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
_updateInitList() {
|
_updateInitList() {
|
||||||
final accessControl = globalState.appController.config.accessControl;
|
acceptList = globalState.config.vpnProps.accessControl.acceptList;
|
||||||
acceptList = accessControl.acceptList;
|
rejectList = globalState.config.vpnProps.accessControl.rejectList;
|
||||||
rejectList = accessControl.rejectList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSearchButton() {
|
Widget _buildSearchButton() {
|
||||||
@@ -51,9 +60,13 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
acceptList: acceptList,
|
acceptList: acceptList,
|
||||||
rejectList: rejectList,
|
rejectList: rejectList,
|
||||||
),
|
),
|
||||||
).then((_) => setState(() {
|
).then(
|
||||||
_updateInitList();
|
(_) => setState(
|
||||||
}));
|
() {
|
||||||
|
_updateInitList();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
);
|
);
|
||||||
@@ -69,28 +82,29 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
return IconButton(
|
return IconButton(
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final config = globalState.appController.config;
|
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
||||||
final isAccept =
|
final isAccept =
|
||||||
config.accessControl.mode == AccessControlMode.acceptSelected;
|
state.accessControl.mode == AccessControlMode.acceptSelected;
|
||||||
if (isSelectedAll) {
|
if (isSelectedAll) {
|
||||||
config.accessControl = switch (isAccept) {
|
return switch (isAccept) {
|
||||||
true => config.accessControl.copyWith(
|
true => state.copyWith.accessControl(
|
||||||
acceptList: [],
|
acceptList: [],
|
||||||
),
|
),
|
||||||
false => config.accessControl.copyWith(
|
false => state.copyWith.accessControl(
|
||||||
rejectList: [],
|
rejectList: [],
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
config.accessControl = switch (isAccept) {
|
return switch (isAccept) {
|
||||||
true => config.accessControl.copyWith(
|
true => state.copyWith.accessControl(
|
||||||
acceptList: allValueList,
|
acceptList: allValueList,
|
||||||
),
|
),
|
||||||
false => config.accessControl.copyWith(
|
false => state.copyWith.accessControl(
|
||||||
rejectList: allValueList,
|
rejectList: allValueList,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
icon: isSelectedAll
|
icon: isSelectedAll
|
||||||
? const Icon(Icons.deselect)
|
? const Icon(Icons.deselect)
|
||||||
@@ -98,218 +112,239 @@ class _AccessFragmentState extends State<AccessFragment> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSettingButton() {
|
_intelligentSelected() async {
|
||||||
return IconButton(
|
final appState = globalState.appState;
|
||||||
onPressed: () {
|
final config = globalState.config;
|
||||||
showSheet(
|
final accessControl = config.vpnProps.accessControl;
|
||||||
title: appLocalizations.proxiesSetting,
|
final packageNames = appState.packages
|
||||||
context: context,
|
.where(
|
||||||
body: AccessControlWidget(
|
(item) =>
|
||||||
context: context,
|
accessControl.isFilterSystemApp ? item.isSystem == false : true,
|
||||||
|
)
|
||||||
|
.map((item) => item.packageName);
|
||||||
|
final commonScaffoldState = context.commonScaffoldState;
|
||||||
|
if (commonScaffoldState?.mounted != true) return;
|
||||||
|
final selectedPackageNames =
|
||||||
|
(await commonScaffoldState?.loadingRun<List<String>>(
|
||||||
|
() async {
|
||||||
|
return await app?.getChinaPackageNames() ?? [];
|
||||||
|
},
|
||||||
|
))
|
||||||
|
?.toSet() ??
|
||||||
|
{};
|
||||||
|
final acceptList = packageNames
|
||||||
|
.where((item) => !selectedPackageNames.contains(item))
|
||||||
|
.toList();
|
||||||
|
final rejectList = packageNames
|
||||||
|
.where((item) => selectedPackageNames.contains(item))
|
||||||
|
.toList();
|
||||||
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith.accessControl(
|
||||||
|
acceptList: acceptList,
|
||||||
|
rejectList: rejectList,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSettingButton() {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final res = await showSheet<int>(
|
||||||
|
title: appLocalizations.proxiesSetting,
|
||||||
|
context: context,
|
||||||
|
body: AccessControlPanel(),
|
||||||
|
);
|
||||||
|
if (res == 1) {
|
||||||
|
_intelligentSelected();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.tune),
|
icon: const Icon(Icons.tune),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleSelected(List<String> valueList, Package package, bool? value) {
|
||||||
|
if (value == true) {
|
||||||
|
valueList.add(package.packageName);
|
||||||
|
} else {
|
||||||
|
valueList.remove(package.packageName);
|
||||||
|
}
|
||||||
|
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
||||||
|
return switch (
|
||||||
|
state.accessControl.mode == AccessControlMode.acceptSelected) {
|
||||||
|
true => state.copyWith.accessControl(
|
||||||
|
acceptList: valueList,
|
||||||
|
),
|
||||||
|
false => state.copyWith.accessControl(
|
||||||
|
rejectList: valueList,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<Config, bool>(
|
final state = ref.watch(packageListSelectorStateProvider);
|
||||||
selector: (_, config) => config.isAccessControl,
|
final accessControl = state.accessControl;
|
||||||
builder: (_, isAccessControl, child) {
|
final accessControlMode = accessControl.mode;
|
||||||
return Column(
|
final packages = state.getList(
|
||||||
mainAxisSize: MainAxisSize.max,
|
accessControlMode == AccessControlMode.acceptSelected
|
||||||
children: [
|
? acceptList
|
||||||
Flexible(
|
: rejectList,
|
||||||
flex: 0,
|
);
|
||||||
child: ListItem.switchItem(
|
final currentList = accessControl.currentList;
|
||||||
title: Text(appLocalizations.appAccessControl),
|
final packageNameList = packages.map((e) => e.packageName).toList();
|
||||||
delegate: SwitchDelegate(
|
final valueList = currentList.intersection(packageNameList);
|
||||||
value: isAccessControl,
|
final describe = accessControlMode == AccessControlMode.acceptSelected
|
||||||
onChanged: (isAccessControl) {
|
? appLocalizations.accessControlAllowDesc
|
||||||
final config = context.read<Config>();
|
: appLocalizations.accessControlNotAllowDesc;
|
||||||
config.isAccessControl = isAccessControl;
|
return Column(
|
||||||
},
|
mainAxisSize: MainAxisSize.max,
|
||||||
),
|
children: [
|
||||||
),
|
Flexible(
|
||||||
|
flex: 0,
|
||||||
|
child: ListItem.switchItem(
|
||||||
|
title: Text(appLocalizations.appAccessControl),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: accessControl.enable,
|
||||||
|
onChanged: (enable) {
|
||||||
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith.accessControl(
|
||||||
|
enable: enable,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Padding(
|
),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
),
|
||||||
child: Divider(
|
const Padding(
|
||||||
height: 12,
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
),
|
child: Divider(
|
||||||
),
|
height: 12,
|
||||||
Flexible(
|
),
|
||||||
child: child!,
|
),
|
||||||
),
|
Flexible(
|
||||||
],
|
child: DisabledMask(
|
||||||
);
|
status: !accessControl.enable,
|
||||||
},
|
child: Column(
|
||||||
child: Selector<AppState, List<Package>>(
|
children: [
|
||||||
selector: (_, appState) => appState.packages,
|
ActivateBox(
|
||||||
builder: (_, packages, ___) {
|
active: accessControl.enable,
|
||||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
child: Padding(
|
||||||
selector: (_, appState, config) => PackageListSelectorState(
|
padding: const EdgeInsets.only(
|
||||||
accessControl: config.accessControl,
|
top: 4,
|
||||||
isAccessControl: config.isAccessControl,
|
bottom: 4,
|
||||||
packages: appState.packages,
|
left: 16,
|
||||||
),
|
right: 8,
|
||||||
builder: (context, state, __) {
|
),
|
||||||
final accessControl = state.accessControl;
|
child: Row(
|
||||||
final isAccessControl = state.isAccessControl;
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
final accessControlMode = accessControl.mode;
|
mainAxisSize: MainAxisSize.max,
|
||||||
final packages = state.getList(
|
children: [
|
||||||
accessControlMode == AccessControlMode.acceptSelected
|
Expanded(
|
||||||
? acceptList
|
child: IntrinsicHeight(
|
||||||
: rejectList,
|
child: Column(
|
||||||
);
|
mainAxisSize: MainAxisSize.max,
|
||||||
final currentList = accessControl.currentList;
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
final packageNameList =
|
|
||||||
packages.map((e) => e.packageName).toList();
|
|
||||||
final valueList = currentList.intersection(packageNameList);
|
|
||||||
final describe =
|
|
||||||
accessControlMode == AccessControlMode.acceptSelected
|
|
||||||
? appLocalizations.accessControlAllowDesc
|
|
||||||
: appLocalizations.accessControlNotAllowDesc;
|
|
||||||
return DisabledMask(
|
|
||||||
status: !isAccessControl,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
ActivateBox(
|
|
||||||
active: isAccessControl,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 4,
|
|
||||||
bottom: 4,
|
|
||||||
left: 16,
|
|
||||||
right: 8,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: IntrinsicHeight(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
appLocalizations.selected,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.labelLarge
|
|
||||||
?.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Flexible(
|
|
||||||
child: SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
"${valueList.length}",
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.labelLarge
|
|
||||||
?.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Text(describe),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Expanded(
|
||||||
child: _buildSearchButton(),
|
child: Row(
|
||||||
),
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: _buildSelectedAllButton(
|
child: Text(
|
||||||
isSelectedAll: valueList.length ==
|
appLocalizations.selected,
|
||||||
packageNameList.length,
|
style: Theme.of(context)
|
||||||
allValueList: packageNameList,
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Flexible(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
"${valueList.length}",
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: _buildSettingButton(),
|
child: Text(describe),
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: _buildSearchButton(),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: _buildSelectedAllButton(
|
||||||
|
isSelectedAll:
|
||||||
|
valueList.length == packageNameList.length,
|
||||||
|
allValueList: packageNameList,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: _buildSettingButton(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
flex: 1,
|
|
||||||
child: packages.isEmpty
|
|
||||||
? const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
)
|
|
||||||
: ListView.builder(
|
|
||||||
itemCount: packages.length,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final package = packages[index];
|
|
||||||
return PackageListItem(
|
|
||||||
key: Key(package.packageName),
|
|
||||||
package: package,
|
|
||||||
value:
|
|
||||||
valueList.contains(package.packageName),
|
|
||||||
isActive: isAccessControl,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == true) {
|
|
||||||
valueList.add(package.packageName);
|
|
||||||
} else {
|
|
||||||
valueList.remove(package.packageName);
|
|
||||||
}
|
|
||||||
final config =
|
|
||||||
globalState.appController.config;
|
|
||||||
if (accessControlMode ==
|
|
||||||
AccessControlMode.acceptSelected) {
|
|
||||||
config.accessControl =
|
|
||||||
config.accessControl.copyWith(
|
|
||||||
acceptList: valueList,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
config.accessControl =
|
|
||||||
config.accessControl.copyWith(
|
|
||||||
rejectList: valueList,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
Expanded(
|
||||||
},
|
flex: 1,
|
||||||
);
|
child: packages.isEmpty
|
||||||
},
|
? const Center(
|
||||||
),
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: CommonScrollBar(
|
||||||
|
controller: _controller,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _controller,
|
||||||
|
itemCount: packages.length,
|
||||||
|
itemExtent: 72,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final package = packages[index];
|
||||||
|
return PackageListItem(
|
||||||
|
key: Key(package.packageName),
|
||||||
|
package: package,
|
||||||
|
value: valueList.contains(package.packageName),
|
||||||
|
isActive: accessControl.enable,
|
||||||
|
onChanged: (value) {
|
||||||
|
_handleSelected(valueList, package, value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,45 +365,47 @@ class PackageListItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ActivateBox(
|
return FadeScaleEnterBox(
|
||||||
active: isActive,
|
child: ActivateBox(
|
||||||
child: ListItem.checkbox(
|
active: isActive,
|
||||||
leading: SizedBox(
|
child: ListItem.checkbox(
|
||||||
width: 48,
|
leading: SizedBox(
|
||||||
height: 48,
|
width: 48,
|
||||||
child: FutureBuilder<ImageProvider?>(
|
height: 48,
|
||||||
future: app?.getPackageIcon(package.packageName),
|
child: FutureBuilder<ImageProvider?>(
|
||||||
builder: (_, snapshot) {
|
future: app?.getPackageIcon(package.packageName),
|
||||||
if (!snapshot.hasData && snapshot.data == null) {
|
builder: (_, snapshot) {
|
||||||
return Container();
|
if (!snapshot.hasData && snapshot.data == null) {
|
||||||
} else {
|
return Container();
|
||||||
return Image(
|
} else {
|
||||||
image: snapshot.data!,
|
return Image(
|
||||||
gaplessPlayback: true,
|
image: snapshot.data!,
|
||||||
width: 48,
|
gaplessPlayback: true,
|
||||||
height: 48,
|
width: 48,
|
||||||
);
|
height: 48,
|
||||||
}
|
);
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
title: Text(
|
||||||
title: Text(
|
package.label,
|
||||||
package.label,
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
overflow: TextOverflow.ellipsis,
|
||||||
overflow: TextOverflow.ellipsis,
|
),
|
||||||
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
subtitle: Text(
|
||||||
),
|
package.packageName,
|
||||||
subtitle: Text(
|
style: const TextStyle(
|
||||||
package.packageName,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
delegate: CheckboxDelegate(
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
delegate: CheckboxDelegate(
|
|
||||||
value: value,
|
|
||||||
onChanged: onChanged,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -413,15 +450,31 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleSelected(
|
||||||
|
WidgetRef ref, List<String> valueList, Package package, bool? value) {
|
||||||
|
if (value == true) {
|
||||||
|
valueList.add(package.packageName);
|
||||||
|
} else {
|
||||||
|
valueList.remove(package.packageName);
|
||||||
|
}
|
||||||
|
ref.read(vpnSettingProvider.notifier).updateState((state) {
|
||||||
|
return switch (
|
||||||
|
state.accessControl.mode == AccessControlMode.acceptSelected) {
|
||||||
|
true => state.copyWith.accessControl(
|
||||||
|
acceptList: valueList,
|
||||||
|
),
|
||||||
|
false => state.copyWith.accessControl(
|
||||||
|
rejectList: valueList,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Widget _packageList() {
|
Widget _packageList() {
|
||||||
final lowQuery = query.toLowerCase();
|
final lowQuery = query.toLowerCase();
|
||||||
return Selector2<AppState, Config, PackageListSelectorState>(
|
return Consumer(
|
||||||
selector: (_, appState, config) => PackageListSelectorState(
|
builder: (context, ref, __) {
|
||||||
packages: appState.packages,
|
final state = ref.watch(packageListSelectorStateProvider);
|
||||||
accessControl: config.accessControl,
|
|
||||||
isAccessControl: config.isAccessControl,
|
|
||||||
),
|
|
||||||
builder: (context, state, __) {
|
|
||||||
final accessControl = state.accessControl;
|
final accessControl = state.accessControl;
|
||||||
final accessControlMode = accessControl.mode;
|
final accessControlMode = accessControl.mode;
|
||||||
final packages = state.getList(
|
final packages = state.getList(
|
||||||
@@ -436,7 +489,7 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
|||||||
package.packageName.contains(lowQuery),
|
package.packageName.contains(lowQuery),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
final isAccessControl = state.isAccessControl;
|
final isAccessControl = state.accessControl.enable;
|
||||||
final currentList = accessControl.currentList;
|
final currentList = accessControl.currentList;
|
||||||
final packageNameList = packages.map((e) => e.packageName).toList();
|
final packageNameList = packages.map((e) => e.packageName).toList();
|
||||||
final valueList = currentList.intersection(packageNameList);
|
final valueList = currentList.intersection(packageNameList);
|
||||||
@@ -452,21 +505,12 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
|||||||
value: valueList.contains(package.packageName),
|
value: valueList.contains(package.packageName),
|
||||||
isActive: isAccessControl,
|
isActive: isAccessControl,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == true) {
|
_handleSelected(
|
||||||
valueList.add(package.packageName);
|
ref,
|
||||||
} else {
|
valueList,
|
||||||
valueList.remove(package.packageName);
|
package,
|
||||||
}
|
value,
|
||||||
final config = globalState.appController.config;
|
);
|
||||||
if (accessControlMode == AccessControlMode.acceptSelected) {
|
|
||||||
config.accessControl = config.accessControl.copyWith(
|
|
||||||
acceptList: valueList,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
config.accessControl = config.accessControl.copyWith(
|
|
||||||
rejectList: valueList,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -487,14 +531,16 @@ class AccessControlSearchDelegate extends SearchDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccessControlWidget extends StatelessWidget {
|
class AccessControlPanel extends ConsumerStatefulWidget {
|
||||||
final BuildContext context;
|
const AccessControlPanel({
|
||||||
|
|
||||||
const AccessControlWidget({
|
|
||||||
super.key,
|
super.key,
|
||||||
required this.context,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState createState() => _AccessControlPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccessControlPanelState extends ConsumerState<AccessControlPanel> {
|
||||||
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
|
IconData _getIconWithAccessControlMode(AccessControlMode mode) {
|
||||||
return switch (mode) {
|
return switch (mode) {
|
||||||
AccessControlMode.acceptSelected => Icons.adjust_outlined,
|
AccessControlMode.acceptSelected => Icons.adjust_outlined,
|
||||||
@@ -539,9 +585,11 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Selector<Config, AccessControlMode>(
|
child: Consumer(
|
||||||
selector: (_, config) => config.accessControl.mode,
|
builder: (_, ref, __) {
|
||||||
builder: (_, accessControlMode, __) {
|
final accessControlMode = ref.watch(
|
||||||
|
vpnSettingProvider.select((state) => state.accessControl.mode),
|
||||||
|
);
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
@@ -553,10 +601,11 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
isSelected: accessControlMode == item,
|
isSelected: accessControlMode == item,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final config = globalState.appController.config;
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
config.accessControl = config.accessControl.copyWith(
|
(state) => state.copyWith.accessControl(
|
||||||
mode: item,
|
mode: item,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -575,9 +624,11 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Selector<Config, AccessSortType>(
|
child: Consumer(
|
||||||
selector: (_, config) => config.accessControl.sort,
|
builder: (_, ref, __) {
|
||||||
builder: (_, accessSortType, __) {
|
final accessSortType = ref.watch(
|
||||||
|
vpnSettingProvider.select((state) => state.accessControl.sort),
|
||||||
|
);
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
@@ -589,10 +640,11 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
isSelected: accessSortType == item,
|
isSelected: accessSortType == item,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final config = globalState.appController.config;
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
config.accessControl = config.accessControl.copyWith(
|
(state) => state.copyWith.accessControl(
|
||||||
sort: item,
|
sort: item,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -611,9 +663,12 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Selector<Config, bool>(
|
child: Consumer(
|
||||||
selector: (_, config) => config.accessControl.isFilterSystemApp,
|
builder: (_, ref, __) {
|
||||||
builder: (_, isFilterSystemApp, __) {
|
final isFilterSystemApp = ref.watch(
|
||||||
|
vpnSettingProvider
|
||||||
|
.select((state) => state.accessControl.isFilterSystemApp),
|
||||||
|
);
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
@@ -622,10 +677,11 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
_getTextWithIsFilterSystemApp(item),
|
_getTextWithIsFilterSystemApp(item),
|
||||||
isSelected: isFilterSystemApp == item,
|
isSelected: isFilterSystemApp == item,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final config = globalState.appController.config;
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
config.accessControl = config.accessControl.copyWith(
|
(state) => state.copyWith.accessControl(
|
||||||
isFilterSystemApp: item,
|
isFilterSystemApp: item,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -637,63 +693,35 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_intelligentSelected() async {
|
|
||||||
final appState = globalState.appController.appState;
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
final accessControl = config.accessControl;
|
|
||||||
final packageNames = appState.packages
|
|
||||||
.where(
|
|
||||||
(item) =>
|
|
||||||
accessControl.isFilterSystemApp ? item.isSystem == false : true,
|
|
||||||
)
|
|
||||||
.map((item) => item.packageName);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
final commonScaffoldState = context.commonScaffoldState;
|
|
||||||
if (commonScaffoldState?.mounted != true) return;
|
|
||||||
final selectedPackageNames =
|
|
||||||
(await commonScaffoldState?.loadingRun<List<String>>(
|
|
||||||
() async {
|
|
||||||
return await app?.getChinaPackageNames() ?? [];
|
|
||||||
},
|
|
||||||
))
|
|
||||||
?.toSet() ??
|
|
||||||
{};
|
|
||||||
final acceptList = packageNames
|
|
||||||
.where((item) => !selectedPackageNames.contains(item))
|
|
||||||
.toList();
|
|
||||||
final rejectList = packageNames
|
|
||||||
.where((item) => selectedPackageNames.contains(item))
|
|
||||||
.toList();
|
|
||||||
config.accessControl = accessControl.copyWith(
|
|
||||||
acceptList: acceptList,
|
|
||||||
rejectList: rejectList,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_copyToClipboard() async {
|
_copyToClipboard() async {
|
||||||
await globalState.safeRun(() {
|
await globalState.safeRun(() {
|
||||||
final data = globalState.appController.config.accessControl.toJson();
|
final data = globalState.config.vpnProps.accessControl.toJson();
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(
|
ClipboardData(
|
||||||
text: json.encode(data),
|
text: json.encode(data),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (!context.mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
_pasteToClipboard() async {
|
_pasteToClipboard() async {
|
||||||
await globalState.safeRun(() async {
|
await globalState.safeRun(
|
||||||
final config = globalState.appController.config;
|
() async {
|
||||||
final data = await Clipboard.getData('text/plain');
|
final data = await Clipboard.getData('text/plain');
|
||||||
final text = data?.text;
|
final text = data?.text;
|
||||||
if (text == null) return;
|
if (text == null) return;
|
||||||
config.accessControl = AccessControl.fromJson(
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
json.decode(text),
|
(state) => state.copyWith(
|
||||||
);
|
accessControl: AccessControl.fromJson(
|
||||||
});
|
json.decode(text),
|
||||||
if (!context.mounted) return;
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,7 +740,9 @@ class AccessControlWidget extends StatelessWidget {
|
|||||||
CommonChip(
|
CommonChip(
|
||||||
avatar: const Icon(Icons.auto_awesome),
|
avatar: const Icon(Icons.auto_awesome),
|
||||||
label: appLocalizations.intelligentSelected,
|
label: appLocalizations.intelligentSelected,
|
||||||
onPressed: _intelligentSelected,
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(1);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
CommonChip(
|
CommonChip(
|
||||||
avatar: const Icon(Icons.paste),
|
avatar: const Icon(Icons.paste),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,208 +1,172 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class OverrideItem extends StatelessWidget {
|
class OverrideItem extends ConsumerWidget {
|
||||||
const OverrideItem({super.key});
|
const OverrideItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final override = ref.watch(overrideDnsProvider);
|
||||||
selector: (_, config) => config.overrideDns,
|
return ListItem.switchItem(
|
||||||
builder: (_, override, __) {
|
title: Text(appLocalizations.overrideDns),
|
||||||
return ListItem.switchItem(
|
subtitle: Text(appLocalizations.overrideDnsDesc),
|
||||||
title: Text(appLocalizations.overrideDns),
|
delegate: SwitchDelegate(
|
||||||
subtitle: Text(appLocalizations.overrideDnsDesc),
|
value: override,
|
||||||
delegate: SwitchDelegate(
|
onChanged: (bool value) async {
|
||||||
value: override,
|
ref.read(overrideDnsProvider.notifier).value = value;
|
||||||
onChanged: (bool value) async {
|
},
|
||||||
final config = globalState.appController.config;
|
),
|
||||||
config.overrideDns = value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatusItem extends StatelessWidget {
|
class StatusItem extends ConsumerWidget {
|
||||||
const StatusItem({super.key});
|
const StatusItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final enable =
|
||||||
selector: (_, clashConfig) => clashConfig.dns.enable,
|
ref.watch(patchClashConfigProvider.select((state) => state.dns.enable));
|
||||||
builder: (_, enable, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
title: Text(appLocalizations.status),
|
||||||
title: Text(appLocalizations.status),
|
subtitle: Text(appLocalizations.statusDesc),
|
||||||
subtitle: Text(appLocalizations.statusDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: enable,
|
||||||
value: enable,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
.read(patchClashConfigProvider.notifier)
|
||||||
final dns = clashConfig.dns;
|
.updateState((state) => state.copyWith.dns(enable: value));
|
||||||
clashConfig.dns = dns.copyWith(
|
},
|
||||||
enable: value,
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreferH3Item extends StatelessWidget {
|
class PreferH3Item extends ConsumerWidget {
|
||||||
const PreferH3Item({super.key});
|
const PreferH3Item({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final preferH3 = ref
|
||||||
selector: (_, clashConfig) => clashConfig.dns.preferH3,
|
.watch(patchClashConfigProvider.select((state) => state.dns.preferH3));
|
||||||
builder: (_, preferH3, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
title: const Text("PreferH3"),
|
||||||
title: const Text("PreferH3"),
|
subtitle: Text(appLocalizations.preferH3Desc),
|
||||||
subtitle: Text(appLocalizations.preferH3Desc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: preferH3,
|
||||||
value: preferH3,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
.read(patchClashConfigProvider.notifier)
|
||||||
final dns = clashConfig.dns;
|
.updateState((state) => state.copyWith.dns(preferH3: value));
|
||||||
clashConfig.dns = dns.copyWith(
|
},
|
||||||
preferH3: value,
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IPv6Item extends StatelessWidget {
|
class IPv6Item extends ConsumerWidget {
|
||||||
const IPv6Item({super.key});
|
const IPv6Item({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final ipv6 = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.ipv6,
|
patchClashConfigProvider.select((state) => state.dns.ipv6),
|
||||||
builder: (_, ipv6, __) {
|
);
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: const Text("IPv6"),
|
title: const Text("IPv6"),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: ipv6,
|
value: ipv6,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(
|
.updateState((state) => state.copyWith.dns(ipv6: value));
|
||||||
ipv6: value,
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RespectRulesItem extends StatelessWidget {
|
class RespectRulesItem extends ConsumerWidget {
|
||||||
const RespectRulesItem({super.key});
|
const RespectRulesItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final respectRules = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.respectRules,
|
patchClashConfigProvider.select((state) => state.dns.respectRules),
|
||||||
builder: (_, respectRules, __) {
|
);
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.respectRules),
|
title: Text(appLocalizations.respectRules),
|
||||||
subtitle: Text(appLocalizations.respectRulesDesc),
|
subtitle: Text(appLocalizations.respectRulesDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: respectRules,
|
value: respectRules,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(
|
.updateState((state) => state.copyWith.dns(respectRules: value));
|
||||||
respectRules: value,
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DnsModeItem extends StatelessWidget {
|
class DnsModeItem extends ConsumerWidget {
|
||||||
const DnsModeItem({super.key});
|
const DnsModeItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, DnsMode>(
|
final enhancedMode = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.enhancedMode,
|
patchClashConfigProvider.select((state) => state.dns.enhancedMode),
|
||||||
builder: (_, enhancedMode, __) {
|
);
|
||||||
return ListItem<DnsMode>.options(
|
return ListItem<DnsMode>.options(
|
||||||
title: Text(appLocalizations.dnsMode),
|
title: Text(appLocalizations.dnsMode),
|
||||||
subtitle: Text(enhancedMode.name),
|
subtitle: Text(enhancedMode.name),
|
||||||
delegate: OptionsDelegate(
|
delegate: OptionsDelegate(
|
||||||
title: appLocalizations.dnsMode,
|
title: appLocalizations.dnsMode,
|
||||||
options: DnsMode.values,
|
options: DnsMode.values,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(enhancedMode: value);
|
.updateState((state) => state.copyWith.dns(enhancedMode: value));
|
||||||
},
|
},
|
||||||
textBuilder: (dnsMode) => dnsMode.name,
|
textBuilder: (dnsMode) => dnsMode.name,
|
||||||
value: enhancedMode,
|
value: enhancedMode,
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeIpRangeItem extends StatelessWidget {
|
class FakeIpRangeItem extends ConsumerWidget {
|
||||||
const FakeIpRangeItem({super.key});
|
const FakeIpRangeItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, String>(
|
final fakeIpRange = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpRange,
|
patchClashConfigProvider.select((state) => state.dns.fakeIpRange),
|
||||||
builder: (_, fakeIpRange, __) {
|
);
|
||||||
return ListItem.input(
|
return ListItem.input(
|
||||||
title: Text(appLocalizations.fakeipRange),
|
title: Text(appLocalizations.fakeipRange),
|
||||||
subtitle: Text(fakeIpRange),
|
subtitle: Text(fakeIpRange),
|
||||||
delegate: InputDelegate(
|
delegate: InputDelegate(
|
||||||
title: appLocalizations.fakeipRange,
|
title: appLocalizations.fakeipRange,
|
||||||
value: fakeIpRange,
|
value: fakeIpRange,
|
||||||
onChanged: (String? value) {
|
onChanged: (String? value) {
|
||||||
if (value != null) {
|
if (value == null) {
|
||||||
try {
|
return;
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
}
|
||||||
clashConfig.dns = clashConfig.dns.copyWith(
|
ref
|
||||||
fakeIpRange: value,
|
.read(patchClashConfigProvider.notifier)
|
||||||
);
|
.updateState((state) => state.copyWith.dns(fakeIpRange: value));
|
||||||
} catch (e) {
|
},
|
||||||
globalState.showMessage(
|
),
|
||||||
title: appLocalizations.fakeipRange,
|
|
||||||
message: TextSpan(
|
|
||||||
text: e.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,20 +181,22 @@ class FakeIpFilterItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.fakeipFilter,
|
title: appLocalizations.fakeipFilter,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter,
|
builder: (_, ref, __) {
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
final fakeIpFilter = ref.watch(
|
||||||
builder: (_, fakeIpFilter, __) {
|
patchClashConfigProvider
|
||||||
|
.select((state) => state.dns.fakeIpFilter),
|
||||||
|
);
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.fakeipFilter,
|
title: appLocalizations.fakeipFilter,
|
||||||
items: fakeIpFilter,
|
items: fakeIpFilter,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items) {
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(
|
.updateState((state) => state.copyWith.dns(
|
||||||
fakeIpFilter: List.from(items),
|
fakeIpFilter: List.from(items),
|
||||||
);
|
));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -252,24 +218,24 @@ class DefaultNameserverItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.defaultNameserver,
|
title: appLocalizations.defaultNameserver,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.defaultNameserver,
|
final defaultNameserver = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider
|
||||||
builder: (_, defaultNameserver, __) {
|
.select((state) => state.dns.defaultNameserver),
|
||||||
return ListPage(
|
);
|
||||||
title: appLocalizations.defaultNameserver,
|
return ListPage(
|
||||||
items: defaultNameserver,
|
title: appLocalizations.defaultNameserver,
|
||||||
titleBuilder: (item) => Text(item),
|
items: defaultNameserver,
|
||||||
onChange: (items) {
|
titleBuilder: (item) => Text(item),
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChange: (items) {
|
||||||
final dns = clashConfig.dns;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.dns = dns.copyWith(
|
(state) => state.copyWith.dns(
|
||||||
defaultNameserver: List.from(items),
|
defaultNameserver: List.from(items),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
),
|
}),
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -287,78 +253,71 @@ class NameserverItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
title: appLocalizations.nameserver,
|
title: appLocalizations.nameserver,
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.nameserver,
|
final nameserver = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider.select((state) => state.dns.nameserver),
|
||||||
builder: (_, nameserver, __) {
|
);
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: "域名服务器",
|
title: "域名服务器",
|
||||||
items: nameserver,
|
items: nameserver,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items) {
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final dns = clashConfig.dns;
|
(state) => state.copyWith.dns(
|
||||||
clashConfig.dns = dns.copyWith(
|
nameserver: List.from(items),
|
||||||
nameserver: List.from(items),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UseHostsItem extends StatelessWidget {
|
class UseHostsItem extends ConsumerWidget {
|
||||||
const UseHostsItem({super.key});
|
const UseHostsItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final useHosts = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.useHosts,
|
patchClashConfigProvider.select((state) => state.dns.useHosts),
|
||||||
builder: (_, useHosts, __) {
|
);
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.useHosts),
|
title: Text(appLocalizations.useHosts),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: useHosts,
|
value: useHosts,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(
|
.updateState((state) => state.copyWith.dns(useHosts: value));
|
||||||
useHosts: value,
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UseSystemHostsItem extends StatelessWidget {
|
class UseSystemHostsItem extends ConsumerWidget {
|
||||||
const UseSystemHostsItem({super.key});
|
const UseSystemHostsItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final useSystemHosts = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.useSystemHosts,
|
patchClashConfigProvider.select((state) => state.dns.useSystemHosts),
|
||||||
builder: (_, useSystemHosts, __) {
|
);
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.useSystemHosts),
|
title: Text(appLocalizations.useSystemHosts),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: useSystemHosts,
|
value: useSystemHosts,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref
|
||||||
final dns = clashConfig.dns;
|
.read(patchClashConfigProvider.notifier)
|
||||||
clashConfig.dns = dns.copyWith(
|
.updateState((state) => state.copyWith.dns(
|
||||||
useSystemHosts: value,
|
useSystemHosts: value,
|
||||||
);
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,26 +333,25 @@ class NameserverPolicyItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.nameserverPolicy,
|
title: appLocalizations.nameserverPolicy,
|
||||||
widget: Selector<ClashConfig, Map<String, String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy,
|
final nameserverPolicy = ref.watch(
|
||||||
shouldRebuild: (prev, next) =>
|
patchClashConfigProvider
|
||||||
!const MapEquality<String, String>().equals(prev, next),
|
.select((state) => state.dns.nameserverPolicy),
|
||||||
builder: (_, nameserverPolicy, __) {
|
);
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.nameserverPolicy,
|
title: appLocalizations.nameserverPolicy,
|
||||||
items: nameserverPolicy.entries,
|
items: nameserverPolicy.entries,
|
||||||
titleBuilder: (item) => Text(item.key),
|
titleBuilder: (item) => Text(item.key),
|
||||||
subtitleBuilder: (item) => Text(item.value),
|
subtitleBuilder: (item) => Text(item.value),
|
||||||
onChange: (items) {
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final dns = clashConfig.dns;
|
(state) => state.copyWith.dns(
|
||||||
clashConfig.dns = dns.copyWith(
|
nameserverPolicy: Map.fromEntries(items),
|
||||||
nameserverPolicy: Map.fromEntries(items),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -411,20 +369,22 @@ class ProxyServerNameserverItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.proxyNameserver,
|
title: appLocalizations.proxyNameserver,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver,
|
builder: (_, ref, __) {
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
final proxyServerNameserver = ref.watch(
|
||||||
builder: (_, proxyServerNameserver, __) {
|
patchClashConfigProvider
|
||||||
|
.select((state) => state.dns.proxyServerNameserver),
|
||||||
|
);
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.proxyNameserver,
|
title: appLocalizations.proxyNameserver,
|
||||||
items: proxyServerNameserver,
|
items: proxyServerNameserver,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items) {
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final dns = clashConfig.dns;
|
(state) => state.copyWith.dns(
|
||||||
clashConfig.dns = dns.copyWith(
|
proxyServerNameserver: List.from(items),
|
||||||
proxyServerNameserver: List.from(items),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -446,93 +406,80 @@ class FallbackItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.fallback,
|
title: appLocalizations.fallback,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallback,
|
final fallback = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider.select((state) => state.dns.fallback),
|
||||||
builder: (_, fallback, __) {
|
);
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.fallback,
|
title: appLocalizations.fallback,
|
||||||
items: fallback,
|
items: fallback,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items) {
|
onChange: (items) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
final dns = clashConfig.dns;
|
(state) => state.copyWith.dns(
|
||||||
clashConfig.dns = dns.copyWith(
|
fallback: List.from(items),
|
||||||
fallback: List.from(items),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeoipItem extends StatelessWidget {
|
class GeoipItem extends ConsumerWidget {
|
||||||
const GeoipItem({super.key});
|
const GeoipItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final geoip = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip,
|
patchClashConfigProvider
|
||||||
builder: (_, geoip, __) {
|
.select((state) => state.dns.fallbackFilter.geoip),
|
||||||
return ListItem.switchItem(
|
);
|
||||||
title: const Text("Geoip"),
|
return ListItem.switchItem(
|
||||||
delegate: SwitchDelegate(
|
title: const Text("Geoip"),
|
||||||
value: geoip,
|
delegate: SwitchDelegate(
|
||||||
onChanged: (bool value) async {
|
value: geoip,
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChanged: (bool value) async {
|
||||||
final dns = clashConfig.dns;
|
ref
|
||||||
clashConfig.dns = dns.copyWith(
|
.read(patchClashConfigProvider.notifier)
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(geoip: value),
|
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||||
);
|
geoip: value,
|
||||||
},
|
));
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeoipCodeItem extends StatelessWidget {
|
class GeoipCodeItem extends ConsumerWidget {
|
||||||
const GeoipCodeItem({super.key});
|
const GeoipCodeItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, String>(
|
final geoipCode = ref.watch(
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode,
|
patchClashConfigProvider
|
||||||
builder: (_, geoipCode, __) {
|
.select((state) => state.dns.fallbackFilter.geoipCode),
|
||||||
return ListItem.input(
|
);
|
||||||
title: Text(appLocalizations.geoipCode),
|
return ListItem.input(
|
||||||
subtitle: Text(geoipCode),
|
title: Text(appLocalizations.geoipCode),
|
||||||
delegate: InputDelegate(
|
subtitle: Text(geoipCode),
|
||||||
title: appLocalizations.geoipCode,
|
delegate: InputDelegate(
|
||||||
value: geoipCode,
|
title: appLocalizations.geoipCode,
|
||||||
onChanged: (String? value) {
|
value: geoipCode,
|
||||||
if (value != null) {
|
onChanged: (String? value) {
|
||||||
try {
|
if (value == null) {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
return;
|
||||||
final dns = clashConfig.dns;
|
}
|
||||||
clashConfig.dns = dns.copyWith(
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
(state) => state.copyWith.dns.fallbackFilter(
|
||||||
geoipCode: value,
|
geoipCode: value,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
},
|
||||||
globalState.showMessage(
|
),
|
||||||
title: appLocalizations.geoipCode,
|
|
||||||
message: TextSpan(
|
|
||||||
text: e.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,26 +494,24 @@ class GeositeItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: "Geosite",
|
title: "Geosite",
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite,
|
final geosite = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider
|
||||||
builder: (_, geosite, __) {
|
.select((state) => state.dns.fallbackFilter.geosite),
|
||||||
return ListPage(
|
);
|
||||||
title: "Geosite",
|
return ListPage(
|
||||||
items: geosite,
|
title: "Geosite",
|
||||||
titleBuilder: (item) => Text(item),
|
items: geosite,
|
||||||
onChange: (items) {
|
titleBuilder: (item) => Text(item),
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChange: (items) {
|
||||||
final dns = clashConfig.dns;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.dns = dns.copyWith(
|
(state) => state.copyWith.dns.fallbackFilter(
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
geosite: List.from(items),
|
||||||
geosite: List.from(items),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -583,26 +528,24 @@ class IpcidrItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.ipcidr,
|
title: appLocalizations.ipcidr,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, ___) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr,
|
final ipcidr = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider
|
||||||
builder: (_, ipcidr, __) {
|
.select((state) => state.dns.fallbackFilter.ipcidr),
|
||||||
return ListPage(
|
);
|
||||||
title: appLocalizations.ipcidr,
|
return ListPage(
|
||||||
items: ipcidr,
|
title: appLocalizations.ipcidr,
|
||||||
titleBuilder: (item) => Text(item),
|
items: ipcidr,
|
||||||
onChange: (items) {
|
titleBuilder: (item) => Text(item),
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChange: (items) {
|
||||||
final dns = clashConfig.dns;
|
ref
|
||||||
clashConfig.dns = dns.copyWith(
|
.read(patchClashConfigProvider.notifier)
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
.updateState((state) => state.copyWith.dns.fallbackFilter(
|
||||||
ipcidr: List.from(items),
|
ipcidr: List.from(items),
|
||||||
),
|
));
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -619,26 +562,24 @@ class DomainItem extends StatelessWidget {
|
|||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
isBlur: false,
|
isBlur: false,
|
||||||
title: appLocalizations.domain,
|
title: appLocalizations.domain,
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
widget: Consumer(builder: (_, ref, __) {
|
||||||
selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain,
|
final domain = ref.watch(
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
patchClashConfigProvider
|
||||||
builder: (_, domain, __) {
|
.select((state) => state.dns.fallbackFilter.domain),
|
||||||
return ListPage(
|
);
|
||||||
title: appLocalizations.domain,
|
return ListPage(
|
||||||
items: domain,
|
title: appLocalizations.domain,
|
||||||
titleBuilder: (item) => Text(item),
|
items: domain,
|
||||||
onChange: (items) {
|
titleBuilder: (item) => Text(item),
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
onChange: (items) {
|
||||||
final dns = clashConfig.dns;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.dns = dns.copyWith(
|
(state) => state.copyWith.dns.fallbackFilter(
|
||||||
fallbackFilter: dns.fallbackFilter.copyWith(
|
domain: List.from(items),
|
||||||
domain: List.from(items),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
);
|
}),
|
||||||
},
|
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -700,14 +641,12 @@ const dnsItems = <Widget>[
|
|||||||
FallbackFilterOptions(),
|
FallbackFilterOptions(),
|
||||||
];
|
];
|
||||||
|
|
||||||
class DnsListView extends StatelessWidget {
|
class DnsListView extends ConsumerWidget {
|
||||||
const DnsListView({super.key});
|
const DnsListView({super.key});
|
||||||
|
|
||||||
_initActions(BuildContext context) {
|
_initActions(BuildContext context, WidgetRef ref) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
final commonScaffoldState =
|
context.commonScaffoldState?.actions = [
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final res = await globalState.showMessage(
|
final res = await globalState.showMessage(
|
||||||
@@ -719,7 +658,12 @@ class DnsListView extends StatelessWidget {
|
|||||||
if (res != true) {
|
if (res != true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
globalState.appController.clashConfig.dns = defaultDns;
|
|
||||||
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
dns: defaultDns,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
tooltip: appLocalizations.reset,
|
tooltip: appLocalizations.reset,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@@ -731,8 +675,8 @@ class DnsListView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
_initActions(context);
|
_initActions(context, ref);
|
||||||
return generateListView(
|
return generateListView(
|
||||||
dnsItems,
|
dnsItems,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -3,200 +3,185 @@ import 'dart:io';
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class VPNItem extends StatelessWidget {
|
class VPNItem extends ConsumerWidget {
|
||||||
const VPNItem({super.key});
|
const VPNItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final enable =
|
||||||
selector: (_, config) => config.vpnProps.enable,
|
ref.watch(vpnSettingProvider.select((state) => state.enable));
|
||||||
builder: (_, enable, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
title: const Text("VPN"),
|
||||||
title: const Text("VPN"),
|
subtitle: Text(appLocalizations.vpnEnableDesc),
|
||||||
subtitle: Text(appLocalizations.vpnEnableDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: enable,
|
||||||
value: enable,
|
onChanged: (value) async {
|
||||||
onChanged: (value) async {
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
final config = globalState.appController.config;
|
(state) => state.copyWith(
|
||||||
config.vpnProps = config.vpnProps.copyWith(
|
enable: value,
|
||||||
enable: value,
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TUNItem extends StatelessWidget {
|
class TUNItem extends ConsumerWidget {
|
||||||
const TUNItem({super.key});
|
const TUNItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final enable =
|
||||||
selector: (_, clashConfig) => clashConfig.tun.enable,
|
ref.watch(patchClashConfigProvider.select((state) => state.tun.enable));
|
||||||
builder: (_, enable, __) {
|
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.tun),
|
title: Text(appLocalizations.tun),
|
||||||
subtitle: Text(appLocalizations.tunDesc),
|
subtitle: Text(appLocalizations.tunDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: enable,
|
value: enable,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.tun = clashConfig.tun.copyWith(
|
(state) => state.copyWith.tun(
|
||||||
enable: value,
|
enable: value,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AllowBypassItem extends StatelessWidget {
|
class AllowBypassItem extends ConsumerWidget {
|
||||||
const AllowBypassItem({super.key});
|
const AllowBypassItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final allowBypass =
|
||||||
selector: (_, config) => config.vpnProps.allowBypass,
|
ref.watch(vpnSettingProvider.select((state) => state.allowBypass));
|
||||||
builder: (_, allowBypass, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
title: Text(appLocalizations.allowBypass),
|
||||||
title: Text(appLocalizations.allowBypass),
|
subtitle: Text(appLocalizations.allowBypassDesc),
|
||||||
subtitle: Text(appLocalizations.allowBypassDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: allowBypass,
|
||||||
value: allowBypass,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
final config = globalState.appController.config;
|
(state) => state.copyWith(
|
||||||
final vpnProps = config.vpnProps;
|
allowBypass: value,
|
||||||
config.vpnProps = vpnProps.copyWith(
|
),
|
||||||
allowBypass: value,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VpnSystemProxyItem extends StatelessWidget {
|
class VpnSystemProxyItem extends ConsumerWidget {
|
||||||
const VpnSystemProxyItem({super.key});
|
const VpnSystemProxyItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final systemProxy =
|
||||||
selector: (_, config) => config.vpnProps.systemProxy,
|
ref.watch(vpnSettingProvider.select((state) => state.systemProxy));
|
||||||
builder: (_, systemProxy, __) {
|
return ListItem.switchItem(
|
||||||
return ListItem.switchItem(
|
title: Text(appLocalizations.systemProxy),
|
||||||
title: Text(appLocalizations.systemProxy),
|
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
delegate: SwitchDelegate(
|
||||||
delegate: SwitchDelegate(
|
value: systemProxy,
|
||||||
value: systemProxy,
|
onChanged: (bool value) async {
|
||||||
onChanged: (bool value) async {
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
final config = globalState.appController.config;
|
(state) => state.copyWith(
|
||||||
final vpnProps = config.vpnProps;
|
systemProxy: value,
|
||||||
config.vpnProps = vpnProps.copyWith(
|
),
|
||||||
systemProxy: value,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemProxyItem extends StatelessWidget {
|
class SystemProxyItem extends ConsumerWidget {
|
||||||
const SystemProxyItem({super.key});
|
const SystemProxyItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final systemProxy =
|
||||||
selector: (_, config) => config.networkProps.systemProxy,
|
ref.watch(networkSettingProvider.select((state) => state.systemProxy));
|
||||||
builder: (_, systemProxy, __) {
|
|
||||||
return ListItem.switchItem(
|
return ListItem.switchItem(
|
||||||
title: Text(appLocalizations.systemProxy),
|
title: Text(appLocalizations.systemProxy),
|
||||||
subtitle: Text(appLocalizations.systemProxyDesc),
|
subtitle: Text(appLocalizations.systemProxyDesc),
|
||||||
delegate: SwitchDelegate(
|
delegate: SwitchDelegate(
|
||||||
value: systemProxy,
|
value: systemProxy,
|
||||||
onChanged: (bool value) async {
|
onChanged: (bool value) async {
|
||||||
final config = globalState.appController.config;
|
ref.read(networkSettingProvider.notifier).updateState(
|
||||||
final networkProps = config.networkProps;
|
(state) => state.copyWith(
|
||||||
config.networkProps = networkProps.copyWith(
|
systemProxy: value,
|
||||||
systemProxy: value,
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Ipv6Item extends StatelessWidget {
|
class Ipv6Item extends ConsumerWidget {
|
||||||
const Ipv6Item({super.key});
|
const Ipv6Item({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<Config, bool>(
|
final ipv6 = ref.watch(vpnSettingProvider.select((state) => state.ipv6));
|
||||||
selector: (_, config) => config.vpnProps.ipv6,
|
return ListItem.switchItem(
|
||||||
builder: (_, ipv6, __) {
|
title: const Text("IPv6"),
|
||||||
return ListItem.switchItem(
|
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
||||||
title: const Text("IPv6"),
|
delegate: SwitchDelegate(
|
||||||
subtitle: Text(appLocalizations.ipv6InboundDesc),
|
value: ipv6,
|
||||||
delegate: SwitchDelegate(
|
onChanged: (bool value) async {
|
||||||
value: ipv6,
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
onChanged: (bool value) async {
|
(state) => state.copyWith(
|
||||||
final config = globalState.appController.config;
|
ipv6: value,
|
||||||
final vpnProps = config.vpnProps;
|
),
|
||||||
config.vpnProps = vpnProps.copyWith(
|
|
||||||
ipv6: value,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TunStackItem extends StatelessWidget {
|
class TunStackItem extends ConsumerWidget {
|
||||||
const TunStackItem({super.key});
|
const TunStackItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, TunStack>(
|
final stack =
|
||||||
selector: (_, clashConfig) => clashConfig.tun.stack,
|
ref.watch(patchClashConfigProvider.select((state) => state.tun.stack));
|
||||||
builder: (_, stack, __) {
|
|
||||||
return ListItem.options(
|
return ListItem.options(
|
||||||
title: Text(appLocalizations.stackMode),
|
title: Text(appLocalizations.stackMode),
|
||||||
subtitle: Text(stack.name),
|
subtitle: Text(stack.name),
|
||||||
delegate: OptionsDelegate<TunStack>(
|
delegate: OptionsDelegate<TunStack>(
|
||||||
value: stack,
|
value: stack,
|
||||||
options: TunStack.values,
|
options: TunStack.values,
|
||||||
textBuilder: (value) => value.name,
|
textBuilder: (value) => value.name,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
clashConfig.tun = clashConfig.tun.copyWith(
|
(state) => state.copyWith.tun(
|
||||||
stack: value,
|
stack: value,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
title: appLocalizations.stackMode,
|
title: appLocalizations.stackMode,
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +189,9 @@ class TunStackItem extends StatelessWidget {
|
|||||||
class BypassDomainItem extends StatelessWidget {
|
class BypassDomainItem extends StatelessWidget {
|
||||||
const BypassDomainItem({super.key});
|
const BypassDomainItem({super.key});
|
||||||
|
|
||||||
_initActions(BuildContext context) {
|
_initActions(BuildContext context, WidgetRef ref) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
final commonScaffoldState =
|
context.commonScaffoldState?.actions = [
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final res = await globalState.showMessage(
|
final res = await globalState.showMessage(
|
||||||
@@ -220,10 +203,11 @@ class BypassDomainItem extends StatelessWidget {
|
|||||||
if (res != true) {
|
if (res != true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final config = globalState.appController.config;
|
ref.read(networkSettingProvider.notifier).updateState(
|
||||||
config.networkProps = config.networkProps.copyWith(
|
(state) => state.copyWith(
|
||||||
bypassDomain: defaultBypassDomain,
|
bypassDomain: defaultBypassDomain,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
tooltip: appLocalizations.reset,
|
tooltip: appLocalizations.reset,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@@ -243,20 +227,21 @@ class BypassDomainItem extends StatelessWidget {
|
|||||||
isBlur: false,
|
isBlur: false,
|
||||||
isScaffold: true,
|
isScaffold: true,
|
||||||
title: appLocalizations.bypassDomain,
|
title: appLocalizations.bypassDomain,
|
||||||
widget: Selector<Config, List<String>>(
|
widget: Consumer(
|
||||||
selector: (_, config) => config.networkProps.bypassDomain,
|
builder: (_, ref, __) {
|
||||||
shouldRebuild: (prev, next) => !stringListEquality.equals(prev, next),
|
_initActions(context, ref);
|
||||||
builder: (context, bypassDomain, __) {
|
final bypassDomain = ref.watch(
|
||||||
_initActions(context);
|
networkSettingProvider.select((state) => state.bypassDomain));
|
||||||
return ListPage(
|
return ListPage(
|
||||||
title: appLocalizations.bypassDomain,
|
title: appLocalizations.bypassDomain,
|
||||||
items: bypassDomain,
|
items: bypassDomain,
|
||||||
titleBuilder: (item) => Text(item),
|
titleBuilder: (item) => Text(item),
|
||||||
onChange: (items) {
|
onChange: (items) {
|
||||||
final config = globalState.appController.config;
|
ref.read(networkSettingProvider.notifier).updateState(
|
||||||
config.networkProps = config.networkProps.copyWith(
|
(state) => state.copyWith(
|
||||||
bypassDomain: List.from(items),
|
bypassDomain: List.from(items),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -267,77 +252,74 @@ class BypassDomainItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RouteModeItem extends StatelessWidget {
|
class RouteModeItem extends ConsumerWidget {
|
||||||
const RouteModeItem({super.key});
|
const RouteModeItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, RouteMode>(
|
final routeMode =
|
||||||
selector: (_, clashConfig) => clashConfig.routeMode,
|
ref.watch(networkSettingProvider.select((state) => state.routeMode));
|
||||||
builder: (_, value, __) {
|
return ListItem<RouteMode>.options(
|
||||||
return ListItem<RouteMode>.options(
|
title: Text(appLocalizations.routeMode),
|
||||||
title: Text(appLocalizations.routeMode),
|
subtitle: Text(Intl.message("routeMode_${routeMode.name}")),
|
||||||
subtitle: Text(Intl.message("routeMode_${value.name}")),
|
delegate: OptionsDelegate<RouteMode>(
|
||||||
delegate: OptionsDelegate<RouteMode>(
|
title: appLocalizations.routeMode,
|
||||||
title: appLocalizations.routeMode,
|
options: RouteMode.values,
|
||||||
options: RouteMode.values,
|
onChanged: (RouteMode? value) {
|
||||||
onChanged: (RouteMode? value) {
|
if (value == null) {
|
||||||
if (value == null) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
ref.read(networkSettingProvider.notifier).updateState(
|
||||||
final appController = globalState.appController;
|
(state) => state.copyWith(
|
||||||
appController.clashConfig.routeMode = value;
|
routeMode: value,
|
||||||
},
|
),
|
||||||
textBuilder: (routeMode) => Intl.message(
|
);
|
||||||
"routeMode_${routeMode.name}",
|
},
|
||||||
),
|
textBuilder: (routeMode) => Intl.message(
|
||||||
value: value,
|
"routeMode_${routeMode.name}",
|
||||||
),
|
),
|
||||||
);
|
value: routeMode,
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RouteAddressItem extends StatelessWidget {
|
class RouteAddressItem extends ConsumerWidget {
|
||||||
const RouteAddressItem({super.key});
|
const RouteAddressItem({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
return Selector<ClashConfig, bool>(
|
final bypassPrivate = ref.watch(networkSettingProvider
|
||||||
selector: (_, clashConfig) => clashConfig.routeMode == RouteMode.config,
|
.select((state) => state.routeMode == RouteMode.bypassPrivate));
|
||||||
builder: (_, value, child) {
|
if (bypassPrivate) {
|
||||||
if (value) {
|
return Container();
|
||||||
return child!;
|
}
|
||||||
}
|
return ListItem.open(
|
||||||
return Container();
|
title: Text(appLocalizations.routeAddress),
|
||||||
},
|
subtitle: Text(appLocalizations.routeAddressDesc),
|
||||||
child: ListItem.open(
|
delegate: OpenDelegate(
|
||||||
title: Text(appLocalizations.routeAddress),
|
isBlur: false,
|
||||||
subtitle: Text(appLocalizations.routeAddressDesc),
|
isScaffold: true,
|
||||||
delegate: OpenDelegate(
|
title: appLocalizations.routeAddress,
|
||||||
isBlur: false,
|
widget: Consumer(
|
||||||
isScaffold: true,
|
builder: (_, ref, __) {
|
||||||
title: appLocalizations.routeAddress,
|
final routeAddress = ref.watch(patchClashConfigProvider
|
||||||
widget: Selector<ClashConfig, List<String>>(
|
.select((state) => state.tun.routeAddress));
|
||||||
selector: (_, clashConfig) => clashConfig.includeRouteAddress,
|
return ListPage(
|
||||||
shouldRebuild: (prev, next) =>
|
title: appLocalizations.routeAddress,
|
||||||
!stringListEquality.equals(prev, next),
|
items: routeAddress,
|
||||||
builder: (context, routeAddress, __) {
|
titleBuilder: (item) => Text(item),
|
||||||
return ListPage(
|
onChange: (items) {
|
||||||
title: appLocalizations.routeAddress,
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
items: routeAddress,
|
(state) => state.copyWith.tun(
|
||||||
titleBuilder: (item) => Text(item),
|
routeAddress: List.from(items),
|
||||||
onChange: (items) {
|
),
|
||||||
final clashConfig = globalState.appController.clashConfig;
|
);
|
||||||
clashConfig.includeRouteAddress =
|
},
|
||||||
Set<String>.from(items).toList();
|
);
|
||||||
},
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
extendPageWidth: 360,
|
|
||||||
),
|
),
|
||||||
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -349,7 +331,8 @@ final networkItems = [
|
|||||||
...generateSection(
|
...generateSection(
|
||||||
title: "VPN",
|
title: "VPN",
|
||||||
items: [
|
items: [
|
||||||
const SystemProxyItem(),
|
const VpnSystemProxyItem(),
|
||||||
|
const BypassDomainItem(),
|
||||||
const AllowBypassItem(),
|
const AllowBypassItem(),
|
||||||
const Ipv6Item(),
|
const Ipv6Item(),
|
||||||
],
|
],
|
||||||
@@ -373,14 +356,12 @@ final networkItems = [
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
class NetworkListView extends StatelessWidget {
|
class NetworkListView extends ConsumerWidget {
|
||||||
const NetworkListView({super.key});
|
const NetworkListView({super.key});
|
||||||
|
|
||||||
_initActions(BuildContext context) {
|
_initActions(BuildContext context, WidgetRef ref) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
final commonScaffoldState =
|
context.commonScaffoldState?.actions = [
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final res = await globalState.showMessage(
|
final res = await globalState.showMessage(
|
||||||
@@ -392,9 +373,14 @@ class NetworkListView extends StatelessWidget {
|
|||||||
if (res != true) {
|
if (res != true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final appController = globalState.appController;
|
ref.read(vpnSettingProvider.notifier).updateState(
|
||||||
appController.config.vpnProps = defaultVpnProps;
|
(state) => defaultVpnProps,
|
||||||
appController.clashConfig.tun = defaultTun;
|
);
|
||||||
|
ref.read(patchClashConfigProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
tun: defaultTun,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
tooltip: appLocalizations.reset,
|
tooltip: appLocalizations.reset,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@@ -406,8 +392,8 @@ class NetworkListView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, ref) {
|
||||||
_initActions(context);
|
_initActions(context, ref);
|
||||||
return generateListView(
|
return generateListView(
|
||||||
networkItems,
|
networkItems,
|
||||||
);
|
);
|
||||||
|
|||||||
150
lib/fragments/connection/connections.dart
Normal file
150
lib/fragments/connection/connections.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fl_clash/clash/clash.dart';
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'item.dart';
|
||||||
|
|
||||||
|
class ConnectionsFragment extends ConsumerStatefulWidget {
|
||||||
|
const ConnectionsFragment({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConnectionsFragment> createState() =>
|
||||||
|
_ConnectionsFragmentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectionsFragmentState extends ConsumerState<ConnectionsFragment>
|
||||||
|
with PageMixin {
|
||||||
|
final _connectionsStateNotifier = ValueNotifier<ConnectionsState>(
|
||||||
|
const ConnectionsState(),
|
||||||
|
);
|
||||||
|
final ScrollController _scrollController = ScrollController(
|
||||||
|
keepScrollOffset: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
Timer? timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> get actions => [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
clashCore.closeConnections();
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(
|
||||||
|
connections: await clashCore.getConnections(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete_sweep_outlined),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onSearch => (value) {
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(
|
||||||
|
query: value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onKeywordsUpdate => (keywords) {
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(keywords: keywords);
|
||||||
|
};
|
||||||
|
|
||||||
|
_updateConnections() async {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
_connectionsStateNotifier.value =
|
||||||
|
_connectionsStateNotifier.value.copyWith(
|
||||||
|
connections: await clashCore.getConnections(),
|
||||||
|
);
|
||||||
|
timer = Timer(Duration(seconds: 1), () async {
|
||||||
|
_updateConnections();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
ref.listenManual(
|
||||||
|
isCurrentPageProvider(
|
||||||
|
PageLabel.connections,
|
||||||
|
handler: (pageLabel, viewMode) =>
|
||||||
|
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
||||||
|
),
|
||||||
|
(prev, next) {
|
||||||
|
if (prev != next && next == true) {
|
||||||
|
initPageState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
_updateConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleBlockConnection(String id) async {
|
||||||
|
clashCore.closeConnection(id);
|
||||||
|
_connectionsStateNotifier.value = _connectionsStateNotifier.value.copyWith(
|
||||||
|
connections: await clashCore.getConnections(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
timer?.cancel();
|
||||||
|
_connectionsStateNotifier.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
timer = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<ConnectionsState>(
|
||||||
|
valueListenable: _connectionsStateNotifier,
|
||||||
|
builder: (_, state, __) {
|
||||||
|
final connections = state.list;
|
||||||
|
if (connections.isEmpty) {
|
||||||
|
return NullStatus(
|
||||||
|
label: appLocalizations.nullConnectionsDesc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CommonScrollBar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.separated(
|
||||||
|
controller: _scrollController,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final connection = connections[index];
|
||||||
|
return ConnectionItem(
|
||||||
|
key: Key(connection.id),
|
||||||
|
connection: connection,
|
||||||
|
onClick: (value) {
|
||||||
|
context.commonScaffoldState?.addKeyword(value);
|
||||||
|
},
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.block),
|
||||||
|
onPressed: () {
|
||||||
|
_handleBlockConnection(connection.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
|
return const Divider(
|
||||||
|
height: 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: connections.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
lib/fragments/connection/item.dart
Normal file
155
lib/fragments/connection/item.dart
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/plugins/app.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class FindProcessBuilder extends StatelessWidget {
|
||||||
|
final Widget Function(bool value) builder;
|
||||||
|
|
||||||
|
const FindProcessBuilder({
|
||||||
|
super.key,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer(
|
||||||
|
builder: (_, ref, __) {
|
||||||
|
final value = ref.watch(
|
||||||
|
patchClashConfigProvider.select(
|
||||||
|
(state) =>
|
||||||
|
state.findProcessMode == FindProcessMode.always &&
|
||||||
|
Platform.isAndroid,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return builder(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionItem extends StatelessWidget {
|
||||||
|
final Connection connection;
|
||||||
|
final Function(String)? onClick;
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
|
const ConnectionItem({
|
||||||
|
super.key,
|
||||||
|
required this.connection,
|
||||||
|
this.onClick,
|
||||||
|
this.trailing,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<ImageProvider?> _getPackageIcon(Connection connection) async {
|
||||||
|
return await app?.getPackageIcon(connection.metadata.process);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSourceText(Connection connection) {
|
||||||
|
final metadata = connection.metadata;
|
||||||
|
if (metadata.process.isEmpty) {
|
||||||
|
return connection.start.lastUpdateTimeDesc;
|
||||||
|
}
|
||||||
|
return "${metadata.process} · ${connection.start.lastUpdateTimeDesc}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final title = Text(
|
||||||
|
connection.desc,
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
);
|
||||||
|
final subTitle = Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_getSourceText(connection),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 8,
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
runSpacing: 6,
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
for (final chain in connection.chains)
|
||||||
|
CommonChip(
|
||||||
|
label: chain,
|
||||||
|
onPressed: () {
|
||||||
|
if (onClick == null) return;
|
||||||
|
onClick!(chain);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return ListItem(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||||
|
title: title,
|
||||||
|
subtitle: subTitle,
|
||||||
|
trailing: trailing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return FindProcessBuilder(
|
||||||
|
builder: (bool value) {
|
||||||
|
final leading = value
|
||||||
|
? GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (onClick == null) return;
|
||||||
|
final process = connection.metadata.process;
|
||||||
|
if (process.isEmpty) return;
|
||||||
|
onClick!(process);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 4),
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
child: FutureBuilder<ImageProvider?>(
|
||||||
|
future: _getPackageIcon(connection),
|
||||||
|
builder: (_, snapshot) {
|
||||||
|
if (!snapshot.hasData && snapshot.data == null) {
|
||||||
|
return Container();
|
||||||
|
} else {
|
||||||
|
return Image(
|
||||||
|
image: snapshot.data!,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
return ListItem(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
tileTitleAlignment: ListTileTitleAlignment.titleHeight,
|
||||||
|
leading: leading,
|
||||||
|
title: title,
|
||||||
|
subtitle: subTitle,
|
||||||
|
trailing: trailing,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
212
lib/fragments/connection/requests.dart
Normal file
212
lib/fragments/connection/requests.dart
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
|
import 'package:fl_clash/state.dart';
|
||||||
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'item.dart';
|
||||||
|
|
||||||
|
double _preOffset = 0;
|
||||||
|
|
||||||
|
class RequestsFragment extends ConsumerStatefulWidget {
|
||||||
|
const RequestsFragment({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<RequestsFragment> createState() => _RequestsFragmentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RequestsFragmentState extends ConsumerState<RequestsFragment>
|
||||||
|
with PageMixin {
|
||||||
|
final _requestsStateNotifier =
|
||||||
|
ValueNotifier<ConnectionsState>(const ConnectionsState());
|
||||||
|
List<Connection> _requests = [];
|
||||||
|
|
||||||
|
final ScrollController _scrollController = ScrollController(
|
||||||
|
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
|
||||||
|
);
|
||||||
|
|
||||||
|
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
|
||||||
|
|
||||||
|
double _currentMaxWidth = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onSearch => (value) {
|
||||||
|
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||||
|
query: value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onKeywordsUpdate => (keywords) {
|
||||||
|
_requestsStateNotifier.value =
|
||||||
|
_requestsStateNotifier.value.copyWith(keywords: keywords);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||||
|
connections: globalState.appState.requests.list,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.listenManual(
|
||||||
|
isCurrentPageProvider(
|
||||||
|
PageLabel.requests,
|
||||||
|
handler: (pageLabel, viewMode) =>
|
||||||
|
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
||||||
|
),
|
||||||
|
(prev, next) {
|
||||||
|
if (prev != next && next == true) {
|
||||||
|
initPageState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
ref.listenManual(
|
||||||
|
requestsProvider.select((state) => state.list),
|
||||||
|
(prev, next) {
|
||||||
|
if (!connectionListEquality.equals(prev, next)) {
|
||||||
|
_requests = next;
|
||||||
|
updateRequestsThrottler();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calcCacheHeight(Connection item) {
|
||||||
|
final cacheHeight = _cacheDynamicHeightMap.get(item.id);
|
||||||
|
if (cacheHeight != null) {
|
||||||
|
return cacheHeight;
|
||||||
|
}
|
||||||
|
final size = globalState.measure.computeTextSize(
|
||||||
|
Text(
|
||||||
|
item.desc,
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
maxWidth: _currentMaxWidth,
|
||||||
|
);
|
||||||
|
final chainsText = item.chains.join("");
|
||||||
|
final length = item.chains.length;
|
||||||
|
final chainSize = globalState.measure.computeTextSize(
|
||||||
|
Text(
|
||||||
|
chainsText,
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
maxWidth: (_currentMaxWidth - (length - 1) * 6 - length * 24),
|
||||||
|
);
|
||||||
|
final baseHeight = globalState.measure.bodyMediumHeight;
|
||||||
|
final lines = (chainSize.height / baseHeight).round();
|
||||||
|
final computerHeight =
|
||||||
|
size.height + chainSize.height + 24 + 24 * (lines - 1);
|
||||||
|
_cacheDynamicHeightMap.put(item.id, computerHeight);
|
||||||
|
return computerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleTryClearCache(double maxWidth) {
|
||||||
|
if (_currentMaxWidth != maxWidth) {
|
||||||
|
_currentMaxWidth = maxWidth;
|
||||||
|
_cacheDynamicHeightMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_requestsStateNotifier.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
_currentMaxWidth = 0;
|
||||||
|
_cacheDynamicHeightMap.clear();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRequestsThrottler() {
|
||||||
|
throttler.call("request", () {
|
||||||
|
final isEquality = connectionListEquality.equals(
|
||||||
|
_requests,
|
||||||
|
_requestsStateNotifier.value.connections,
|
||||||
|
);
|
||||||
|
if (isEquality) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_requestsStateNotifier.value = _requestsStateNotifier.value.copyWith(
|
||||||
|
connections: _requests,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, duration: commonDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (_, constraints) {
|
||||||
|
return FindProcessBuilder(builder: (value) {
|
||||||
|
_handleTryClearCache(constraints.maxWidth - 40 - (value ? 60 : 0));
|
||||||
|
return ValueListenableBuilder<ConnectionsState>(
|
||||||
|
valueListenable: _requestsStateNotifier,
|
||||||
|
builder: (_, state, __) {
|
||||||
|
final connections = state.list;
|
||||||
|
if (connections.isEmpty) {
|
||||||
|
return NullStatus(
|
||||||
|
label: appLocalizations.nullRequestsDesc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final items = connections
|
||||||
|
.map<Widget>(
|
||||||
|
(connection) => ConnectionItem(
|
||||||
|
key: Key(connection.id),
|
||||||
|
connection: connection,
|
||||||
|
onClick: (value) {
|
||||||
|
context.commonScaffoldState?.addKeyword(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.separated(
|
||||||
|
const Divider(
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: NotificationListener<ScrollEndNotification>(
|
||||||
|
onNotification: (details) {
|
||||||
|
_preOffset = details.metrics.pixels;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: CommonScrollBar(
|
||||||
|
controller: _scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
reverse: true,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: NextClampingScrollPhysics(),
|
||||||
|
controller: _scrollController,
|
||||||
|
itemExtentBuilder: (index, __) {
|
||||||
|
final widget = items[index];
|
||||||
|
if (widget.runtimeType == Divider) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
final measure = globalState.measure;
|
||||||
|
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||||
|
final connection = connections[(index / 2).floor()];
|
||||||
|
final height = _calcCacheHeight(connection);
|
||||||
|
return height + bodyMediumHeight + 32;
|
||||||
|
},
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
return items[index];
|
||||||
|
},
|
||||||
|
itemCount: items.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class ConnectionsFragment extends StatefulWidget {
|
|
||||||
const ConnectionsFragment({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ConnectionsFragment> createState() => _ConnectionsFragmentState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ConnectionsFragmentState extends State<ConnectionsFragment> {
|
|
||||||
final connectionsNotifier =
|
|
||||||
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
|
|
||||||
final ScrollController _scrollController = ScrollController(
|
|
||||||
keepScrollOffset: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
Timer? timer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
connections: await clashCore.getConnections(),
|
|
||||||
);
|
|
||||||
if (timer != null) {
|
|
||||||
timer?.cancel();
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
timer = Timer.periodic(
|
|
||||||
const Duration(seconds: 1),
|
|
||||||
(timer) async {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
connections: await clashCore.getConnections(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_initActions() {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) {
|
|
||||||
final commonScaffoldState =
|
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
showSearch(
|
|
||||||
context: context,
|
|
||||||
delegate: ConnectionsSearchDelegate(
|
|
||||||
state: connectionsNotifier.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.search),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
clashCore.closeConnections();
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
connections: await clashCore.getConnections(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.delete_sweep_outlined),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_addKeyword(String keyword) {
|
|
||||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (isContains) return;
|
|
||||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
|
||||||
..add(keyword);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_deleteKeyword(String keyword) {
|
|
||||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (!isContains) return;
|
|
||||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
|
||||||
..remove(keyword);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleBlockConnection(String id) async {
|
|
||||||
clashCore.closeConnection(id);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
connections: await clashCore.getConnections(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
timer?.cancel();
|
|
||||||
connectionsNotifier.dispose();
|
|
||||||
_scrollController.dispose();
|
|
||||||
timer = null;
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<AppState, bool?>(
|
|
||||||
selector: (_, appState) =>
|
|
||||||
appState.currentLabel == 'connections' ||
|
|
||||||
appState.viewMode == ViewMode.mobile &&
|
|
||||||
appState.currentLabel == "tools",
|
|
||||||
builder: (_, isCurrent, child) {
|
|
||||||
if (isCurrent == null || isCurrent) {
|
|
||||||
_initActions();
|
|
||||||
}
|
|
||||||
return child!;
|
|
||||||
},
|
|
||||||
child: ValueListenableBuilder<ConnectionsAndKeywords>(
|
|
||||||
valueListenable: connectionsNotifier,
|
|
||||||
builder: (_, state, __) {
|
|
||||||
var connections = state.filteredConnections;
|
|
||||||
if (connections.isEmpty) {
|
|
||||||
return NullStatus(
|
|
||||||
label: appLocalizations.nullConnectionsDesc,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
connections = connections.reversed.toList();
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (state.keywords.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 6,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
for (final keyword in state.keywords)
|
|
||||||
CommonChip(
|
|
||||||
label: keyword,
|
|
||||||
type: ChipType.delete,
|
|
||||||
onPressed: () {
|
|
||||||
_deleteKeyword(keyword);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.separated(
|
|
||||||
controller: _scrollController,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final connection = connections[index];
|
|
||||||
return ConnectionItem(
|
|
||||||
key: Key(connection.id),
|
|
||||||
connection: connection,
|
|
||||||
onClick: _addKeyword,
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Icons.block),
|
|
||||||
onPressed: () {
|
|
||||||
_handleBlockConnection(connection.id);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
|
||||||
return const Divider(
|
|
||||||
height: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: connections.length,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConnectionsSearchDelegate extends SearchDelegate {
|
|
||||||
ValueNotifier<ConnectionsAndKeywords> connectionsNotifier;
|
|
||||||
|
|
||||||
ConnectionsSearchDelegate({
|
|
||||||
required ConnectionsAndKeywords state,
|
|
||||||
}) : connectionsNotifier = ValueNotifier<ConnectionsAndKeywords>(state);
|
|
||||||
|
|
||||||
get state => connectionsNotifier.value;
|
|
||||||
|
|
||||||
List<Connection> get _results {
|
|
||||||
final lowerQuery = query.toLowerCase().trim();
|
|
||||||
return connectionsNotifier.value.filteredConnections.where((request) {
|
|
||||||
final lowerNetwork = request.metadata.network.toLowerCase();
|
|
||||||
final lowerHost = request.metadata.host.toLowerCase();
|
|
||||||
final lowerDestinationIP = request.metadata.destinationIP.toLowerCase();
|
|
||||||
final lowerProcess = request.metadata.process.toLowerCase();
|
|
||||||
final lowerChains = request.chains.join("").toLowerCase();
|
|
||||||
return lowerNetwork.contains(lowerQuery) ||
|
|
||||||
lowerHost.contains(lowerQuery) ||
|
|
||||||
lowerDestinationIP.contains(lowerQuery) ||
|
|
||||||
lowerProcess.contains(lowerQuery) ||
|
|
||||||
lowerChains.contains(lowerQuery);
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
_addKeyword(String keyword) {
|
|
||||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (isContains) return;
|
|
||||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
|
||||||
..add(keyword);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_deleteKeyword(String keyword) {
|
|
||||||
final isContains = connectionsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (!isContains) return;
|
|
||||||
final keywords = List<String>.from(connectionsNotifier.value.keywords)
|
|
||||||
..remove(keyword);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleBlockConnection(String id) async {
|
|
||||||
clashCore.closeConnection(id);
|
|
||||||
connectionsNotifier.value = connectionsNotifier.value.copyWith(
|
|
||||||
connections: await clashCore.getConnections(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget>? buildActions(BuildContext context) {
|
|
||||||
return [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (query.isEmpty) {
|
|
||||||
close(context, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
query = '';
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget? buildLeading(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
close(context, null);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildResults(BuildContext context) {
|
|
||||||
return buildSuggestions(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
connectionsNotifier.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildSuggestions(BuildContext context) {
|
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable: connectionsNotifier,
|
|
||||||
builder: (_, __, ___) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (state.keywords.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 6,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
for (final keyword in state.keywords)
|
|
||||||
CommonChip(
|
|
||||||
label: keyword,
|
|
||||||
type: ChipType.delete,
|
|
||||||
onPressed: () {
|
|
||||||
_deleteKeyword(keyword);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.separated(
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final connection = _results[index];
|
|
||||||
return ConnectionItem(
|
|
||||||
key: Key(connection.id),
|
|
||||||
connection: connection,
|
|
||||||
onClick: _addKeyword,
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Icons.block),
|
|
||||||
onPressed: () {
|
|
||||||
_handleBlockConnection(connection.id);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
|
||||||
return const Divider(
|
|
||||||
height: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: _results.length,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,43 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'widgets/start_button.dart';
|
import 'widgets/start_button.dart';
|
||||||
|
|
||||||
class DashboardFragment extends StatefulWidget {
|
class DashboardFragment extends ConsumerStatefulWidget {
|
||||||
const DashboardFragment({super.key});
|
const DashboardFragment({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DashboardFragment> createState() => _DashboardFragmentState();
|
ConsumerState<DashboardFragment> createState() => _DashboardFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DashboardFragmentState extends State<DashboardFragment> {
|
class _DashboardFragmentState extends ConsumerState<DashboardFragment>
|
||||||
|
with PageMixin {
|
||||||
final key = GlobalKey<SuperGridState>();
|
final key = GlobalKey<SuperGridState>();
|
||||||
|
|
||||||
_initScaffold(bool isCurrent) {
|
@override
|
||||||
if (!isCurrent) {
|
initState() {
|
||||||
return;
|
ref.listenManual(
|
||||||
}
|
isCurrentPageProvider(PageLabel.dashboard),
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
(prev, next) {
|
||||||
final commonScaffoldState =
|
if (prev != next && next == true) {
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
initPageState();
|
||||||
commonScaffoldState?.floatingActionButton = const StartButton();
|
}
|
||||||
commonScaffoldState?.actions = [
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
return super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? get floatingActionButton => const StartButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> get actions => [
|
||||||
ValueListenableBuilder(
|
ValueListenableBuilder(
|
||||||
valueListenable: key.currentState!.addedChildrenNotifier,
|
valueListenable: key.currentState!.addedChildrenNotifier,
|
||||||
builder: (_, addedChildren, child) {
|
builder: (_, addedChildren, child) {
|
||||||
@@ -67,72 +78,59 @@ class _DashboardFragmentState extends State<DashboardFragment> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
});
|
|
||||||
|
_handleSave(List<GridItem> girdItems, WidgetRef ref) {
|
||||||
|
final dashboardWidgets = girdItems
|
||||||
|
.map(
|
||||||
|
(item) => DashboardWidget.getDashboardWidget(item),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ActiveBuilder(
|
final dashboardState = ref.watch(dashboardStateProvider);
|
||||||
label: "dashboard",
|
final columns = max(4 * ((dashboardState.viewWidth / 350).ceil()), 8);
|
||||||
builder: (isCurrent, child) {
|
return Align(
|
||||||
_initScaffold(isCurrent);
|
alignment: Alignment.topCenter,
|
||||||
return child!;
|
child: SingleChildScrollView(
|
||||||
},
|
padding: const EdgeInsets.all(16).copyWith(
|
||||||
child: Align(
|
bottom: 88,
|
||||||
alignment: Alignment.topCenter,
|
),
|
||||||
child: SingleChildScrollView(
|
child: SuperGrid(
|
||||||
padding: const EdgeInsets.all(16).copyWith(
|
key: key,
|
||||||
bottom: 88,
|
crossAxisCount: columns,
|
||||||
),
|
crossAxisSpacing: 16,
|
||||||
child: Selector2<AppState, Config, DashboardState>(
|
mainAxisSpacing: 16,
|
||||||
selector: (_, appState, config) => DashboardState(
|
children: [
|
||||||
dashboardWidgets: config.appSetting.dashboardWidgets,
|
...dashboardState.dashboardWidgets
|
||||||
viewWidth: appState.viewWidth,
|
.where(
|
||||||
),
|
(item) => item.platforms.contains(
|
||||||
builder: (_, state, ___) {
|
SupportPlatform.currentPlatform,
|
||||||
final columns = max(4 * ((state.viewWidth / 350).ceil()), 8);
|
),
|
||||||
return SuperGrid(
|
)
|
||||||
key: key,
|
.map(
|
||||||
crossAxisCount: columns,
|
(item) => item.widget,
|
||||||
crossAxisSpacing: 16,
|
),
|
||||||
mainAxisSpacing: 16,
|
],
|
||||||
children: [
|
onSave: (girdItems) {
|
||||||
...state.dashboardWidgets
|
_handleSave(girdItems, ref);
|
||||||
.where(
|
},
|
||||||
(item) => item.platforms.contains(
|
addedItemsBuilder: (girdItems) {
|
||||||
SupportPlatform.currentPlatform,
|
return DashboardWidget.values
|
||||||
),
|
.where(
|
||||||
)
|
(item) =>
|
||||||
.map(
|
!girdItems.contains(item.widget) &&
|
||||||
(item) => item.widget,
|
item.platforms.contains(
|
||||||
|
SupportPlatform.currentPlatform,
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
onSave: (girdItems) {
|
.map((item) => item.widget)
|
||||||
final dashboardWidgets = girdItems
|
.toList();
|
||||||
.map(
|
},
|
||||||
(item) => DashboardWidget.getDashboardWidget(item),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
dashboardWidgets: dashboardWidgets,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
addedItemsBuilder: (girdItems) {
|
|
||||||
return DashboardWidget.values
|
|
||||||
.where(
|
|
||||||
(item) =>
|
|
||||||
!girdItems.contains(item.widget) &&
|
|
||||||
item.platforms.contains(
|
|
||||||
SupportPlatform.currentPlatform,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map((item) => item.widget)
|
|
||||||
.toList();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class CoreInfo extends StatelessWidget {
|
|
||||||
const CoreInfo({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<AppState, VersionInfo?>(
|
|
||||||
selector: (_, appState) => appState.versionInfo,
|
|
||||||
builder: (_, versionInfo, __) {
|
|
||||||
return CommonCard(
|
|
||||||
onPressed: () {},
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.coreInfo,
|
|
||||||
iconData: Icons.memory,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
padding: const EdgeInsets.all(16).copyWith(top: 0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: Text(
|
|
||||||
versionInfo?.clashName ?? '',
|
|
||||||
style: context
|
|
||||||
.textTheme
|
|
||||||
.titleMedium
|
|
||||||
?.toSoftBold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: Text(
|
|
||||||
versionInfo?.version ?? '',
|
|
||||||
style: context
|
|
||||||
.textTheme
|
|
||||||
.titleLarge
|
|
||||||
?.toSoftBold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final darkenLighter = context.colorScheme.secondaryContainer
|
||||||
|
.blendDarken(context, factor: 0.1)
|
||||||
|
.toLighter;
|
||||||
|
final darken = context.colorScheme.secondaryContainer
|
||||||
|
.blendDarken(context, factor: 0.1);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: getWidgetHeight(2),
|
height: getWidgetHeight(2),
|
||||||
child: CommonCard(
|
child: CommonCard(
|
||||||
@@ -90,17 +95,14 @@ class _MemoryInfoState extends State<MemoryInfo> {
|
|||||||
child: WaveView(
|
child: WaveView(
|
||||||
waveAmplitude: 12.0,
|
waveAmplitude: 12.0,
|
||||||
waveFrequency: 0.35,
|
waveFrequency: 0.35,
|
||||||
waveColor: context.colorScheme.secondaryContainer
|
waveColor: darkenLighter,
|
||||||
.blendDarken(context, factor: 0.1)
|
|
||||||
.toLighter,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: WaveView(
|
child: WaveView(
|
||||||
waveAmplitude: 12.0,
|
waveAmplitude: 12.0,
|
||||||
waveFrequency: 0.9,
|
waveFrequency: 0.9,
|
||||||
waveColor: context.colorScheme.secondaryContainer
|
waveColor: darken,
|
||||||
.blendDarken(context, factor: 0.1),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/app.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
||||||
const NetworkDetectionState(
|
const NetworkDetectionState(
|
||||||
@@ -16,14 +17,14 @@ final _networkDetectionState = ValueNotifier<NetworkDetectionState>(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class NetworkDetection extends StatefulWidget {
|
class NetworkDetection extends ConsumerStatefulWidget {
|
||||||
const NetworkDetection({super.key});
|
const NetworkDetection({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NetworkDetection> createState() => _NetworkDetectionState();
|
ConsumerState<NetworkDetection> createState() => _NetworkDetectionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NetworkDetectionState extends State<NetworkDetection> {
|
class _NetworkDetectionState extends ConsumerState<NetworkDetection> {
|
||||||
bool? _preIsStart;
|
bool? _preIsStart;
|
||||||
Timer? _setTimeoutTimer;
|
Timer? _setTimeoutTimer;
|
||||||
CancelToken? cancelToken;
|
CancelToken? cancelToken;
|
||||||
@@ -31,6 +32,11 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
ref.listenManual(checkIpNumProvider, (prev, next) {
|
||||||
|
if (prev != next) {
|
||||||
|
_startCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,12 +53,13 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_checkIp() async {
|
_checkIp() async {
|
||||||
final appState = globalState.appController.appState;
|
final appState = globalState.appState;
|
||||||
final appFlowingState = globalState.appController.appFlowingState;
|
|
||||||
final isInit = appState.isInit;
|
final isInit = appState.isInit;
|
||||||
if (!isInit) return;
|
if (!isInit) return;
|
||||||
final isStart = appFlowingState.isStart;
|
final isStart = appState.runTime != null;
|
||||||
if (_preIsStart == false && _preIsStart == isStart) return;
|
if (_preIsStart == false &&
|
||||||
|
_preIsStart == isStart &&
|
||||||
|
_networkDetectionState.value.ipInfo != null) return;
|
||||||
_clearSetTimeoutTimer();
|
_clearSetTimeoutTimer();
|
||||||
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
_networkDetectionState.value = _networkDetectionState.value.copyWith(
|
||||||
isTesting: true,
|
isTesting: true,
|
||||||
@@ -109,24 +116,6 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkIpContainer(Widget child) {
|
|
||||||
return Selector<AppState, num>(
|
|
||||||
selector: (_, appState) {
|
|
||||||
return appState.checkIpNum;
|
|
||||||
},
|
|
||||||
shouldRebuild: (prev, next) {
|
|
||||||
if (prev != next) {
|
|
||||||
_startCheck();
|
|
||||||
}
|
|
||||||
return prev != next;
|
|
||||||
},
|
|
||||||
builder: (_, checkIpNum, child) {
|
|
||||||
return child!;
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_countryCodeToEmoji(String countryCode) {
|
_countryCodeToEmoji(String countryCode) {
|
||||||
final String code = countryCode.toUpperCase();
|
final String code = countryCode.toUpperCase();
|
||||||
if (code.length != 2) {
|
if (code.length != 2) {
|
||||||
@@ -141,109 +130,130 @@ class _NetworkDetectionState extends State<NetworkDetection> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: getWidgetHeight(1),
|
height: getWidgetHeight(1),
|
||||||
child: _checkIpContainer(
|
child: ValueListenableBuilder<NetworkDetectionState>(
|
||||||
ValueListenableBuilder<NetworkDetectionState>(
|
valueListenable: _networkDetectionState,
|
||||||
valueListenable: _networkDetectionState,
|
builder: (_, state, __) {
|
||||||
builder: (_, state, __) {
|
final ipInfo = state.ipInfo;
|
||||||
final ipInfo = state.ipInfo;
|
final isTesting = state.isTesting;
|
||||||
final isTesting = state.isTesting;
|
return CommonCard(
|
||||||
return CommonCard(
|
onPressed: () {},
|
||||||
onPressed: () {},
|
child: Column(
|
||||||
child: Column(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
Container(
|
||||||
Container(
|
height: globalState.measure.titleMediumHeight + 16,
|
||||||
height: globalState.measure.titleMediumHeight + 16,
|
padding: baseInfoEdgeInsets.copyWith(
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
bottom: 0,
|
||||||
bottom: 0,
|
),
|
||||||
),
|
child: Row(
|
||||||
child: Row(
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisSize: MainAxisSize.max,
|
children: [
|
||||||
children: [
|
ipInfo != null
|
||||||
ipInfo != null
|
? Text(
|
||||||
? Text(
|
_countryCodeToEmoji(
|
||||||
_countryCodeToEmoji(
|
ipInfo.countryCode,
|
||||||
ipInfo.countryCode,
|
|
||||||
),
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.titleMedium
|
|
||||||
?.toLight
|
|
||||||
.copyWith(
|
|
||||||
fontFamily: FontFamily.twEmoji.value,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
Icons.network_check,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: TooltipText(
|
|
||||||
text: Text(
|
|
||||||
appLocalizations.networkDetection,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleSmall
|
.titleMedium
|
||||||
?.copyWith(
|
?.toLight
|
||||||
color: context.colorScheme.onSurfaceVariant,
|
.copyWith(
|
||||||
|
fontFamily: FontFamily.twEmoji.value,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
Icons.network_check,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: TooltipText(
|
||||||
|
text: Text(
|
||||||
|
appLocalizations.networkDetection,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleSmall
|
||||||
|
?.copyWith(
|
||||||
|
color: context.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
SizedBox(width: 2),
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: () {
|
||||||
|
globalState.showMessage(
|
||||||
|
title: appLocalizations.tip,
|
||||||
|
message: TextSpan(
|
||||||
|
text: appLocalizations.detectionTip,
|
||||||
|
),
|
||||||
|
cancelable: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
size: 16,
|
||||||
|
Icons.info_outline,
|
||||||
|
color: context.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Container(
|
),
|
||||||
padding: baseInfoEdgeInsets.copyWith(
|
Container(
|
||||||
top: 0,
|
padding: baseInfoEdgeInsets.copyWith(
|
||||||
),
|
top: 0,
|
||||||
child: SizedBox(
|
),
|
||||||
height: globalState.measure.bodyMediumHeight + 2,
|
child: SizedBox(
|
||||||
child: FadeBox(
|
height: globalState.measure.bodyMediumHeight + 2,
|
||||||
child: ipInfo != null
|
child: FadeBox(
|
||||||
? TooltipText(
|
child: ipInfo != null
|
||||||
text: Text(
|
? TooltipText(
|
||||||
ipInfo.ip,
|
text: Text(
|
||||||
style: context.textTheme.bodyMedium?.toLight
|
ipInfo.ip,
|
||||||
.adjustSize(1),
|
style: context.textTheme.bodyMedium?.toLight
|
||||||
maxLines: 1,
|
.adjustSize(1),
|
||||||
overflow: TextOverflow.ellipsis,
|
maxLines: 1,
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
),
|
||||||
: FadeBox(
|
)
|
||||||
child: isTesting == false && ipInfo == null
|
: FadeBox(
|
||||||
? Text(
|
child: isTesting == false && ipInfo == null
|
||||||
"timeout",
|
? Text(
|
||||||
style: context.textTheme.bodyMedium
|
"timeout",
|
||||||
?.copyWith(color: Colors.red)
|
style: context.textTheme.bodyMedium
|
||||||
.adjustSize(1),
|
?.copyWith(color: Colors.red)
|
||||||
maxLines: 1,
|
.adjustSize(1),
|
||||||
overflow: TextOverflow.ellipsis,
|
maxLines: 1,
|
||||||
)
|
overflow: TextOverflow.ellipsis,
|
||||||
: Container(
|
)
|
||||||
padding: const EdgeInsets.all(2),
|
: Container(
|
||||||
child: const AspectRatio(
|
padding: const EdgeInsets.all(2),
|
||||||
aspectRatio: 1,
|
child: const AspectRatio(
|
||||||
child: CircularProgressIndicator(),
|
aspectRatio: 1,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
),
|
||||||
)
|
);
|
||||||
],
|
},
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/app.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
class TrafficUsage extends StatelessWidget {
|
class TrafficUsage extends StatelessWidget {
|
||||||
const TrafficUsage({super.key});
|
const TrafficUsage({super.key});
|
||||||
@@ -62,9 +63,9 @@ class TrafficUsage extends StatelessWidget {
|
|||||||
iconData: Icons.data_saver_off,
|
iconData: Icons.data_saver_off,
|
||||||
),
|
),
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
child: Selector<AppFlowingState, Traffic>(
|
child: Consumer(
|
||||||
selector: (_, appFlowingState) => appFlowingState.totalTraffic,
|
builder: (_, ref, __) {
|
||||||
builder: (_, totalTraffic, __) {
|
final totalTraffic = ref.watch(totalTrafficProvider);
|
||||||
final upTotalTrafficValue = totalTraffic.up;
|
final upTotalTrafficValue = totalTraffic.up;
|
||||||
final downTotalTrafficValue = totalTraffic.down;
|
final downTotalTrafficValue = totalTraffic.down;
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ export 'dashboard/dashboard.dart';
|
|||||||
export 'tools.dart';
|
export 'tools.dart';
|
||||||
export 'profiles/profiles.dart';
|
export 'profiles/profiles.dart';
|
||||||
export 'logs.dart';
|
export 'logs.dart';
|
||||||
export 'connections.dart';
|
|
||||||
export 'access.dart';
|
export 'access.dart';
|
||||||
export 'config/config.dart';
|
export 'config/config.dart';
|
||||||
export 'application_setting.dart';
|
export 'application_setting.dart';
|
||||||
export 'about.dart';
|
export 'about.dart';
|
||||||
export 'backup_and_recovery.dart';
|
export 'backup_and_recovery.dart';
|
||||||
export 'resources.dart';
|
export 'resources.dart';
|
||||||
export 'requests.dart';
|
export 'connection/requests.dart';
|
||||||
|
export 'connection/connections.dart';
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,106 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../models/models.dart';
|
import '../models/models.dart';
|
||||||
import '../widgets/widgets.dart';
|
import '../widgets/widgets.dart';
|
||||||
|
|
||||||
class LogsFragment extends StatefulWidget {
|
double _preOffset = 0;
|
||||||
|
|
||||||
|
class LogsFragment extends ConsumerStatefulWidget {
|
||||||
const LogsFragment({super.key});
|
const LogsFragment({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LogsFragment> createState() => _LogsFragmentState();
|
ConsumerState<LogsFragment> createState() => _LogsFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LogsFragmentState extends State<LogsFragment> {
|
class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
|
||||||
final logsNotifier = ValueNotifier<LogsAndKeywords>(const LogsAndKeywords());
|
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
|
||||||
final scrollController = ScrollController(
|
final _scrollController = ScrollController(
|
||||||
keepScrollOffset: false,
|
initialScrollOffset: _preOffset != 0 ? _preOffset : double.maxFinite,
|
||||||
);
|
);
|
||||||
|
final FixedMap<String, double?> _cacheDynamicHeightMap = FixedMap(1000);
|
||||||
|
double _currentMaxWidth = 0;
|
||||||
|
|
||||||
Timer? timer;
|
List<Log> _logs = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
||||||
final appFlowingState = globalState.appController.appFlowingState;
|
logs: globalState.appState.logs.list,
|
||||||
logsNotifier.value =
|
);
|
||||||
logsNotifier.value.copyWith(logs: appFlowingState.logs);
|
ref.listenManual(
|
||||||
if (timer != null) {
|
logsProvider.select((state) => state.list),
|
||||||
timer?.cancel();
|
(prev, next) {
|
||||||
timer = null;
|
if (prev != next) {
|
||||||
}
|
final isEquality = logListEquality.equals(prev, next);
|
||||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
if (!isEquality) {
|
||||||
final logs = appFlowingState.logs;
|
_logs = next;
|
||||||
if (!logListEquality.equals(
|
updateLogsThrottler();
|
||||||
logsNotifier.value.logs,
|
}
|
||||||
logs,
|
|
||||||
)) {
|
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
|
||||||
logs: logs,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
});
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
ref.listenManual(
|
||||||
|
isCurrentPageProvider(
|
||||||
|
PageLabel.logs,
|
||||||
|
handler: (pageLabel, viewMode) =>
|
||||||
|
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
||||||
|
),
|
||||||
|
(prev, next) {
|
||||||
|
if (prev != next && next == true) {
|
||||||
|
initPageState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> get actions => [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_handleExport();
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.file_download_outlined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onSearch => (value) {
|
||||||
|
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
||||||
|
query: value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onKeywordsUpdate => (keywords) {
|
||||||
|
_logsStateNotifier.value =
|
||||||
|
_logsStateNotifier.value.copyWith(keywords: keywords);
|
||||||
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
timer?.cancel();
|
_logsStateNotifier.dispose();
|
||||||
logsNotifier.dispose();
|
_scrollController.dispose();
|
||||||
scrollController.dispose();
|
_cacheDynamicHeightMap.clear();
|
||||||
timer = null;
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleTryClearCache(double maxWidth) {
|
||||||
|
if (_currentMaxWidth != maxWidth) {
|
||||||
|
_currentMaxWidth = maxWidth;
|
||||||
|
_cacheDynamicHeightMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_handleExport() async {
|
_handleExport() async {
|
||||||
final commonScaffoldState = context.commonScaffoldState;
|
final commonScaffoldState = context.commonScaffoldState;
|
||||||
final res = await commonScaffoldState?.loadingRun<bool>(
|
final res = await commonScaffoldState?.loadingRun<bool>(
|
||||||
@@ -71,295 +116,115 @@ class _LogsFragmentState extends State<LogsFragment> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initActions() {
|
double _calcCacheHeight(String text) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
final cacheHeight = _cacheDynamicHeightMap.get(text);
|
||||||
final commonScaffoldState =
|
if (cacheHeight != null) {
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
return cacheHeight;
|
||||||
commonScaffoldState?.actions = [
|
}
|
||||||
IconButton(
|
final size = globalState.measure.computeTextSize(
|
||||||
onPressed: () {
|
Text(
|
||||||
showSearch(
|
text,
|
||||||
context: context,
|
style: globalState.appController.context.textTheme.bodyLarge,
|
||||||
delegate: LogsSearchDelegate(
|
),
|
||||||
logs: logsNotifier.value,
|
maxWidth: _currentMaxWidth,
|
||||||
),
|
);
|
||||||
);
|
_cacheDynamicHeightMap.put(text, size.height);
|
||||||
},
|
return size.height;
|
||||||
icon: const Icon(Icons.search),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
_handleExport();
|
|
||||||
},
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.file_download_outlined,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_addKeyword(String keyword) {
|
double _getItemHeight(Log log) {
|
||||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
final measure = globalState.measure;
|
||||||
if (isContains) return;
|
final bodySmallHeight = measure.bodySmallHeight;
|
||||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
final bodyMediumHeight = measure.bodyMediumHeight;
|
||||||
..add(keyword);
|
final height = _calcCacheHeight(log.payload ?? "");
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
return height + bodySmallHeight + 8 + bodyMediumHeight + 40;
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_deleteKeyword(String keyword) {
|
updateLogsThrottler() {
|
||||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
throttler.call("logs", () {
|
||||||
if (!isContains) return;
|
final isEquality = logListEquality.equals(
|
||||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
_logs,
|
||||||
..remove(keyword);
|
_logsStateNotifier.value.logs,
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
);
|
||||||
keywords: keywords,
|
if (isEquality) {
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
||||||
|
logs: _logs,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, duration: commonDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<AppState, bool?>(
|
return LayoutBuilder(
|
||||||
selector: (_, appState) =>
|
builder: (_, constraints) {
|
||||||
appState.currentLabel == 'logs' ||
|
_handleTryClearCache(constraints.maxWidth - 40);
|
||||||
appState.viewMode == ViewMode.mobile &&
|
return Align(
|
||||||
appState.currentLabel == "tools",
|
alignment: Alignment.topCenter,
|
||||||
builder: (_, isCurrent, child) {
|
child: ValueListenableBuilder<LogsState>(
|
||||||
if (isCurrent == null || isCurrent) {
|
valueListenable: _logsStateNotifier,
|
||||||
_initActions();
|
builder: (_, state, __) {
|
||||||
}
|
final logs = state.list;
|
||||||
return child!;
|
if (logs.isEmpty) {
|
||||||
},
|
return NullStatus(
|
||||||
child: ValueListenableBuilder<LogsAndKeywords>(
|
label: appLocalizations.nullLogsDesc,
|
||||||
valueListenable: logsNotifier,
|
);
|
||||||
builder: (_, state, __) {
|
}
|
||||||
final logs = state.filteredLogs;
|
final items = logs
|
||||||
if (logs.isEmpty) {
|
.map<Widget>(
|
||||||
return NullStatus(
|
(log) => LogItem(
|
||||||
label: appLocalizations.nullLogsDesc,
|
key: Key(log.dateTime.toString()),
|
||||||
);
|
log: log,
|
||||||
}
|
onClick: (value) {
|
||||||
final reversedLogs = logs.reversed.toList();
|
context.commonScaffoldState?.addKeyword(value);
|
||||||
final logWidgets = reversedLogs
|
},
|
||||||
.map<Widget>(
|
),
|
||||||
(log) => LogItem(
|
)
|
||||||
key: Key(log.dateTime.toString()),
|
.separated(
|
||||||
log: log,
|
const Divider(
|
||||||
onClick: _addKeyword,
|
height: 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.separated(
|
.toList();
|
||||||
const Divider(
|
return NotificationListener<ScrollEndNotification>(
|
||||||
height: 0,
|
onNotification: (details) {
|
||||||
),
|
_preOffset = details.metrics.pixels;
|
||||||
)
|
return false;
|
||||||
.toList();
|
},
|
||||||
return Column(
|
child: CommonScrollBar(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
controller: _scrollController,
|
||||||
children: [
|
child: ListView.builder(
|
||||||
if (state.keywords.isNotEmpty)
|
reverse: true,
|
||||||
Padding(
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.symmetric(
|
physics: NextClampingScrollPhysics(),
|
||||||
horizontal: 16,
|
controller: _scrollController,
|
||||||
vertical: 16,
|
itemBuilder: (_, index) {
|
||||||
),
|
return items[index];
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 8,
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
for (final keyword in state.keywords)
|
|
||||||
CommonChip(
|
|
||||||
label: keyword,
|
|
||||||
type: ChipType.delete,
|
|
||||||
onPressed: () {
|
|
||||||
_deleteKeyword(keyword);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (_, constraints) {
|
|
||||||
return ScrollConfiguration(
|
|
||||||
behavior: ShowBarScrollBehavior(),
|
|
||||||
child: ListView.builder(
|
|
||||||
controller: scrollController,
|
|
||||||
itemExtentBuilder: (index, __) {
|
|
||||||
final widget = logWidgets[index];
|
|
||||||
if (widget.runtimeType == Divider) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
final measure = globalState.measure;
|
|
||||||
final bodyLargeSize = measure.bodyLargeSize;
|
|
||||||
final bodySmallHeight = measure.bodySmallHeight;
|
|
||||||
final bodyMediumHeight = measure.bodyMediumHeight;
|
|
||||||
final log = reversedLogs[(index / 2).floor()];
|
|
||||||
final width = (log.payload?.length ?? 0) *
|
|
||||||
bodyLargeSize.width +
|
|
||||||
200;
|
|
||||||
final lines = (width / constraints.maxWidth).ceil();
|
|
||||||
return lines * bodyLargeSize.height +
|
|
||||||
bodySmallHeight +
|
|
||||||
8 +
|
|
||||||
bodyMediumHeight +
|
|
||||||
40;
|
|
||||||
},
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
return logWidgets[index];
|
|
||||||
},
|
|
||||||
itemCount: logWidgets.length,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogsSearchDelegate extends SearchDelegate {
|
|
||||||
ValueNotifier<LogsAndKeywords> logsNotifier;
|
|
||||||
|
|
||||||
LogsSearchDelegate({
|
|
||||||
required LogsAndKeywords logs,
|
|
||||||
}) : logsNotifier = ValueNotifier(logs);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
logsNotifier.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
get state => logsNotifier.value;
|
|
||||||
|
|
||||||
List<Log> get _results {
|
|
||||||
final lowQuery = query.toLowerCase();
|
|
||||||
return logsNotifier.value.filteredLogs
|
|
||||||
.where(
|
|
||||||
(log) =>
|
|
||||||
(log.payload?.toLowerCase().contains(lowQuery) ?? false) ||
|
|
||||||
log.logLevel.name.contains(lowQuery),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget>? buildActions(BuildContext context) {
|
|
||||||
return [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (query.isEmpty) {
|
|
||||||
close(context, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
query = '';
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget? buildLeading(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
close(context, null);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildResults(BuildContext context) {
|
|
||||||
return buildSuggestions(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
_addKeyword(String keyword) {
|
|
||||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (isContains) return;
|
|
||||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
|
||||||
..add(keyword);
|
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_deleteKeyword(String keyword) {
|
|
||||||
final isContains = logsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (!isContains) return;
|
|
||||||
final keywords = List<String>.from(logsNotifier.value.keywords)
|
|
||||||
..remove(keyword);
|
|
||||||
logsNotifier.value = logsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildSuggestions(BuildContext context) {
|
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable: logsNotifier,
|
|
||||||
builder: (_, __, ___) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (state.keywords.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 6,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
for (final keyword in state.keywords)
|
|
||||||
CommonChip(
|
|
||||||
label: keyword,
|
|
||||||
type: ChipType.delete,
|
|
||||||
onPressed: () {
|
|
||||||
_deleteKeyword(keyword);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.separated(
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final log = _results[index];
|
|
||||||
return LogItem(
|
|
||||||
key: Key(log.dateTime.toString()),
|
|
||||||
log: log,
|
|
||||||
onClick: (value) {
|
|
||||||
_addKeyword(value);
|
|
||||||
},
|
},
|
||||||
);
|
itemExtentBuilder: (index, __) {
|
||||||
},
|
final item = items[index];
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
if (item.runtimeType == Divider) {
|
||||||
return const Divider(
|
return 0;
|
||||||
height: 0,
|
}
|
||||||
);
|
final log = logs[(index / 2).floor()];
|
||||||
},
|
return _getItemHeight(log);
|
||||||
itemCount: _results.length,
|
},
|
||||||
),
|
itemCount: items.length,
|
||||||
)
|
),
|
||||||
],
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogItem extends StatefulWidget {
|
class LogItem extends StatelessWidget {
|
||||||
final Log log;
|
final Log log;
|
||||||
final Function(String)? onClick;
|
final Function(String)? onClick;
|
||||||
|
|
||||||
@@ -369,14 +234,8 @@ class LogItem extends StatefulWidget {
|
|||||||
this.onClick,
|
this.onClick,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
State<LogItem> createState() => _LogItemState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LogItemState extends State<LogItem> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final log = widget.log;
|
|
||||||
return ListItem(
|
return ListItem(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
@@ -384,14 +243,16 @@ class _LogItemState extends State<LogItem> {
|
|||||||
),
|
),
|
||||||
title: SelectableText(
|
title: SelectableText(
|
||||||
log.payload ?? '',
|
log.payload ?? '',
|
||||||
|
style: context.textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SelectableText(
|
SelectableText(
|
||||||
"${log.dateTime}",
|
"${log.dateTime}",
|
||||||
style: context.textTheme.bodySmall
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
?.copyWith(color: context.colorScheme.primary),
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
@@ -400,8 +261,8 @@ class _LogItemState extends State<LogItem> {
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: CommonChip(
|
child: CommonChip(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (widget.onClick == null) return;
|
if (onClick == null) return;
|
||||||
widget.onClick!(log.logLevel.name);
|
onClick!(log.logLevel.name);
|
||||||
},
|
},
|
||||||
label: log.logLevel.name,
|
label: log.logLevel.name,
|
||||||
),
|
),
|
||||||
@@ -411,3 +272,11 @@ class _LogItemState extends State<LogItem> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NoGlowScrollBehavior extends ScrollBehavior {
|
||||||
|
@override
|
||||||
|
Widget buildOverscrollIndicator(
|
||||||
|
BuildContext context, Widget child, ScrollableDetails details) {
|
||||||
|
return child; // 禁用过度滚动效果
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
54
lib/fragments/profiles/custom_profile.dart
Normal file
54
lib/fragments/profiles/custom_profile.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/widgets/scaffold.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CustomProfile extends StatefulWidget {
|
||||||
|
final String profileId;
|
||||||
|
|
||||||
|
const CustomProfile({
|
||||||
|
super.key,
|
||||||
|
required this.profileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CustomProfile> createState() => _CustomProfileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomProfileState extends State<CustomProfile> {
|
||||||
|
final _currentClashConfigNotifier = ValueNotifier<ClashConfig?>(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initCurrentClashConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
_initCurrentClashConfig() async {
|
||||||
|
// final currentProfileId = globalState.config.currentProfileId;
|
||||||
|
// if (currentProfileId == null) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// _currentClashConfigNotifier.value =
|
||||||
|
// await clashCore.getProfile(currentProfileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CommonScaffold(
|
||||||
|
body: ValueListenableBuilder(
|
||||||
|
valueListenable: _currentClashConfigNotifier,
|
||||||
|
builder: (_, clashConfig, ___) {
|
||||||
|
if (clashConfig == null) {
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
children: [],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: "自定义",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,11 @@ import 'package:fl_clash/common/common.dart';
|
|||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
|
import 'package:fl_clash/fragments/profiles/edit_profile.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'add_profile.dart';
|
import 'add_profile.dart';
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ class ProfilesFragment extends StatefulWidget {
|
|||||||
State<ProfilesFragment> createState() => _ProfilesFragmentState();
|
State<ProfilesFragment> createState() => _ProfilesFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProfilesFragmentState extends State<ProfilesFragment> {
|
class _ProfilesFragmentState extends State<ProfilesFragment> with PageMixin {
|
||||||
Function? applyConfigDebounce;
|
Function? applyConfigDebounce;
|
||||||
|
|
||||||
_handleShowAddExtendPage() {
|
_handleShowAddExtendPage() {
|
||||||
@@ -33,21 +33,19 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateProfiles() async {
|
_updateProfiles() async {
|
||||||
final appController = globalState.appController;
|
final profiles = globalState.config.profiles;
|
||||||
final config = appController.config;
|
|
||||||
final profiles = appController.config.profiles;
|
|
||||||
final messages = [];
|
final messages = [];
|
||||||
final updateProfiles = profiles.map<Future>(
|
final updateProfiles = profiles.map<Future>(
|
||||||
(profile) async {
|
(profile) async {
|
||||||
if (profile.type == ProfileType.file) return;
|
if (profile.type == ProfileType.file) return;
|
||||||
config.setProfile(
|
globalState.appController.setProfile(
|
||||||
profile.copyWith(isUpdating: true),
|
profile.copyWith(isUpdating: true),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await appController.updateProfile(profile);
|
await globalState.appController.updateProfile(profile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
messages.add("${profile.label ?? profile.id}: $e \n");
|
messages.add("${profile.label ?? profile.id}: $e \n");
|
||||||
config.setProfile(
|
globalState.appController.setProfile(
|
||||||
profile.copyWith(
|
profile.copyWith(
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
),
|
),
|
||||||
@@ -70,97 +68,90 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initScaffold() {
|
@override
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
List<Widget> get actions => [
|
||||||
(_) {
|
IconButton(
|
||||||
if (!mounted) return;
|
onPressed: () {
|
||||||
final commonScaffoldState =
|
_updateProfiles();
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
},
|
||||||
commonScaffoldState?.actions = [
|
icon: const Icon(Icons.sync),
|
||||||
IconButton(
|
),
|
||||||
onPressed: () {
|
IconButton(
|
||||||
_updateProfiles();
|
onPressed: () {
|
||||||
},
|
final profiles = globalState.config.profiles;
|
||||||
icon: const Icon(Icons.sync),
|
showSheet(
|
||||||
),
|
title: appLocalizations.profilesSort,
|
||||||
IconButton(
|
context: context,
|
||||||
onPressed: () {
|
body: SizedBox(
|
||||||
final profiles = globalState.appController.config.profiles;
|
height: 400,
|
||||||
showSheet(
|
child: ReorderableProfiles(profiles: profiles),
|
||||||
title: appLocalizations.profilesSort,
|
),
|
||||||
context: context,
|
);
|
||||||
body: SizedBox(
|
},
|
||||||
height: 400,
|
icon: const Icon(Icons.sort),
|
||||||
child: ReorderableProfiles(profiles: profiles),
|
iconSize: 26,
|
||||||
),
|
),
|
||||||
);
|
];
|
||||||
},
|
|
||||||
icon: const Icon(Icons.sort),
|
@override
|
||||||
iconSize: 26,
|
Widget? get floatingActionButton => FloatingActionButton(
|
||||||
),
|
heroTag: null,
|
||||||
];
|
onPressed: _handleShowAddExtendPage,
|
||||||
commonScaffoldState?.floatingActionButton = FloatingActionButton(
|
child: const Icon(
|
||||||
heroTag: null,
|
Icons.add,
|
||||||
onPressed: _handleShowAddExtendPage,
|
),
|
||||||
child: const Icon(
|
);
|
||||||
Icons.add,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ActiveBuilder(
|
return Consumer(
|
||||||
label: "profiles",
|
builder: (_, ref, __) {
|
||||||
builder: (isCurrent, child) {
|
ref.listenManual(
|
||||||
if (isCurrent) {
|
isCurrentPageProvider(PageLabel.profiles),
|
||||||
_initScaffold();
|
(prev, next) {
|
||||||
}
|
if (prev != next && next == true) {
|
||||||
return child!;
|
initPageState();
|
||||||
},
|
}
|
||||||
child: Selector2<AppState, Config, ProfilesSelectorState>(
|
},
|
||||||
selector: (_, appState, config) => ProfilesSelectorState(
|
fireImmediately: true,
|
||||||
profiles: config.profiles,
|
);
|
||||||
currentProfileId: config.currentProfileId,
|
final profilesSelectorState = ref.watch(profilesSelectorStateProvider);
|
||||||
columns: other.getProfilesColumns(appState.viewWidth),
|
if (profilesSelectorState.profiles.isEmpty) {
|
||||||
),
|
return NullStatus(
|
||||||
builder: (context, state, child) {
|
label: appLocalizations.nullProfileDesc,
|
||||||
if (state.profiles.isEmpty) {
|
|
||||||
return NullStatus(
|
|
||||||
label: appLocalizations.nullProfileDesc,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
top: 16,
|
|
||||||
bottom: 88,
|
|
||||||
),
|
|
||||||
child: Grid(
|
|
||||||
mainAxisSpacing: 16,
|
|
||||||
crossAxisSpacing: 16,
|
|
||||||
crossAxisCount: state.columns,
|
|
||||||
children: [
|
|
||||||
for (int i = 0; i < state.profiles.length; i++)
|
|
||||||
GridItem(
|
|
||||||
child: ProfileItem(
|
|
||||||
key: Key(state.profiles[i].id),
|
|
||||||
profile: state.profiles[i],
|
|
||||||
groupValue: state.currentProfileId,
|
|
||||||
onChanged: globalState.appController.changeProfile,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
),
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 16,
|
||||||
|
bottom: 88,
|
||||||
|
),
|
||||||
|
child: Grid(
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
crossAxisCount: profilesSelectorState.columns,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < profilesSelectorState.profiles.length; i++)
|
||||||
|
GridItem(
|
||||||
|
child: ProfileItem(
|
||||||
|
key: Key(profilesSelectorState.profiles[i].id),
|
||||||
|
profile: profilesSelectorState.profiles[i],
|
||||||
|
groupValue: profilesSelectorState.currentProfileId,
|
||||||
|
onChanged: (profileId) {
|
||||||
|
ref.read(currentProfileIdProvider.notifier).value =
|
||||||
|
profileId;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,18 +187,17 @@ class ProfileItem extends StatelessWidget {
|
|||||||
|
|
||||||
Future updateProfile() async {
|
Future updateProfile() async {
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final config = appController.config;
|
|
||||||
if (profile.type == ProfileType.file) return;
|
if (profile.type == ProfileType.file) return;
|
||||||
await globalState.safeRun(silence: false, () async {
|
await globalState.safeRun(silence: false, () async {
|
||||||
try {
|
try {
|
||||||
config.setProfile(
|
appController.setProfile(
|
||||||
profile.copyWith(
|
profile.copyWith(
|
||||||
isUpdating: true,
|
isUpdating: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await appController.updateProfile(profile);
|
await appController.updateProfile(profile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
config.setProfile(
|
appController.setProfile(
|
||||||
profile.copyWith(
|
profile.copyWith(
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
),
|
),
|
||||||
@@ -257,16 +247,16 @@ class ProfileItem extends StatelessWidget {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleCopyLink(BuildContext context) async {
|
// _handleCopyLink(BuildContext context) async {
|
||||||
await Clipboard.setData(
|
// await Clipboard.setData(
|
||||||
ClipboardData(
|
// ClipboardData(
|
||||||
text: profile.url,
|
// text: profile.url,
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
if (context.mounted) {
|
// if (context.mounted) {
|
||||||
context.showNotifier(appLocalizations.copySuccess);
|
// context.showNotifier(appLocalizations.copySuccess);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
_handleExportFile(BuildContext context) async {
|
_handleExportFile(BuildContext context) async {
|
||||||
final commonScaffoldState = context.commonScaffoldState;
|
final commonScaffoldState = context.commonScaffoldState;
|
||||||
@@ -287,8 +277,18 @@ class ProfileItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _handlePushCustomPage(BuildContext context, String id) {
|
||||||
|
// BaseNavigator.push(
|
||||||
|
// context,
|
||||||
|
// CustomProfile(
|
||||||
|
// profileId: id,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final key = GlobalKey<CommonPopupBoxState>();
|
||||||
return CommonCard(
|
return CommonCard(
|
||||||
isSelected: profile.id == groupValue,
|
isSelected: profile.id == groupValue,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -308,6 +308,7 @@ class ProfileItem extends StatelessWidget {
|
|||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
)
|
)
|
||||||
: CommonPopupBox(
|
: CommonPopupBox(
|
||||||
|
key: key,
|
||||||
popup: CommonPopupMenu(
|
popup: CommonPopupMenu(
|
||||||
items: [
|
items: [
|
||||||
ActionItemData(
|
ActionItemData(
|
||||||
@@ -325,14 +326,21 @@ class ProfileItem extends StatelessWidget {
|
|||||||
_handleUpdateProfile();
|
_handleUpdateProfile();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ActionItemData(
|
// ActionItemData(
|
||||||
icon: Icons.copy,
|
// icon: Icons.copy,
|
||||||
label: appLocalizations.copyLink,
|
// label: appLocalizations.copyLink,
|
||||||
onPressed: () {
|
// onPressed: () {
|
||||||
_handleCopyLink(context);
|
// _handleCopyLink(context);
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
],
|
],
|
||||||
|
// ActionItemData(
|
||||||
|
// icon: Icons.extension_outlined,
|
||||||
|
// label: "自定义",
|
||||||
|
// onPressed: () {
|
||||||
|
// _handlePushCustomPage(context, profile.id);
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
ActionItemData(
|
ActionItemData(
|
||||||
icon: Icons.file_copy_outlined,
|
icon: Icons.file_copy_outlined,
|
||||||
label: appLocalizations.exportFile,
|
label: appLocalizations.exportFile,
|
||||||
@@ -352,7 +360,9 @@ class ProfileItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
target: IconButton(
|
target: IconButton(
|
||||||
onPressed: () {},
|
onPressed: () {
|
||||||
|
key.currentState?.pop();
|
||||||
|
},
|
||||||
icon: Icon(Icons.more_vert),
|
icon: Icon(Icons.more_vert),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -496,7 +506,7 @@ class _ReorderableProfilesState extends State<ReorderableProfiles> {
|
|||||||
child: FilledButton.tonal(
|
child: FilledButton.tonal(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
globalState.appController.config.profiles = profiles;
|
globalState.appController.setProfiles(profiles);
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: WidgetStateProperty.all(
|
padding: WidgetStateProperty.all(
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,6 @@ import 'package:fl_clash/common/common.dart';
|
|||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
Widget currentSelectedProxyNameBuilder({
|
|
||||||
required String groupName,
|
|
||||||
required Widget Function(String currentGroupName) builder,
|
|
||||||
}) {
|
|
||||||
return Selector2<AppState, Config, String>(
|
|
||||||
selector: (_, appState, config) {
|
|
||||||
final group = appState.getGroupWithName(groupName);
|
|
||||||
final selectedProxyName = config.currentSelectedMap[groupName];
|
|
||||||
return group?.getCurrentSelectedName(selectedProxyName ?? "") ?? "";
|
|
||||||
},
|
|
||||||
builder: (_, currentSelectedProxyName, ___) {
|
|
||||||
return builder(currentSelectedProxyName);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
double get listHeaderHeight {
|
double get listHeaderHeight {
|
||||||
final measure = globalState.measure;
|
final measure = globalState.measure;
|
||||||
@@ -30,9 +12,9 @@ double get listHeaderHeight {
|
|||||||
double getItemHeight(ProxyCardType proxyCardType) {
|
double getItemHeight(ProxyCardType proxyCardType) {
|
||||||
final measure = globalState.measure;
|
final measure = globalState.measure;
|
||||||
final baseHeight =
|
final baseHeight =
|
||||||
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
|
16 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8 + 4;
|
||||||
return switch (proxyCardType) {
|
return switch (proxyCardType) {
|
||||||
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
|
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 6,
|
||||||
ProxyCardType.shrink => baseHeight,
|
ProxyCardType.shrink => baseHeight,
|
||||||
ProxyCardType.min => baseHeight - measure.bodyMediumHeight,
|
ProxyCardType.min => baseHeight - measure.bodyMediumHeight,
|
||||||
};
|
};
|
||||||
@@ -40,16 +22,16 @@ double getItemHeight(ProxyCardType proxyCardType) {
|
|||||||
|
|
||||||
proxyDelayTest(Proxy proxy, [String? testUrl]) async {
|
proxyDelayTest(Proxy proxy, [String? testUrl]) async {
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final proxyName = appController.appState.getRealProxyName(proxy.name);
|
final proxyName = appController.getRealProxyName(proxy.name);
|
||||||
final url = appController.getRealTestUrl(testUrl);
|
final url = appController.getRealTestUrl(testUrl);
|
||||||
globalState.appController.setDelay(
|
appController.setDelay(
|
||||||
Delay(
|
Delay(
|
||||||
url: url,
|
url: url,
|
||||||
name: proxyName,
|
name: proxyName,
|
||||||
value: 0,
|
value: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
globalState.appController.setDelay(
|
appController.setDelay(
|
||||||
await clashCore.getDelay(
|
await clashCore.getDelay(
|
||||||
url,
|
url,
|
||||||
proxyName,
|
proxyName,
|
||||||
@@ -60,21 +42,21 @@ proxyDelayTest(Proxy proxy, [String? testUrl]) async {
|
|||||||
delayTest(List<Proxy> proxies, [String? testUrl]) async {
|
delayTest(List<Proxy> proxies, [String? testUrl]) async {
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final proxyNames = proxies
|
final proxyNames = proxies
|
||||||
.map((proxy) => appController.appState.getRealProxyName(proxy.name))
|
.map((proxy) => appController.getRealProxyName(proxy.name))
|
||||||
.toSet()
|
.toSet()
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final url = appController.getRealTestUrl(testUrl);
|
final url = appController.getRealTestUrl(testUrl);
|
||||||
|
|
||||||
final delayProxies = proxyNames.map<Future>((proxyName) async {
|
final delayProxies = proxyNames.map<Future>((proxyName) async {
|
||||||
globalState.appController.setDelay(
|
appController.setDelay(
|
||||||
Delay(
|
Delay(
|
||||||
url: url,
|
url: url,
|
||||||
name: proxyName,
|
name: proxyName,
|
||||||
value: 0,
|
value: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
globalState.appController.setDelay(
|
appController.setDelay(
|
||||||
await clashCore.getDelay(
|
await clashCore.getDelay(
|
||||||
url,
|
url,
|
||||||
proxyName,
|
proxyName,
|
||||||
@@ -86,7 +68,7 @@ delayTest(List<Proxy> proxies, [String? testUrl]) async {
|
|||||||
for (final batchDelayProxies in batchesDelayProxies) {
|
for (final batchDelayProxies in batchesDelayProxies) {
|
||||||
await Future.wait(batchDelayProxies);
|
await Future.wait(batchDelayProxies);
|
||||||
}
|
}
|
||||||
appController.appState.sortNum++;
|
appController.addSortNum();
|
||||||
}
|
}
|
||||||
|
|
||||||
double getScrollToSelectedOffset({
|
double getScrollToSelectedOffset({
|
||||||
@@ -94,14 +76,11 @@ double getScrollToSelectedOffset({
|
|||||||
required List<Proxy> proxies,
|
required List<Proxy> proxies,
|
||||||
}) {
|
}) {
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final columns = other.getProxiesColumns(
|
final columns = appController.getProxiesColumns();
|
||||||
appController.appState.viewWidth,
|
final proxyCardType = globalState.config.proxiesStyle.cardType;
|
||||||
appController.config.proxiesStyle.layout,
|
final selectedProxyName = appController.getSelectedProxyName(groupName);
|
||||||
);
|
|
||||||
final proxyCardType = appController.config.proxiesStyle.cardType;
|
|
||||||
final selectedName = appController.getCurrentSelectedName(groupName);
|
|
||||||
final findSelectedIndex = proxies.indexWhere(
|
final findSelectedIndex = proxies.indexWhere(
|
||||||
(proxy) => proxy.name == selectedName,
|
(proxy) => proxy.name == selectedProxyName,
|
||||||
);
|
);
|
||||||
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
|
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
|
||||||
final rows = (selectedIndex / columns).floor();
|
final rows = (selectedIndex / columns).floor();
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import 'dart:math';
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/app.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
|
import 'package:fl_clash/providers/state.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'card.dart';
|
import 'card.dart';
|
||||||
import 'common.dart';
|
import 'common.dart';
|
||||||
@@ -78,7 +81,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
|||||||
} else {
|
} else {
|
||||||
tempUnfoldSet.add(groupName);
|
tempUnfoldSet.add(groupName);
|
||||||
}
|
}
|
||||||
globalState.appController.config.updateCurrentUnfoldSet(
|
globalState.appController.updateCurrentUnfoldSet(
|
||||||
tempUnfoldSet,
|
tempUnfoldSet,
|
||||||
);
|
);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -105,7 +108,8 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
|||||||
return itemHeightList;
|
return itemHeightList;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildItems({
|
List<Widget> _buildItems(
|
||||||
|
WidgetRef ref, {
|
||||||
required List<String> groupNames,
|
required List<String> groupNames,
|
||||||
required int columns,
|
required int columns,
|
||||||
required Set<String> currentUnfoldSet,
|
required Set<String> currentUnfoldSet,
|
||||||
@@ -115,7 +119,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
|||||||
final GroupNameProxiesMap groupNameProxiesMap = {};
|
final GroupNameProxiesMap groupNameProxiesMap = {};
|
||||||
for (final groupName in groupNames) {
|
for (final groupName in groupNames) {
|
||||||
final group =
|
final group =
|
||||||
globalState.appController.appState.getGroupWithName(groupName)!;
|
ref.read(groupsProvider.select((state) => state.getGroup(groupName)));
|
||||||
|
if (group == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
final isExpand = currentUnfoldSet.contains(groupName);
|
final isExpand = currentUnfoldSet.contains(groupName);
|
||||||
items.addAll([
|
items.addAll([
|
||||||
ListHeader(
|
ListHeader(
|
||||||
@@ -186,16 +193,21 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildHeader({
|
_buildHeader(
|
||||||
|
WidgetRef ref, {
|
||||||
required String groupName,
|
required String groupName,
|
||||||
required Set<String> currentUnfoldSet,
|
required Set<String> currentUnfoldSet,
|
||||||
}) {
|
}) {
|
||||||
final group =
|
final group =
|
||||||
globalState.appController.appState.getGroupWithName(groupName)!;
|
ref.read(groupsProvider.select((state) => state.getGroup(groupName)));
|
||||||
|
if (group == null) {
|
||||||
|
return SizedBox();
|
||||||
|
}
|
||||||
final isExpand = currentUnfoldSet.contains(groupName);
|
final isExpand = currentUnfoldSet.contains(groupName);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: listHeaderHeight,
|
height: listHeaderHeight,
|
||||||
child: ListHeader(
|
child: ListHeader(
|
||||||
|
enterAnimated: false,
|
||||||
onScrollToSelected: _scrollToGroupSelected,
|
onScrollToSelected: _scrollToGroupSelected,
|
||||||
key: Key(groupName),
|
key: Key(groupName),
|
||||||
isExpand: isExpand,
|
isExpand: isExpand,
|
||||||
@@ -212,7 +224,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final appController = globalState.appController;
|
final appController = globalState.appController;
|
||||||
final currentGroups = appController.appState.currentGroups;
|
final currentGroups = appController.getCurrentGroups();
|
||||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
final groupNames = currentGroups.map((e) => e.name).toList();
|
||||||
final findIndex = groupNames.indexWhere((item) => item == groupName);
|
final findIndex = groupNames.indexWhere((item) => item == groupName);
|
||||||
final index = findIndex != -1 ? findIndex : 0;
|
final index = findIndex != -1 ? findIndex : 0;
|
||||||
@@ -235,51 +247,24 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector2<AppState, Config, ProxiesListSelectorState>(
|
return Consumer(
|
||||||
selector: (_, appState, config) {
|
builder: (_, ref, __) {
|
||||||
final currentGroups = appState.currentGroups;
|
final state = ref.watch(proxiesListSelectorStateProvider);
|
||||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
|
||||||
return ProxiesListSelectorState(
|
|
||||||
groupNames: groupNames,
|
|
||||||
currentUnfoldSet: config.currentUnfoldSet,
|
|
||||||
proxyCardType: config.proxiesStyle.cardType,
|
|
||||||
proxiesSortType: config.proxiesStyle.sortType,
|
|
||||||
columns: other.getProxiesColumns(
|
|
||||||
appState.viewWidth,
|
|
||||||
config.proxiesStyle.layout,
|
|
||||||
),
|
|
||||||
sortNum: appState.sortNum,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
shouldRebuild: (prev, next) {
|
|
||||||
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
|
|
||||||
_headerStateNotifier.value = const ProxiesListHeaderSelectorState(
|
|
||||||
offset: 0,
|
|
||||||
currentIndex: 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return prev != next;
|
|
||||||
},
|
|
||||||
builder: (_, state, __) {
|
|
||||||
if (state.groupNames.isEmpty) {
|
if (state.groupNames.isEmpty) {
|
||||||
return NullStatus(
|
return NullStatus(
|
||||||
label: appLocalizations.nullProxies,
|
label: appLocalizations.nullProxies,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final items = _buildItems(
|
final items = _buildItems(
|
||||||
|
ref,
|
||||||
groupNames: state.groupNames,
|
groupNames: state.groupNames,
|
||||||
currentUnfoldSet: state.currentUnfoldSet,
|
currentUnfoldSet: state.currentUnfoldSet,
|
||||||
columns: state.columns,
|
columns: state.columns,
|
||||||
type: state.proxyCardType,
|
type: state.proxyCardType,
|
||||||
);
|
);
|
||||||
final itemsOffset = _getItemHeightList(items, state.proxyCardType);
|
final itemsOffset = _getItemHeightList(items, state.proxyCardType);
|
||||||
return Scrollbar(
|
return CommonScrollBar(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
thumbVisibility: true,
|
|
||||||
trackVisibility: true,
|
|
||||||
thickness: 8,
|
|
||||||
radius: const Radius.circular(8),
|
|
||||||
interactive: true,
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
@@ -323,6 +308,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
|
|||||||
bottom: 8,
|
bottom: 8,
|
||||||
),
|
),
|
||||||
child: _buildHeader(
|
child: _buildHeader(
|
||||||
|
ref,
|
||||||
groupName: state.groupNames[index],
|
groupName: state.groupNames[index],
|
||||||
currentUnfoldSet: state.currentUnfoldSet,
|
currentUnfoldSet: state.currentUnfoldSet,
|
||||||
),
|
),
|
||||||
@@ -348,8 +334,11 @@ class ListHeader extends StatefulWidget {
|
|||||||
final Function(String groupName) onScrollToSelected;
|
final Function(String groupName) onScrollToSelected;
|
||||||
final bool isExpand;
|
final bool isExpand;
|
||||||
|
|
||||||
|
final bool enterAnimated;
|
||||||
|
|
||||||
const ListHeader({
|
const ListHeader({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.enterAnimated = true,
|
||||||
required this.group,
|
required this.group,
|
||||||
required this.onChange,
|
required this.onChange,
|
||||||
required this.onScrollToSelected,
|
required this.onScrollToSelected,
|
||||||
@@ -422,57 +411,53 @@ class _ListHeaderState extends State<ListHeader>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildIcon() {
|
Widget _buildIcon() {
|
||||||
return Selector<Config, ProxiesIconStyle>(
|
return Consumer(
|
||||||
selector: (_, config) => config.proxiesStyle.iconStyle,
|
builder: (_, ref, child) {
|
||||||
builder: (_, iconStyle, child) {
|
final iconStyle = ref.watch(
|
||||||
return Selector<Config, String>(
|
proxiesStyleSettingProvider.select((state) => state.iconStyle));
|
||||||
selector: (_, config) {
|
final icon = ref.watch(proxiesStyleSettingProvider.select((state) {
|
||||||
final iconMapEntryList =
|
final iconMapEntryList = state.iconMap.entries.toList();
|
||||||
config.proxiesStyle.iconMap.entries.toList();
|
final index = iconMapEntryList.indexWhere((item) {
|
||||||
final index = iconMapEntryList.indexWhere((item) {
|
try {
|
||||||
try {
|
return RegExp(item.key).hasMatch(groupName);
|
||||||
return RegExp(item.key).hasMatch(groupName);
|
} catch (_) {
|
||||||
} catch (_) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (index != -1) {
|
|
||||||
return iconMapEntryList[index].value;
|
|
||||||
}
|
}
|
||||||
return icon;
|
});
|
||||||
},
|
if (index != -1) {
|
||||||
builder: (_, icon, __) {
|
return iconMapEntryList[index].value;
|
||||||
return switch (iconStyle) {
|
}
|
||||||
ProxiesIconStyle.standard => Container(
|
return this.icon;
|
||||||
height: 48,
|
}));
|
||||||
width: 48,
|
return switch (iconStyle) {
|
||||||
margin: const EdgeInsets.only(
|
ProxiesIconStyle.standard => Container(
|
||||||
right: 16,
|
height: 48,
|
||||||
),
|
width: 48,
|
||||||
padding: const EdgeInsets.all(8),
|
margin: const EdgeInsets.only(
|
||||||
decoration: BoxDecoration(
|
right: 16,
|
||||||
color: context.colorScheme.secondaryContainer,
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
padding: const EdgeInsets.all(8),
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
clipBehavior: Clip.antiAlias,
|
color: context.colorScheme.secondaryContainer,
|
||||||
child: CommonTargetIcon(
|
borderRadius: BorderRadius.circular(12),
|
||||||
src: icon,
|
),
|
||||||
size: 32,
|
clipBehavior: Clip.antiAlias,
|
||||||
),
|
child: CommonTargetIcon(
|
||||||
),
|
src: icon,
|
||||||
ProxiesIconStyle.icon => Container(
|
size: 32,
|
||||||
margin: const EdgeInsets.only(
|
),
|
||||||
right: 16,
|
),
|
||||||
),
|
ProxiesIconStyle.icon => Container(
|
||||||
child: CommonTargetIcon(
|
margin: const EdgeInsets.only(
|
||||||
src: icon,
|
right: 16,
|
||||||
size: 42,
|
),
|
||||||
),
|
child: CommonTargetIcon(
|
||||||
),
|
src: icon,
|
||||||
ProxiesIconStyle.none => Container(),
|
size: 42,
|
||||||
};
|
),
|
||||||
},
|
),
|
||||||
);
|
ProxiesIconStyle.none => Container(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -480,13 +465,15 @@ class _ListHeaderState extends State<ListHeader>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CommonCard(
|
return CommonCard(
|
||||||
|
enterAnimated: widget.enterAnimated,
|
||||||
key: widget.key,
|
key: widget.key,
|
||||||
|
borderSide: WidgetStatePropertyAll(BorderSide.none),
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
context.colorScheme.surfaceContainer,
|
context.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
radius: 14,
|
radius: 14,
|
||||||
type: CommonCardType.filled,
|
type: CommonCardType.filled,
|
||||||
child: Container(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 12,
|
vertical: 12,
|
||||||
@@ -523,9 +510,13 @@ class _ListHeaderState extends State<ListHeader>
|
|||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
child: currentSelectedProxyNameBuilder(
|
child: Consumer(
|
||||||
groupName: groupName,
|
builder: (_, ref, __) {
|
||||||
builder: (currentGroupName) {
|
final proxyName = ref
|
||||||
|
.watch(getSelectedProxyNameProvider(
|
||||||
|
groupName,
|
||||||
|
))
|
||||||
|
.getSafeValue("");
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
@@ -533,12 +524,12 @@ class _ListHeaderState extends State<ListHeader>
|
|||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
CrossAxisAlignment.center,
|
CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (currentGroupName.isNotEmpty) ...[
|
if (proxyName.isNotEmpty) ...[
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
child: EmojiText(
|
child: EmojiText(
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
" · $currentGroupName",
|
" · $proxyName",
|
||||||
style: context.textTheme
|
style: context.textTheme
|
||||||
.labelMedium?.toLight,
|
.labelMedium?.toLight,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,34 +3,32 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:fl_clash/clash/clash.dart';
|
import 'package:fl_clash/clash/clash.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/app.dart';
|
|
||||||
import 'package:fl_clash/models/core.dart';
|
import 'package:fl_clash/models/core.dart';
|
||||||
|
import 'package:fl_clash/providers/app.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
typedef UpdatingMap = Map<String, bool>;
|
typedef UpdatingMap = Map<String, bool>;
|
||||||
|
|
||||||
class Providers extends StatefulWidget {
|
class ProvidersView extends ConsumerStatefulWidget {
|
||||||
const Providers({
|
const ProvidersView({
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<Providers> createState() => _ProvidersState();
|
ConsumerState<ProvidersView> createState() => _ProvidersViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProvidersState extends State<Providers> {
|
class _ProvidersViewState extends ConsumerState<ProvidersView> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
(_) {
|
(_) {
|
||||||
globalState.appController.updateProviders();
|
globalState.appController.updateProviders();
|
||||||
final commonScaffoldState =
|
context.commonScaffoldState?.actions = [
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_updateProviders();
|
_updateProviders();
|
||||||
@@ -45,12 +43,12 @@ class _ProvidersState extends State<Providers> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateProviders() async {
|
_updateProviders() async {
|
||||||
final appState = globalState.appController.appState;
|
final providers = ref.read(providersProvider);
|
||||||
final providers = globalState.appController.appState.providers;
|
final providersNotifier = ref.read(providersProvider.notifier);
|
||||||
final messages = [];
|
final messages = [];
|
||||||
final updateProviders = providers.map<Future>(
|
final updateProviders = providers.map<Future>(
|
||||||
(provider) async {
|
(provider) async {
|
||||||
appState.setProvider(
|
providersNotifier.setProvider(
|
||||||
provider.copyWith(isUpdating: true),
|
provider.copyWith(isUpdating: true),
|
||||||
);
|
);
|
||||||
final message = await clashCore.updateExternalProvider(
|
final message = await clashCore.updateExternalProvider(
|
||||||
@@ -59,7 +57,7 @@ class _ProvidersState extends State<Providers> {
|
|||||||
if (message.isNotEmpty) {
|
if (message.isNotEmpty) {
|
||||||
messages.add("${provider.name}: $message \n");
|
messages.add("${provider.name}: $message \n");
|
||||||
}
|
}
|
||||||
appState.setProvider(
|
providersNotifier.setProvider(
|
||||||
await clashCore.getExternalProvider(provider.name),
|
await clashCore.getExternalProvider(provider.name),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -85,35 +83,29 @@ class _ProvidersState extends State<Providers> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<AppState, List<ExternalProvider>>(
|
final providers = ref.watch(providersProvider);
|
||||||
selector: (_, appState) => appState.providers,
|
final proxyProviders = providers.where((item) => item.type == "Proxy").map(
|
||||||
builder: (_, providers, ___) {
|
(item) => ProviderItem(
|
||||||
final proxyProviders =
|
provider: item,
|
||||||
providers.where((item) => item.type == "Proxy").map(
|
),
|
||||||
(item) => ProviderItem(
|
|
||||||
provider: item,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final ruleProviders =
|
|
||||||
providers.where((item) => item.type == "Rule").map(
|
|
||||||
(item) => ProviderItem(
|
|
||||||
provider: item,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final proxySection = generateSection(
|
|
||||||
title: appLocalizations.proxyProviders,
|
|
||||||
items: proxyProviders,
|
|
||||||
);
|
);
|
||||||
final ruleSection = generateSection(
|
final ruleProviders = providers.where((item) => item.type == "Rule").map(
|
||||||
title: appLocalizations.ruleProviders,
|
(item) => ProviderItem(
|
||||||
items: ruleProviders,
|
provider: item,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return generateListView([
|
final proxySection = generateSection(
|
||||||
...proxySection,
|
title: appLocalizations.proxyProviders,
|
||||||
...ruleSection,
|
items: proxyProviders,
|
||||||
]);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
final ruleSection = generateSection(
|
||||||
|
title: appLocalizations.ruleProviders,
|
||||||
|
items: ruleProviders,
|
||||||
|
);
|
||||||
|
return generateListView([
|
||||||
|
...proxySection,
|
||||||
|
...ruleSection,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,11 +118,11 @@ class ProviderItem extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
_handleUpdateProvider() async {
|
_handleUpdateProvider() async {
|
||||||
final appState = globalState.appController.appState;
|
final appController = globalState.appController;
|
||||||
if (provider.vehicleType != "HTTP") return;
|
if (provider.vehicleType != "HTTP") return;
|
||||||
await globalState.safeRun(
|
await globalState.safeRun(
|
||||||
() async {
|
() async {
|
||||||
appState.setProvider(
|
appController.setProvider(
|
||||||
provider.copyWith(
|
provider.copyWith(
|
||||||
isUpdating: true,
|
isUpdating: true,
|
||||||
),
|
),
|
||||||
@@ -142,7 +134,7 @@ class ProviderItem extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
silence: false,
|
silence: false,
|
||||||
);
|
);
|
||||||
appState.setProvider(
|
appController.setProvider(
|
||||||
await clashCore.getExternalProvider(provider.name),
|
await clashCore.getExternalProvider(provider.name),
|
||||||
);
|
);
|
||||||
await globalState.appController.updateGroupsDebounce();
|
await globalState.appController.updateGroupsDebounce();
|
||||||
@@ -151,7 +143,6 @@ class ProviderItem extends StatelessWidget {
|
|||||||
_handleSideLoadProvider() async {
|
_handleSideLoadProvider() async {
|
||||||
await globalState.safeRun<void>(() async {
|
await globalState.safeRun<void>(() async {
|
||||||
final platformFile = await picker.pickerFile();
|
final platformFile = await picker.pickerFile();
|
||||||
final appState = globalState.appController.appState;
|
|
||||||
final bytes = platformFile?.bytes;
|
final bytes = platformFile?.bytes;
|
||||||
if (bytes == null || provider.path == null) return;
|
if (bytes == null || provider.path == null) return;
|
||||||
final file = await File(provider.path!).create(recursive: true);
|
final file = await File(provider.path!).create(recursive: true);
|
||||||
@@ -162,7 +153,7 @@ class ProviderItem extends StatelessWidget {
|
|||||||
data: utf8.decode(bytes),
|
data: utf8.decode(bytes),
|
||||||
);
|
);
|
||||||
if (message.isNotEmpty) throw message;
|
if (message.isNotEmpty) throw message;
|
||||||
appState.setProvider(
|
globalState.appController.setProvider(
|
||||||
await clashCore.getExternalProvider(provider.name),
|
await clashCore.getExternalProvider(provider.name),
|
||||||
);
|
);
|
||||||
if (message.isNotEmpty) throw message;
|
if (message.isNotEmpty) throw message;
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/fragments/proxies/list.dart';
|
import 'package:fl_clash/fragments/proxies/list.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/fragments/proxies/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'providers.dart';
|
import 'common.dart';
|
||||||
import 'setting.dart';
|
import 'setting.dart';
|
||||||
import 'tab.dart';
|
import 'tab.dart';
|
||||||
|
|
||||||
class ProxiesFragment extends StatefulWidget {
|
class ProxiesFragment extends ConsumerStatefulWidget {
|
||||||
const ProxiesFragment({super.key});
|
const ProxiesFragment({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ProxiesFragment> createState() => _ProxiesFragmentState();
|
ConsumerState<ProxiesFragment> createState() => _ProxiesFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ProxiesFragmentState extends State<ProxiesFragment> {
|
class _ProxiesFragmentState extends ConsumerState<ProxiesFragment>
|
||||||
|
with PageMixin {
|
||||||
final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey();
|
final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey();
|
||||||
|
bool _hasProviders = false;
|
||||||
|
bool _isTab = false;
|
||||||
|
|
||||||
_initActions(ProxiesType proxiesType, bool hasProvider) {
|
@override
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
get actions => [
|
||||||
final commonScaffoldState =
|
if (_hasProviders)
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
if (hasProvider) ...[
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showExtendPage(
|
showExtendPage(
|
||||||
isScaffold: true,
|
isScaffold: true,
|
||||||
extendPageWidth: 360,
|
extendPageWidth: 360,
|
||||||
context,
|
context,
|
||||||
body: const Providers(),
|
body: const ProvidersView(),
|
||||||
title: appLocalizations.providers,
|
title: appLocalizations.providers,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -40,71 +40,28 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
|
|||||||
Icons.poll_outlined,
|
Icons.poll_outlined,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
_isTab
|
||||||
if (proxiesType == ProxiesType.tab) ...[
|
? IconButton(
|
||||||
IconButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
_proxiesTabKey.currentState?.scrollToGroupSelected();
|
||||||
_proxiesTabKey.currentState?.scrollToGroupSelected();
|
},
|
||||||
},
|
icon: const Icon(
|
||||||
icon: const Icon(
|
Icons.adjust_outlined,
|
||||||
Icons.adjust_outlined,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
showExtendPage(
|
|
||||||
context,
|
|
||||||
extendPageWidth: 360,
|
|
||||||
title: appLocalizations.iconConfiguration,
|
|
||||||
body: Selector<Config, Map<String, String>>(
|
|
||||||
selector: (_, config) => config.proxiesStyle.iconMap,
|
|
||||||
shouldRebuild: (prev, next) {
|
|
||||||
return !stringAndStringMapEntryIterableEquality.equals(
|
|
||||||
prev.entries,
|
|
||||||
next.entries,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
builder: (_, iconMap, __) {
|
|
||||||
final entries = iconMap.entries.toList();
|
|
||||||
return ListPage(
|
|
||||||
title: appLocalizations.iconConfiguration,
|
|
||||||
items: entries,
|
|
||||||
keyLabel: appLocalizations.regExp,
|
|
||||||
valueLabel: appLocalizations.icon,
|
|
||||||
keyBuilder: (item) => Key(item.key),
|
|
||||||
titleBuilder: (item) => Text(item.key),
|
|
||||||
leadingBuilder: (item) => Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: CommonTargetIcon(
|
|
||||||
src: item.value,
|
|
||||||
size: 42,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitleBuilder: (item) => Text(
|
|
||||||
item.value,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
onChange: (entries) {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.proxiesStyle = config.proxiesStyle.copyWith(
|
|
||||||
iconMap: Map.fromEntries(entries),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
},
|
: IconButton(
|
||||||
icon: const Icon(
|
onPressed: () {
|
||||||
Icons.style_outlined,
|
showExtendPage(
|
||||||
),
|
context,
|
||||||
),
|
extendPageWidth: 360,
|
||||||
],
|
title: appLocalizations.iconConfiguration,
|
||||||
|
body: _IconConfigView(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.style_outlined,
|
||||||
|
),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showSheet(
|
showSheet(
|
||||||
@@ -118,28 +75,88 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
});
|
|
||||||
|
@override
|
||||||
|
get floatingActionButton => _isTab
|
||||||
|
? DelayTestButton(
|
||||||
|
onClick: () async {
|
||||||
|
await delayTest(
|
||||||
|
currentTabProxies,
|
||||||
|
currentTabTestUrl,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
ref.listenManual(
|
||||||
|
proxiesActionsStateProvider,
|
||||||
|
fireImmediately: true,
|
||||||
|
(prev, next) {
|
||||||
|
if (prev == next) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (next.pageLabel == PageLabel.proxies) {
|
||||||
|
_hasProviders = next.hasProviders;
|
||||||
|
_isTab = next.type == ProxiesType.tab;
|
||||||
|
initPageState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<Config, ProxiesType>(
|
final proxiesType =
|
||||||
selector: (_, config) => config.proxiesStyle.type,
|
ref.watch(proxiesStyleSettingProvider.select((state) => state.type));
|
||||||
builder: (_, proxiesType, __) {
|
return switch (proxiesType) {
|
||||||
return ProxiesActionsBuilder(
|
ProxiesType.tab => ProxiesTabFragment(
|
||||||
builder: (state, child) {
|
key: _proxiesTabKey,
|
||||||
if (state.isCurrent) {
|
),
|
||||||
_initActions(proxiesType, state.hasProvider);
|
ProxiesType.list => const ProxiesListFragment(),
|
||||||
}
|
};
|
||||||
return child!;
|
}
|
||||||
},
|
}
|
||||||
child: switch (proxiesType) {
|
|
||||||
ProxiesType.tab => ProxiesTabFragment(
|
class _IconConfigView extends ConsumerWidget {
|
||||||
key: _proxiesTabKey,
|
const _IconConfigView();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final iconMap =
|
||||||
|
ref.watch(proxiesStyleSettingProvider.select((state) => state.iconMap));
|
||||||
|
final entries = iconMap.entries.toList();
|
||||||
|
return ListPage(
|
||||||
|
title: appLocalizations.iconConfiguration,
|
||||||
|
items: entries,
|
||||||
|
keyLabel: appLocalizations.regExp,
|
||||||
|
valueLabel: appLocalizations.icon,
|
||||||
|
keyBuilder: (item) => Key(item.key),
|
||||||
|
titleBuilder: (item) => Text(item.key),
|
||||||
|
leadingBuilder: (item) => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: CommonTargetIcon(
|
||||||
|
src: item.value,
|
||||||
|
size: 42,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitleBuilder: (item) => Text(
|
||||||
|
item.value,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
onChange: (entries) {
|
||||||
|
ref.read(proxiesStyleSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
iconMap: Map.fromEntries(entries),
|
||||||
),
|
),
|
||||||
ProxiesType.list => const ProxiesListFragment(),
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -1,34 +1,40 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/models/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'card.dart';
|
import 'card.dart';
|
||||||
import 'common.dart';
|
import 'common.dart';
|
||||||
|
|
||||||
List<Proxy> currentProxies = [];
|
List<Proxy> currentTabProxies = [];
|
||||||
String? currentTestUrl;
|
String? currentTabTestUrl;
|
||||||
|
|
||||||
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
|
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
|
||||||
|
|
||||||
class ProxiesTabFragment extends StatefulWidget {
|
class ProxiesTabFragment extends ConsumerStatefulWidget {
|
||||||
const ProxiesTabFragment({super.key});
|
const ProxiesTabFragment({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ProxiesTabFragment> createState() => ProxiesTabFragmentState();
|
ConsumerState<ProxiesTabFragment> createState() => ProxiesTabFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
class ProxiesTabFragmentState extends ConsumerState<ProxiesTabFragment>
|
||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
TabController? _tabController;
|
TabController? _tabController;
|
||||||
final _hasMoreButtonNotifier = ValueNotifier<bool>(false);
|
final _hasMoreButtonNotifier = ValueNotifier<bool>(false);
|
||||||
GroupNameKeyMap _keyMap = {};
|
GroupNameKeyMap _keyMap = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_handleTabListen();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_destroyTabController();
|
_destroyTabController();
|
||||||
@@ -36,17 +42,17 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollToGroupSelected() {
|
scrollToGroupSelected() {
|
||||||
final currentGroupName = globalState.appController.config.currentGroupName;
|
final currentGroupName = globalState.appController.getCurrentGroupName();
|
||||||
_keyMap[currentGroupName]?.currentState?.scrollToSelected();
|
_keyMap[currentGroupName]?.currentState?.scrollToSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildMoreButton() {
|
_buildMoreButton() {
|
||||||
return Selector<AppState, bool>(
|
return Consumer(
|
||||||
selector: (_, appState) => appState.viewMode == ViewMode.mobile,
|
builder: (_, ref, ___) {
|
||||||
builder: (_, value, ___) {
|
final isMobileView = ref.watch(viewWidthProvider.notifier).isMobileView;
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: _showMoreMenu,
|
onPressed: _showMoreMenu,
|
||||||
icon: value
|
icon: isMobileView
|
||||||
? const Icon(
|
? const Icon(
|
||||||
Icons.expand_more,
|
Icons.expand_more,
|
||||||
)
|
)
|
||||||
@@ -65,16 +71,9 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
|||||||
isScrollControlled: false,
|
isScrollControlled: false,
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Selector2<AppState, Config, ProxiesSelectorState>(
|
child: Consumer(
|
||||||
selector: (_, appState, config) {
|
builder: (_, ref, __) {
|
||||||
final currentGroups = appState.currentGroups;
|
final state = ref.watch(proxiesSelectorStateProvider);
|
||||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
|
||||||
return ProxiesSelectorState(
|
|
||||||
groupNames: groupNames,
|
|
||||||
currentGroupName: config.currentGroupName,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
builder: (_, state, __) {
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
@@ -86,13 +85,13 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
|||||||
SettingTextCard(
|
SettingTextCard(
|
||||||
groupName,
|
groupName,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final index = state.groupNames
|
final index = state.groupNames.indexWhere(
|
||||||
.indexWhere((item) => item == groupName);
|
(item) => item == groupName,
|
||||||
|
);
|
||||||
if (index == -1) return;
|
if (index == -1) return;
|
||||||
_tabController?.animateTo(index);
|
_tabController?.animateTo(index);
|
||||||
globalState.appController.config.updateCurrentGroupName(
|
globalState.appController
|
||||||
groupName,
|
.updateCurrentGroupName(groupName);
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
isSelected: groupName == state.currentGroupName,
|
isSelected: groupName == state.currentGroupName,
|
||||||
@@ -108,16 +107,24 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
|||||||
}
|
}
|
||||||
|
|
||||||
_tabControllerListener([int? index]) {
|
_tabControllerListener([int? index]) {
|
||||||
final appController = globalState.appController;
|
int? groupIndex = index;
|
||||||
final currentGroups = appController.appState.currentGroups;
|
if(groupIndex == -1){
|
||||||
if (_tabController?.index == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final currentGroup = currentGroups[index ?? _tabController!.index];
|
final appController = globalState.appController;
|
||||||
currentProxies = currentGroup.all;
|
if (groupIndex == null) {
|
||||||
currentTestUrl = currentGroup.testUrl;
|
final currentIndex = _tabController?.index;
|
||||||
|
groupIndex = currentIndex;
|
||||||
|
}
|
||||||
|
final currentGroups = appController.getCurrentGroups();
|
||||||
|
if (groupIndex == null || groupIndex > currentGroups.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final currentGroup = currentGroups[groupIndex];
|
||||||
|
currentTabProxies = currentGroup.all;
|
||||||
|
currentTabTestUrl = currentGroup.testUrl;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
appController.config.updateCurrentGroupName(
|
globalState.appController.updateCurrentGroupName(
|
||||||
currentGroup.name,
|
currentGroup.name,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -144,122 +151,117 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
|
|||||||
_tabController?.addListener(_tabControllerListener);
|
_tabController?.addListener(_tabControllerListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleTabListen() {
|
||||||
|
ref.listenManual(
|
||||||
|
proxiesSelectorStateProvider,
|
||||||
|
(prev, next) {
|
||||||
|
if (prev == next) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (prev?.groupNames.length != next.groupNames.length) {
|
||||||
|
_destroyTabController();
|
||||||
|
final index = next.groupNames.indexWhere(
|
||||||
|
(item) => item == next.currentGroupName,
|
||||||
|
);
|
||||||
|
_updateTabController(next.groupNames.length, index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector2<AppState, Config, ProxiesSelectorState>(
|
final state = ref.watch(groupNamesStateProvider);
|
||||||
selector: (_, appState, config) {
|
final groupNames = state.groupNames;
|
||||||
final currentGroups = appState.currentGroups;
|
if (groupNames.isEmpty) {
|
||||||
final groupNames = currentGroups.map((e) => e.name).toList();
|
return NullStatus(
|
||||||
return ProxiesSelectorState(
|
label: appLocalizations.nullProxies,
|
||||||
groupNames: groupNames,
|
);
|
||||||
currentGroupName: config.currentGroupName,
|
}
|
||||||
);
|
final GroupNameKeyMap keyMap = {};
|
||||||
},
|
final children = groupNames.map((groupName) {
|
||||||
shouldRebuild: (prev, next) {
|
keyMap[groupName] = GlobalObjectKey(groupName);
|
||||||
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
|
return KeepScope(
|
||||||
_destroyTabController();
|
child: ProxyGroupView(
|
||||||
return true;
|
key: keyMap[groupName],
|
||||||
}
|
groupName: groupName,
|
||||||
return false;
|
),
|
||||||
},
|
);
|
||||||
builder: (_, state, __) {
|
}).toList();
|
||||||
if (state.groupNames.isEmpty) {
|
_keyMap = keyMap;
|
||||||
return NullStatus(
|
return Column(
|
||||||
label: appLocalizations.nullProxies,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
);
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
}
|
children: [
|
||||||
final index = state.groupNames.indexWhere(
|
NotificationListener<ScrollMetricsNotification>(
|
||||||
(item) => item == state.currentGroupName,
|
onNotification: (scrollNotification) {
|
||||||
);
|
_hasMoreButtonNotifier.value =
|
||||||
_updateTabController(state.groupNames.length, index);
|
scrollNotification.metrics.maxScrollExtent > 0;
|
||||||
if (state.groupNames.isEmpty) {
|
return true;
|
||||||
return Container();
|
},
|
||||||
}
|
child: ValueListenableBuilder(
|
||||||
final GroupNameKeyMap keyMap = {};
|
valueListenable: _hasMoreButtonNotifier,
|
||||||
final children = state.groupNames.map((groupName) {
|
builder: (_, value, child) {
|
||||||
keyMap[groupName] = GlobalObjectKey(groupName);
|
return Stack(
|
||||||
return KeepScope(
|
alignment: AlignmentDirectional.centerStart,
|
||||||
child: ProxyGroupView(
|
children: [
|
||||||
key: keyMap[groupName],
|
TabBar(
|
||||||
groupName: groupName,
|
controller: _tabController,
|
||||||
),
|
padding: EdgeInsets.only(
|
||||||
);
|
left: 16,
|
||||||
}).toList();
|
right: 16 + (value ? 16 : 0),
|
||||||
_keyMap = keyMap;
|
),
|
||||||
return Column(
|
dividerColor: Colors.transparent,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
isScrollable: true,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
tabAlignment: TabAlignment.start,
|
||||||
children: [
|
overlayColor:
|
||||||
NotificationListener<ScrollMetricsNotification>(
|
const WidgetStatePropertyAll(Colors.transparent),
|
||||||
onNotification: (scrollNotification) {
|
tabs: [
|
||||||
_hasMoreButtonNotifier.value =
|
for (final groupName in groupNames)
|
||||||
scrollNotification.metrics.maxScrollExtent > 0;
|
Tab(
|
||||||
return true;
|
text: groupName,
|
||||||
},
|
|
||||||
child: ValueListenableBuilder(
|
|
||||||
valueListenable: _hasMoreButtonNotifier,
|
|
||||||
builder: (_, value, child) {
|
|
||||||
return Stack(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
children: [
|
|
||||||
TabBar(
|
|
||||||
controller: _tabController,
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: 16,
|
|
||||||
right: 16 + (value ? 16 : 0),
|
|
||||||
),
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
isScrollable: true,
|
|
||||||
tabAlignment: TabAlignment.start,
|
|
||||||
overlayColor:
|
|
||||||
const WidgetStatePropertyAll(Colors.transparent),
|
|
||||||
tabs: [
|
|
||||||
for (final groupName in state.groupNames)
|
|
||||||
Tab(
|
|
||||||
text: groupName,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (value)
|
|
||||||
Positioned(
|
|
||||||
right: 0,
|
|
||||||
child: child!,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.centerLeft,
|
|
||||||
end: Alignment.centerRight,
|
|
||||||
colors: [
|
|
||||||
context.colorScheme.surface.withOpacity(0.1),
|
|
||||||
context.colorScheme.surface,
|
|
||||||
],
|
|
||||||
stops: const [
|
|
||||||
0.0,
|
|
||||||
0.1
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
child: _buildMoreButton(),
|
if (value)
|
||||||
),
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
child: child!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
colors: [
|
||||||
|
context.colorScheme.surface.withOpacity(0.1),
|
||||||
|
context.colorScheme.surface,
|
||||||
|
],
|
||||||
|
stops: const [
|
||||||
|
0.0,
|
||||||
|
0.1
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
|
child: _buildMoreButton(),
|
||||||
),
|
),
|
||||||
Expanded(
|
),
|
||||||
child: TabBarView(
|
),
|
||||||
controller: _tabController,
|
Expanded(
|
||||||
children: children,
|
child: TabBarView(
|
||||||
),
|
controller: _tabController,
|
||||||
)
|
children: children,
|
||||||
],
|
),
|
||||||
);
|
)
|
||||||
},
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProxyGroupView extends StatefulWidget {
|
class ProxyGroupView extends ConsumerStatefulWidget {
|
||||||
final String groupName;
|
final String groupName;
|
||||||
|
|
||||||
const ProxyGroupView({
|
const ProxyGroupView({
|
||||||
@@ -268,25 +270,14 @@ class ProxyGroupView extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ProxyGroupView> createState() => ProxyGroupViewState();
|
ConsumerState<ProxyGroupView> createState() => ProxyGroupViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProxyGroupViewState extends State<ProxyGroupView> {
|
class ProxyGroupViewState extends ConsumerState<ProxyGroupView> {
|
||||||
var isLock = false;
|
|
||||||
final _controller = ScrollController();
|
final _controller = ScrollController();
|
||||||
|
|
||||||
String get groupName => widget.groupName;
|
String get groupName => widget.groupName;
|
||||||
|
|
||||||
_delayTest() async {
|
|
||||||
if (isLock) return;
|
|
||||||
isLock = true;
|
|
||||||
await delayTest(
|
|
||||||
currentProxies,
|
|
||||||
currentTestUrl,
|
|
||||||
);
|
|
||||||
isLock = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
@@ -297,9 +288,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
|||||||
if (_controller.position.maxScrollExtent == 0) {
|
if (_controller.position.maxScrollExtent == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final sortedProxies = globalState.appController.getSortProxies(
|
final sortedProxies = globalState.appController.getSortProxies(
|
||||||
currentProxies,
|
currentTabProxies,
|
||||||
currentTestUrl,
|
currentTabTestUrl,
|
||||||
);
|
);
|
||||||
_controller.animateTo(
|
_controller.animateTo(
|
||||||
min(
|
min(
|
||||||
@@ -315,85 +307,45 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
initFab(bool isCurrent) {
|
|
||||||
if (!isCurrent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
final commonScaffoldState =
|
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.floatingActionButton = DelayTestButton(
|
|
||||||
onClick: () async {
|
|
||||||
await _delayTest();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector2<AppState, Config, ProxyGroupSelectorState>(
|
final state = ref.watch(proxyGroupSelectorStateProvider(groupName));
|
||||||
selector: (_, appState, config) {
|
final proxies = state.proxies;
|
||||||
final group = appState.getGroupWithName(groupName)!;
|
final columns = state.columns;
|
||||||
return ProxyGroupSelectorState(
|
final proxyCardType = state.proxyCardType;
|
||||||
proxyCardType: config.proxiesStyle.cardType,
|
final sortedProxies = globalState.appController.getSortProxies(
|
||||||
proxiesSortType: config.proxiesStyle.sortType,
|
proxies,
|
||||||
columns: other.getProxiesColumns(
|
state.testUrl,
|
||||||
appState.viewWidth,
|
);
|
||||||
config.proxiesStyle.layout,
|
return Align(
|
||||||
),
|
alignment: Alignment.topCenter,
|
||||||
sortNum: appState.sortNum,
|
child: GridView.builder(
|
||||||
proxies: group.all,
|
controller: _controller,
|
||||||
groupType: group.type,
|
padding: const EdgeInsets.only(
|
||||||
testUrl: group.testUrl,
|
top: 16,
|
||||||
);
|
left: 16,
|
||||||
},
|
right: 16,
|
||||||
builder: (_, state, __) {
|
bottom: 96,
|
||||||
final proxies = state.proxies;
|
),
|
||||||
final columns = state.columns;
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
final proxyCardType = state.proxyCardType;
|
crossAxisCount: columns,
|
||||||
final sortedProxies = globalState.appController.getSortProxies(
|
mainAxisSpacing: 8,
|
||||||
proxies,
|
crossAxisSpacing: 8,
|
||||||
state.testUrl,
|
mainAxisExtent: getItemHeight(proxyCardType),
|
||||||
);
|
),
|
||||||
return ActiveBuilder(
|
itemCount: sortedProxies.length,
|
||||||
label: "proxies",
|
itemBuilder: (_, index) {
|
||||||
builder: (isCurrent, child) {
|
final proxy = sortedProxies[index];
|
||||||
initFab(isCurrent);
|
return ProxyCard(
|
||||||
return child!;
|
testUrl: state.testUrl,
|
||||||
},
|
groupType: state.groupType,
|
||||||
child: Align(
|
type: proxyCardType,
|
||||||
alignment: Alignment.topCenter,
|
key: ValueKey('$groupName.${proxy.name}'),
|
||||||
child: GridView.builder(
|
proxy: proxy,
|
||||||
controller: _controller,
|
groupName: groupName,
|
||||||
padding: const EdgeInsets.only(
|
);
|
||||||
top: 16,
|
},
|
||||||
left: 16,
|
),
|
||||||
right: 16,
|
|
||||||
bottom: 96,
|
|
||||||
),
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: columns,
|
|
||||||
mainAxisSpacing: 8,
|
|
||||||
crossAxisSpacing: 8,
|
|
||||||
mainAxisExtent: getItemHeight(proxyCardType),
|
|
||||||
),
|
|
||||||
itemCount: sortedProxies.length,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final proxy = sortedProxies[index];
|
|
||||||
return ProxyCard(
|
|
||||||
testUrl: state.testUrl,
|
|
||||||
groupType: state.groupType,
|
|
||||||
type: proxyCardType,
|
|
||||||
key: ValueKey('$groupName.${proxy.name}'),
|
|
||||||
proxy: proxy,
|
|
||||||
groupName: groupName,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,6 +368,9 @@ class _DelayTestButtonState extends State<DelayTestButton>
|
|||||||
late Animation<double> _scale;
|
late Animation<double> _scale;
|
||||||
|
|
||||||
_healthcheck() async {
|
_healthcheck() async {
|
||||||
|
if (_controller.isAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_controller.forward();
|
_controller.forward();
|
||||||
await widget.onClick();
|
await widget.onClick();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
@@ -1,317 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:fl_clash/common/common.dart';
|
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
|
||||||
import 'package:fl_clash/models/models.dart';
|
|
||||||
import 'package:fl_clash/state.dart';
|
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class RequestsFragment extends StatefulWidget {
|
|
||||||
const RequestsFragment({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<RequestsFragment> createState() => _RequestsFragmentState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RequestsFragmentState extends State<RequestsFragment> {
|
|
||||||
final requestsNotifier =
|
|
||||||
ValueNotifier<ConnectionsAndKeywords>(const ConnectionsAndKeywords());
|
|
||||||
final ScrollController _scrollController = ScrollController(
|
|
||||||
keepScrollOffset: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
Timer? timer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
final appState = globalState.appController.appState;
|
|
||||||
requestsNotifier.value =
|
|
||||||
requestsNotifier.value.copyWith(connections: appState.requests);
|
|
||||||
if (timer != null) {
|
|
||||||
timer?.cancel();
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
|
|
||||||
final maxLength = Platform.isAndroid ? 1000 : 60;
|
|
||||||
final requests = appState.requests.safeSublist(
|
|
||||||
appState.requests.length - maxLength,
|
|
||||||
);
|
|
||||||
if (!connectionListEquality.equals(
|
|
||||||
requestsNotifier.value.connections,
|
|
||||||
requests,
|
|
||||||
)) {
|
|
||||||
requestsNotifier.value =
|
|
||||||
requestsNotifier.value.copyWith(connections: requests);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_initActions() {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
|
||||||
(_) {
|
|
||||||
final commonScaffoldState =
|
|
||||||
context.findAncestorStateOfType<CommonScaffoldState>();
|
|
||||||
commonScaffoldState?.actions = [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
showSearch(
|
|
||||||
context: context,
|
|
||||||
delegate: RequestsSearchDelegate(
|
|
||||||
state: requestsNotifier.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.search),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_addKeyword(String keyword) {
|
|
||||||
final isContains = requestsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (isContains) return;
|
|
||||||
final keywords = List<String>.from(requestsNotifier.value.keywords)
|
|
||||||
..add(keyword);
|
|
||||||
requestsNotifier.value = requestsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_deleteKeyword(String keyword) {
|
|
||||||
final isContains = requestsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (!isContains) return;
|
|
||||||
final keywords = List<String>.from(requestsNotifier.value.keywords)
|
|
||||||
..remove(keyword);
|
|
||||||
requestsNotifier.value = requestsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
timer?.cancel();
|
|
||||||
_scrollController.dispose();
|
|
||||||
timer = null;
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<AppState, bool?>(
|
|
||||||
selector: (_, appState) =>
|
|
||||||
appState.currentLabel == 'requests' ||
|
|
||||||
appState.viewMode == ViewMode.mobile &&
|
|
||||||
appState.currentLabel == "tools",
|
|
||||||
builder: (_, isCurrent, child) {
|
|
||||||
if (isCurrent == null || isCurrent) {
|
|
||||||
_initActions();
|
|
||||||
}
|
|
||||||
return child!;
|
|
||||||
},
|
|
||||||
child: ValueListenableBuilder<ConnectionsAndKeywords>(
|
|
||||||
valueListenable: requestsNotifier,
|
|
||||||
builder: (_, state, __) {
|
|
||||||
var connections = state.filteredConnections;
|
|
||||||
if (connections.isEmpty) {
|
|
||||||
return NullStatus(
|
|
||||||
label: appLocalizations.nullRequestsDesc,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
connections = connections.reversed.toList();
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (state.keywords.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 6,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
for (final keyword in state.keywords)
|
|
||||||
CommonChip(
|
|
||||||
label: keyword,
|
|
||||||
type: ChipType.delete,
|
|
||||||
onPressed: () {
|
|
||||||
_deleteKeyword(keyword);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.separated(
|
|
||||||
controller: _scrollController,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final connection = connections[index];
|
|
||||||
return ConnectionItem(
|
|
||||||
key: Key(connection.id),
|
|
||||||
connection: connection,
|
|
||||||
onClick: _addKeyword,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
|
||||||
return const Divider(
|
|
||||||
height: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: connections.length,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RequestsSearchDelegate extends SearchDelegate {
|
|
||||||
ValueNotifier<ConnectionsAndKeywords> requestsNotifier;
|
|
||||||
|
|
||||||
RequestsSearchDelegate({
|
|
||||||
required ConnectionsAndKeywords state,
|
|
||||||
}) : requestsNotifier = ValueNotifier<ConnectionsAndKeywords>(state);
|
|
||||||
|
|
||||||
get state => requestsNotifier.value;
|
|
||||||
|
|
||||||
List<Connection> get _results {
|
|
||||||
final lowerQuery = query.toLowerCase().trim();
|
|
||||||
return requestsNotifier.value.filteredConnections.where((request) {
|
|
||||||
final lowerNetwork = request.metadata.network.toLowerCase();
|
|
||||||
final lowerHost = request.metadata.host.toLowerCase();
|
|
||||||
final lowerDestinationIP = request.metadata.destinationIP.toLowerCase();
|
|
||||||
final lowerProcess = request.metadata.process.toLowerCase();
|
|
||||||
final lowerChains = request.chains.join("").toLowerCase();
|
|
||||||
return lowerNetwork.contains(lowerQuery) ||
|
|
||||||
lowerHost.contains(lowerQuery) ||
|
|
||||||
lowerDestinationIP.contains(lowerQuery) ||
|
|
||||||
lowerProcess.contains(lowerQuery) ||
|
|
||||||
lowerChains.contains(lowerQuery);
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
_addKeyword(String keyword) {
|
|
||||||
final isContains = requestsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (isContains) return;
|
|
||||||
final keywords = List<String>.from(requestsNotifier.value.keywords)
|
|
||||||
..add(keyword);
|
|
||||||
requestsNotifier.value = requestsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_deleteKeyword(String keyword) {
|
|
||||||
final isContains = requestsNotifier.value.keywords.contains(keyword);
|
|
||||||
if (!isContains) return;
|
|
||||||
final keywords = List<String>.from(requestsNotifier.value.keywords)
|
|
||||||
..remove(keyword);
|
|
||||||
requestsNotifier.value = requestsNotifier.value.copyWith(
|
|
||||||
keywords: keywords,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget>? buildActions(BuildContext context) {
|
|
||||||
return [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (query.isEmpty) {
|
|
||||||
close(context, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
query = '';
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget? buildLeading(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
close(context, null);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildResults(BuildContext context) {
|
|
||||||
return buildSuggestions(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
requestsNotifier.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildSuggestions(BuildContext context) {
|
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable: requestsNotifier,
|
|
||||||
builder: (_, __, ___) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (state.keywords.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 6,
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
for (final keyword in state.keywords)
|
|
||||||
CommonChip(
|
|
||||||
label: keyword,
|
|
||||||
type: ChipType.delete,
|
|
||||||
onPressed: () {
|
|
||||||
_deleteKeyword(keyword);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.separated(
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final connection = _results[index];
|
|
||||||
return ConnectionItem(
|
|
||||||
key: Key(connection.id),
|
|
||||||
connection: connection,
|
|
||||||
onClick: (value) {
|
|
||||||
_addKeyword(value);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
|
||||||
return const Divider(
|
|
||||||
height: 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: _results.length,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,12 @@ import 'dart:io';
|
|||||||
import 'package:fl_clash/clash/clash.dart';
|
import 'package:fl_clash/clash/clash.dart';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:path/path.dart' hide context;
|
import 'package:path/path.dart' hide context;
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class GeoItem {
|
class GeoItem {
|
||||||
@@ -84,12 +85,13 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
|||||||
|
|
||||||
GeoItem get geoItem => widget.geoItem;
|
GeoItem get geoItem => widget.geoItem;
|
||||||
|
|
||||||
_updateUrl(String url) async {
|
_updateUrl(String url, WidgetRef ref) async {
|
||||||
|
final defaultMap = defaultGeoXUrl.toJson();
|
||||||
final newUrl = await globalState.showCommonDialog<String>(
|
final newUrl = await globalState.showCommonDialog<String>(
|
||||||
child: UpdateGeoUrlFormDialog(
|
child: UpdateGeoUrlFormDialog(
|
||||||
title: geoItem.label,
|
title: geoItem.label,
|
||||||
url: url,
|
url: url,
|
||||||
defaultValue: defaultGeoXMap[geoItem.key],
|
defaultValue: defaultMap[geoItem.key],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (newUrl != null && newUrl != url && mounted) {
|
if (newUrl != null && newUrl != url && mounted) {
|
||||||
@@ -97,9 +99,13 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
|||||||
if (!newUrl.isUrl) {
|
if (!newUrl.isUrl) {
|
||||||
throw "Invalid url";
|
throw "Invalid url";
|
||||||
}
|
}
|
||||||
final appController = globalState.appController;
|
ref.read(patchClashConfigProvider.notifier).updateState((state) {
|
||||||
appController.clashConfig.geoXUrl =
|
final map = state.geoXUrl.toJson();
|
||||||
Map.from(appController.clashConfig.geoXUrl)..[geoItem.key] = newUrl;
|
map[geoItem.key] = newUrl;
|
||||||
|
return state.copyWith(
|
||||||
|
geoXUrl: GeoXUrl.fromJson(map),
|
||||||
|
);
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
globalState.showMessage(
|
globalState.showMessage(
|
||||||
title: geoItem.label,
|
title: geoItem.label,
|
||||||
@@ -112,7 +118,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<FileInfo> _getGeoFileLastModified(String fileName) async {
|
Future<FileInfo> _getGeoFileLastModified(String fileName) async {
|
||||||
final homePath = await appPath.getHomeDirPath();
|
final homePath = await appPath.homeDirPath;
|
||||||
final file = File(join(homePath, fileName));
|
final file = File(join(homePath, fileName));
|
||||||
final lastModified = await file.lastModified();
|
final lastModified = await file.lastModified();
|
||||||
final size = await file.length();
|
final size = await file.length();
|
||||||
@@ -122,68 +128,84 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSubtitle(String url) {
|
Widget _buildSubtitle() {
|
||||||
return Column(
|
return Consumer(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (_, ref, __) {
|
||||||
children: [
|
final url = ref.watch(
|
||||||
const SizedBox(
|
patchClashConfigProvider
|
||||||
height: 4,
|
.select((state) => state.geoXUrl.toJson()[geoItem.key]),
|
||||||
),
|
);
|
||||||
FutureBuilder<FileInfo>(
|
if (url == null) {
|
||||||
future: _getGeoFileLastModified(geoItem.fileName),
|
return SizedBox();
|
||||||
builder: (_, snapshot) {
|
}
|
||||||
return SizedBox(
|
return Column(
|
||||||
height: 24,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: FadeBox(
|
|
||||||
key: Key("fade_box_${geoItem.label}"),
|
|
||||||
child: snapshot.data == null
|
|
||||||
? const SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
snapshot.data!.desc,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
url,
|
|
||||||
style: context.textTheme.bodyMedium?.toLight,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 8,
|
|
||||||
),
|
|
||||||
Wrap(
|
|
||||||
runSpacing: 6,
|
|
||||||
spacing: 12,
|
|
||||||
children: [
|
children: [
|
||||||
CommonChip(
|
const SizedBox(
|
||||||
avatar: const Icon(Icons.edit),
|
height: 4,
|
||||||
label: appLocalizations.edit,
|
),
|
||||||
onPressed: () {
|
FutureBuilder<FileInfo>(
|
||||||
_updateUrl(url);
|
future: _getGeoFileLastModified(geoItem.fileName),
|
||||||
|
builder: (_, snapshot) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 24,
|
||||||
|
child: FadeBox(
|
||||||
|
key: Key("fade_box_${geoItem.label}"),
|
||||||
|
child: snapshot.data == null
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
snapshot.data!.desc,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
CommonChip(
|
Text(
|
||||||
avatar: const Icon(Icons.sync),
|
url,
|
||||||
label: appLocalizations.sync,
|
style: context.textTheme.bodyMedium?.toLight,
|
||||||
onPressed: () {
|
),
|
||||||
_handleUpdateGeoDataItem();
|
const SizedBox(
|
||||||
},
|
height: 8,
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
runSpacing: 6,
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
CommonChip(
|
||||||
|
avatar: const Icon(Icons.edit),
|
||||||
|
label: appLocalizations.edit,
|
||||||
|
onPressed: () {
|
||||||
|
_updateUrl(url, ref);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CommonChip(
|
||||||
|
avatar: const Icon(Icons.sync),
|
||||||
|
label: appLocalizations.sync,
|
||||||
|
onPressed: () {
|
||||||
|
_handleUpdateGeoDataItem();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleUpdateGeoDataItem() async {
|
_handleUpdateGeoDataItem() async {
|
||||||
await globalState.safeRun<void>(updateGeoDateItem);
|
await globalState.safeRun<void>(
|
||||||
|
() async {
|
||||||
|
await updateGeoDateItem();
|
||||||
|
},
|
||||||
|
silence: false,
|
||||||
|
);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,12 +241,7 @@ class _GeoDataListItemState extends State<GeoDataListItem> {
|
|||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
title: Text(geoItem.label),
|
title: Text(geoItem.label),
|
||||||
subtitle: Selector<ClashConfig, String>(
|
subtitle: _buildSubtitle(),
|
||||||
selector: (_, clashConfig) => clashConfig.geoXUrl[geoItem.key]!,
|
|
||||||
builder: (_, value, __) {
|
|
||||||
return _buildSubtitle(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
width: 48,
|
width: 48,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
import 'package:fl_clash/enum/enum.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/providers/config.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../widgets/widgets.dart';
|
|
||||||
|
|
||||||
class ThemeModeItem {
|
class ThemeModeItem {
|
||||||
final ThemeMode themeMode;
|
final ThemeMode themeMode;
|
||||||
@@ -88,99 +86,106 @@ class ItemCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThemeColorsBox extends StatefulWidget {
|
class ThemeColorsBox extends ConsumerStatefulWidget {
|
||||||
const ThemeColorsBox({super.key});
|
const ThemeColorsBox({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ThemeColorsBox> createState() => _ThemeColorsBoxState();
|
ConsumerState<ThemeColorsBox> createState() => _ThemeColorsBoxState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ThemeColorsBoxState extends State<ThemeColorsBox> {
|
class _ThemeColorsBoxState extends ConsumerState<ThemeColorsBox> {
|
||||||
Widget _themeModeCheckBox({
|
|
||||||
bool? isSelected,
|
|
||||||
required ThemeModeItem themeModeItem,
|
|
||||||
}) {
|
|
||||||
return CommonCard(
|
|
||||||
isSelected: isSelected,
|
|
||||||
onPressed: () {
|
|
||||||
final appController = globalState.appController;
|
|
||||||
appController.config.themeProps =
|
|
||||||
appController.config.themeProps.copyWith(
|
|
||||||
themeMode: themeModeItem.themeMode,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Icon(themeModeItem.iconData),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 8,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
themeModeItem.label,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _primaryColorCheckBox({
|
|
||||||
bool? isSelected,
|
|
||||||
Color? color,
|
|
||||||
}) {
|
|
||||||
return ColorSchemeBox(
|
|
||||||
isSelected: isSelected,
|
|
||||||
primaryColor: color,
|
|
||||||
onPressed: () {
|
|
||||||
final appController = globalState.appController;
|
|
||||||
appController.config.themeProps =
|
|
||||||
appController.config.themeProps.copyWith(
|
|
||||||
primaryColor: color?.value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _fontFamilyCheckBox({
|
|
||||||
bool? isSelected,
|
|
||||||
required FontFamilyItem fontFamilyItem,
|
|
||||||
}) {
|
|
||||||
return CommonCard(
|
|
||||||
isSelected: isSelected,
|
|
||||||
onPressed: () {
|
|
||||||
final appController = globalState.appController;
|
|
||||||
appController.config.themeProps =
|
|
||||||
appController.config.themeProps.copyWith(
|
|
||||||
fontFamily: fontFamilyItem.fontFamily,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
fontFamilyItem.label,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_FontFamilyItem(),
|
||||||
|
_ThemeModeItem(),
|
||||||
|
_PrimaryColorItem(),
|
||||||
|
_PrueBlackItem(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 64,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FontFamilyItem extends ConsumerWidget {
|
||||||
|
const _FontFamilyItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final fontFamily =
|
||||||
|
ref.watch(themeSettingProvider.select((state) => state.fontFamily));
|
||||||
|
List<FontFamilyItem> fontFamilyItems = [
|
||||||
|
FontFamilyItem(
|
||||||
|
label: appLocalizations.systemFont,
|
||||||
|
fontFamily: FontFamily.system,
|
||||||
|
),
|
||||||
|
const FontFamilyItem(
|
||||||
|
label: "MiSans",
|
||||||
|
fontFamily: FontFamily.miSans,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
return ItemCard(
|
||||||
|
info: Info(
|
||||||
|
label: appLocalizations.fontFamily,
|
||||||
|
iconData: Icons.text_fields,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
height: 48,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final fontFamilyItem = fontFamilyItems[index];
|
||||||
|
return CommonCard(
|
||||||
|
isSelected: fontFamilyItem.fontFamily == fontFamily,
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(themeSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
fontFamily: fontFamilyItem.fontFamily,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
fontFamilyItem.label,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) {
|
||||||
|
return const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: fontFamilyItems.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ThemeModeItem extends ConsumerWidget {
|
||||||
|
const _ThemeModeItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final themeMode =
|
||||||
|
ref.watch(themeSettingProvider.select((state) => state.themeMode));
|
||||||
List<ThemeModeItem> themeModeItems = [
|
List<ThemeModeItem> themeModeItems = [
|
||||||
ThemeModeItem(
|
ThemeModeItem(
|
||||||
iconData: Icons.auto_mode,
|
iconData: Icons.auto_mode,
|
||||||
@@ -198,6 +203,68 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
|
|||||||
themeMode: ThemeMode.dark,
|
themeMode: ThemeMode.dark,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
return ItemCard(
|
||||||
|
info: Info(
|
||||||
|
label: appLocalizations.themeMode,
|
||||||
|
iconData: Icons.brightness_high,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
height: 64,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: themeModeItems.length,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final themeModeItem = themeModeItems[index];
|
||||||
|
return CommonCard(
|
||||||
|
isSelected: themeModeItem.themeMode == themeMode,
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(themeSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
themeMode: themeModeItem.themeMode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Icon(themeModeItem.iconData),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
themeModeItem.label,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) {
|
||||||
|
return const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PrimaryColorItem extends ConsumerWidget {
|
||||||
|
const _PrimaryColorItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final primaryColor =
|
||||||
|
ref.watch(themeSettingProvider.select((state) => state.primaryColor));
|
||||||
List<Color?> primaryColors = [
|
List<Color?> primaryColors = [
|
||||||
null,
|
null,
|
||||||
defaultPrimaryColor,
|
defaultPrimaryColor,
|
||||||
@@ -207,147 +274,72 @@ class _ThemeColorsBoxState extends State<ThemeColorsBox> {
|
|||||||
Colors.yellowAccent,
|
Colors.yellowAccent,
|
||||||
Colors.purple,
|
Colors.purple,
|
||||||
];
|
];
|
||||||
List<FontFamilyItem> fontFamilyItems = [
|
return ItemCard(
|
||||||
FontFamilyItem(
|
info: Info(
|
||||||
label: appLocalizations.systemFont,
|
label: appLocalizations.themeColor,
|
||||||
fontFamily: FontFamily.system,
|
iconData: Icons.palette,
|
||||||
),
|
),
|
||||||
const FontFamilyItem(
|
child: Container(
|
||||||
label: "MiSans",
|
margin: const EdgeInsets.only(
|
||||||
fontFamily: FontFamily.miSans,
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
bottom: 16,
|
||||||
|
),
|
||||||
|
height: 88,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final color = primaryColors[index];
|
||||||
|
return ColorSchemeBox(
|
||||||
|
isSelected: color?.value == primaryColor,
|
||||||
|
primaryColor: color,
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(themeSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
primaryColor: color?.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) {
|
||||||
|
return const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: primaryColors.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PrueBlackItem extends ConsumerWidget {
|
||||||
|
const _PrueBlackItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final prueBlack =
|
||||||
|
ref.watch(themeSettingProvider.select((state) => state.prueBlack));
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: ListItem.switchItem(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.contrast,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: Text(appLocalizations.prueBlackMode),
|
||||||
|
delegate: SwitchDelegate(
|
||||||
|
value: prueBlack,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref.read(themeSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
prueBlack: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
];
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
ItemCard(
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.fontFamily,
|
|
||||||
iconData: Icons.text_fields,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
),
|
|
||||||
height: 48,
|
|
||||||
child: Selector<Config, FontFamily>(
|
|
||||||
selector: (_, config) => config.themeProps.fontFamily,
|
|
||||||
builder: (_, fontFamily, __) {
|
|
||||||
return ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final fontFamilyItem = fontFamilyItems[index];
|
|
||||||
return _fontFamilyCheckBox(
|
|
||||||
isSelected: fontFamily == fontFamilyItem.fontFamily,
|
|
||||||
fontFamilyItem: fontFamilyItem,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (_, __) {
|
|
||||||
return const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: fontFamilyItems.length,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ItemCard(
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.themeMode,
|
|
||||||
iconData: Icons.brightness_high,
|
|
||||||
),
|
|
||||||
child: Selector<Config, ThemeMode>(
|
|
||||||
selector: (_, config) => config.themeProps.themeMode,
|
|
||||||
builder: (_, themeMode, __) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
height: 64,
|
|
||||||
child: ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: themeModeItems.length,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final themeModeItem = themeModeItems[index];
|
|
||||||
return _themeModeCheckBox(
|
|
||||||
isSelected: themeMode == themeModeItem.themeMode,
|
|
||||||
themeModeItem: themeModeItem,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (_, __) {
|
|
||||||
return const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ItemCard(
|
|
||||||
info: Info(
|
|
||||||
label: appLocalizations.themeColor,
|
|
||||||
iconData: Icons.palette,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
bottom: 16,
|
|
||||||
),
|
|
||||||
height: 88,
|
|
||||||
child: Selector<Config, int?>(
|
|
||||||
selector: (_, config) => config.themeProps.primaryColor,
|
|
||||||
builder: (_, currentPrimaryColor, __) {
|
|
||||||
return ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final primaryColor = primaryColors[index];
|
|
||||||
return _primaryColorCheckBox(
|
|
||||||
isSelected: currentPrimaryColor == primaryColor?.value,
|
|
||||||
color: primaryColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (_, __) {
|
|
||||||
return const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: primaryColors.length,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
child: Selector<Config, bool>(
|
|
||||||
selector: (_, config) => config.themeProps.prueBlack,
|
|
||||||
builder: (_, value, ___) {
|
|
||||||
return ListItem.switchItem(
|
|
||||||
leading: Icon(
|
|
||||||
Icons.contrast,
|
|
||||||
color: context.colorScheme.primary,
|
|
||||||
),
|
|
||||||
title: Text(appLocalizations.prueBlackMode),
|
|
||||||
delegate: SwitchDelegate(
|
|
||||||
value: value,
|
|
||||||
onChanged: (value) {
|
|
||||||
final appController = globalState.appController;
|
|
||||||
appController.config.themeProps =
|
|
||||||
appController.config.themeProps.copyWith(
|
|
||||||
prueBlack: value,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 64,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:fl_clash/common/common.dart';
|
import 'package:fl_clash/common/common.dart';
|
||||||
import 'package:fl_clash/enum/enum.dart';
|
|
||||||
import 'package:fl_clash/fragments/about.dart';
|
import 'package:fl_clash/fragments/about.dart';
|
||||||
import 'package:fl_clash/fragments/access.dart';
|
import 'package:fl_clash/fragments/access.dart';
|
||||||
import 'package:fl_clash/fragments/application_setting.dart';
|
import 'package:fl_clash/fragments/application_setting.dart';
|
||||||
@@ -8,33 +7,35 @@ import 'package:fl_clash/fragments/config/config.dart';
|
|||||||
import 'package:fl_clash/fragments/hotkey.dart';
|
import 'package:fl_clash/fragments/hotkey.dart';
|
||||||
import 'package:fl_clash/l10n/l10n.dart';
|
import 'package:fl_clash/l10n/l10n.dart';
|
||||||
import 'package:fl_clash/models/models.dart';
|
import 'package:fl_clash/models/models.dart';
|
||||||
|
import 'package:fl_clash/providers/providers.dart';
|
||||||
import 'package:fl_clash/state.dart';
|
import 'package:fl_clash/state.dart';
|
||||||
import 'package:fl_clash/widgets/widgets.dart';
|
import 'package:fl_clash/widgets/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'backup_and_recovery.dart';
|
import 'backup_and_recovery.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
import 'package:path/path.dart' show dirname, join;
|
import 'package:path/path.dart' show dirname, join;
|
||||||
|
|
||||||
class ToolsFragment extends StatefulWidget {
|
class ToolsFragment extends ConsumerStatefulWidget {
|
||||||
const ToolsFragment({super.key});
|
const ToolsFragment({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ToolsFragment> createState() => _ToolboxFragmentState();
|
ConsumerState<ToolsFragment> createState() => _ToolboxFragmentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ToolboxFragmentState extends State<ToolsFragment> {
|
class _ToolboxFragmentState extends ConsumerState<ToolsFragment> {
|
||||||
_buildNavigationMenuItem(NavigationItem navigationItem) {
|
_buildNavigationMenuItem(NavigationItem navigationItem) {
|
||||||
return ListItem.open(
|
return ListItem.open(
|
||||||
leading: navigationItem.icon,
|
leading: navigationItem.icon,
|
||||||
title: Text(Intl.message(navigationItem.label)),
|
title: Text(Intl.message(navigationItem.label.name)),
|
||||||
subtitle: navigationItem.description != null
|
subtitle: navigationItem.description != null
|
||||||
? Text(Intl.message(navigationItem.description!))
|
? Text(Intl.message(navigationItem.description!))
|
||||||
: null,
|
: null,
|
||||||
delegate: OpenDelegate(
|
delegate: OpenDelegate(
|
||||||
title: Intl.message(navigationItem.label),
|
title: Intl.message(navigationItem.label.name),
|
||||||
widget: navigationItem.fragment,
|
widget: navigationItem.fragment,
|
||||||
|
extendPageWidth: 360,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -54,34 +55,12 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getLocaleString(Locale? locale) {
|
|
||||||
if (locale == null) return appLocalizations.defaultText;
|
|
||||||
return Intl.message(locale.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _getOtherList() {
|
List<Widget> _getOtherList() {
|
||||||
return generateSection(
|
return generateSection(
|
||||||
title: appLocalizations.other,
|
title: appLocalizations.other,
|
||||||
items: [
|
items: [
|
||||||
ListItem(
|
_DisclaimerItem(),
|
||||||
leading: const Icon(Icons.gavel),
|
_InfoItem(),
|
||||||
title: Text(appLocalizations.disclaimer),
|
|
||||||
onTap: () async {
|
|
||||||
final isDisclaimerAccepted =
|
|
||||||
await globalState.appController.showDisclaimer();
|
|
||||||
if (!isDisclaimerAccepted) {
|
|
||||||
globalState.appController.handleExit();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListItem.open(
|
|
||||||
leading: const Icon(Icons.info),
|
|
||||||
title: Text(appLocalizations.about),
|
|
||||||
delegate: OpenDelegate(
|
|
||||||
title: appLocalizations.about,
|
|
||||||
widget: const AboutFragment(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -90,142 +69,233 @@ class _ToolboxFragmentState extends State<ToolsFragment> {
|
|||||||
return generateSection(
|
return generateSection(
|
||||||
title: appLocalizations.settings,
|
title: appLocalizations.settings,
|
||||||
items: [
|
items: [
|
||||||
Selector<Config, String?>(
|
_LocaleItem(),
|
||||||
selector: (_, config) => config.appSetting.locale,
|
_ThemeItem(),
|
||||||
builder: (_, localeString, __) {
|
_BackupItem(),
|
||||||
final subTitle = localeString ?? appLocalizations.defaultText;
|
if (system.isDesktop) _HotkeyItem(),
|
||||||
final currentLocale = other.getLocaleForString(localeString);
|
if (Platform.isWindows) _LoopbackItem(),
|
||||||
return ListItem<Locale?>.options(
|
if (Platform.isAndroid) _AccessItem(),
|
||||||
leading: const Icon(Icons.language_outlined),
|
_OverrideItem(),
|
||||||
title: Text(appLocalizations.language),
|
_SettingItem(),
|
||||||
subtitle: Text(Intl.message(subTitle)),
|
|
||||||
delegate: OptionsDelegate(
|
|
||||||
title: appLocalizations.language,
|
|
||||||
options: [null, ...AppLocalizations.delegate.supportedLocales],
|
|
||||||
onChanged: (Locale? value) {
|
|
||||||
final config = globalState.appController.config;
|
|
||||||
config.appSetting = config.appSetting.copyWith(
|
|
||||||
locale: value?.toString(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
textBuilder: (locale) => _getLocaleString(locale),
|
|
||||||
value: currentLocale,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListItem.open(
|
|
||||||
leading: const Icon(Icons.style),
|
|
||||||
title: Text(appLocalizations.theme),
|
|
||||||
subtitle: Text(appLocalizations.themeDesc),
|
|
||||||
delegate: OpenDelegate(
|
|
||||||
title: appLocalizations.theme,
|
|
||||||
widget: const ThemeFragment(),
|
|
||||||
extendPageWidth: 360,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListItem.open(
|
|
||||||
leading: const Icon(Icons.cloud_sync),
|
|
||||||
title: Text(appLocalizations.backupAndRecovery),
|
|
||||||
subtitle: Text(appLocalizations.backupAndRecoveryDesc),
|
|
||||||
delegate: OpenDelegate(
|
|
||||||
title: appLocalizations.backupAndRecovery,
|
|
||||||
widget: const BackupAndRecovery(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (system.isDesktop)
|
|
||||||
ListItem.open(
|
|
||||||
leading: const Icon(Icons.keyboard),
|
|
||||||
title: Text(appLocalizations.hotkeyManagement),
|
|
||||||
subtitle: Text(appLocalizations.hotkeyManagementDesc),
|
|
||||||
delegate: OpenDelegate(
|
|
||||||
title: appLocalizations.hotkeyManagement,
|
|
||||||
widget: const HotKeyFragment(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (Platform.isWindows)
|
|
||||||
ListItem(
|
|
||||||
leading: const Icon(Icons.lock),
|
|
||||||
title: Text(appLocalizations.loopback),
|
|
||||||
subtitle: Text(appLocalizations.loopbackDesc),
|
|
||||||
onTap: () {
|
|
||||||
windows?.runas(
|
|
||||||
'"${join(dirname(Platform.resolvedExecutable), "EnableLoopback.exe")}"',
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (Platform.isAndroid)
|
|
||||||
ListItem.open(
|
|
||||||
leading: const Icon(Icons.view_list),
|
|
||||||
title: Text(appLocalizations.accessControl),
|
|
||||||
subtitle: Text(appLocalizations.accessControlDesc),
|
|
||||||
delegate: OpenDelegate(
|
|
||||||
title: appLocalizations.appAccessControl,
|
|
||||||
widget: const AccessFragment(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListItem.open(
|
|
||||||
leading: const Icon(Icons.edit),
|
|
||||||
title: Text(appLocalizations.override),
|
|
||||||
subtitle: Text(appLocalizations.overrideDesc),
|
|
||||||
delegate: OpenDelegate(
|
|
||||||
title: appLocalizations.override,
|
|
||||||
widget: const ConfigFragment(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListItem.open(
|
|
||||||
leading: const Icon(Icons.settings),
|
|
||||||
title: Text(appLocalizations.application),
|
|
||||||
subtitle: Text(appLocalizations.applicationDesc),
|
|
||||||
delegate: OpenDelegate(
|
|
||||||
title: appLocalizations.application,
|
|
||||||
widget: const ApplicationSettingFragment(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LocaleBuilder(
|
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||||
builder: (_) {
|
final items = [
|
||||||
final items = [
|
Consumer(
|
||||||
Selector<AppState, MoreToolsSelectorState>(
|
builder: (_, ref, __) {
|
||||||
selector: (_, appState) {
|
final state = ref.watch(moreToolsSelectorStateProvider);
|
||||||
return MoreToolsSelectorState(
|
if (state.navigationItems.isEmpty) {
|
||||||
navigationItems: appState.viewMode == ViewMode.mobile
|
return Container();
|
||||||
? appState.navigationItems.where(
|
}
|
||||||
(element) {
|
return Column(
|
||||||
return element.modes
|
children: [
|
||||||
.contains(NavigationItemMode.more);
|
ListHeader(title: appLocalizations.more),
|
||||||
},
|
_buildNavigationMenu(state.navigationItems)
|
||||||
).toList()
|
],
|
||||||
: [],
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
..._getSettingList(),
|
||||||
|
..._getOtherList(),
|
||||||
|
];
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: items.length,
|
||||||
|
itemBuilder: (_, index) => items[index],
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LocaleItem extends ConsumerWidget {
|
||||||
|
const _LocaleItem();
|
||||||
|
|
||||||
|
String _getLocaleString(Locale? locale) {
|
||||||
|
if (locale == null) return appLocalizations.defaultText;
|
||||||
|
return Intl.message(locale.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final locale =
|
||||||
|
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||||
|
final subTitle = locale ?? appLocalizations.defaultText;
|
||||||
|
final currentLocale = other.getLocaleForString(locale);
|
||||||
|
return ListItem<Locale?>.options(
|
||||||
|
leading: const Icon(Icons.language_outlined),
|
||||||
|
title: Text(appLocalizations.language),
|
||||||
|
subtitle: Text(Intl.message(subTitle)),
|
||||||
|
delegate: OptionsDelegate(
|
||||||
|
title: appLocalizations.language,
|
||||||
|
options: [null, ...AppLocalizations.delegate.supportedLocales],
|
||||||
|
onChanged: (Locale? locale) {
|
||||||
|
ref.read(appSettingProvider.notifier).updateState(
|
||||||
|
(state) => state.copyWith(locale: locale?.toString()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
builder: (_, state, __) {
|
textBuilder: (locale) => _getLocaleString(locale),
|
||||||
if (state.navigationItems.isEmpty) {
|
value: currentLocale,
|
||||||
return Container();
|
),
|
||||||
}
|
);
|
||||||
return Column(
|
}
|
||||||
children: [
|
}
|
||||||
ListHeader(title: appLocalizations.more),
|
|
||||||
_buildNavigationMenu(state.navigationItems)
|
class _ThemeItem extends StatelessWidget {
|
||||||
],
|
const _ThemeItem();
|
||||||
);
|
|
||||||
},
|
@override
|
||||||
),
|
Widget build(BuildContext context) {
|
||||||
..._getSettingList(),
|
return ListItem.open(
|
||||||
..._getOtherList(),
|
leading: const Icon(Icons.style),
|
||||||
];
|
title: Text(appLocalizations.theme),
|
||||||
return ListView.builder(
|
subtitle: Text(appLocalizations.themeDesc),
|
||||||
itemCount: items.length,
|
delegate: OpenDelegate(
|
||||||
itemBuilder: (_, index) => items[index],
|
title: appLocalizations.theme,
|
||||||
padding: const EdgeInsets.only(bottom: 20),
|
widget: const ThemeFragment(),
|
||||||
|
extendPageWidth: 360,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackupItem extends StatelessWidget {
|
||||||
|
const _BackupItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListItem.open(
|
||||||
|
leading: const Icon(Icons.cloud_sync),
|
||||||
|
title: Text(appLocalizations.backupAndRecovery),
|
||||||
|
subtitle: Text(appLocalizations.backupAndRecoveryDesc),
|
||||||
|
delegate: OpenDelegate(
|
||||||
|
title: appLocalizations.backupAndRecovery,
|
||||||
|
widget: const BackupAndRecovery(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HotkeyItem extends StatelessWidget {
|
||||||
|
const _HotkeyItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListItem.open(
|
||||||
|
leading: const Icon(Icons.keyboard),
|
||||||
|
title: Text(appLocalizations.hotkeyManagement),
|
||||||
|
subtitle: Text(appLocalizations.hotkeyManagementDesc),
|
||||||
|
delegate: OpenDelegate(
|
||||||
|
title: appLocalizations.hotkeyManagement,
|
||||||
|
widget: const HotKeyFragment(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoopbackItem extends StatelessWidget {
|
||||||
|
const _LoopbackItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListItem(
|
||||||
|
leading: const Icon(Icons.lock),
|
||||||
|
title: Text(appLocalizations.loopback),
|
||||||
|
subtitle: Text(appLocalizations.loopbackDesc),
|
||||||
|
onTap: () {
|
||||||
|
windows?.runas(
|
||||||
|
'"${join(dirname(Platform.resolvedExecutable), "EnableLoopback.exe")}"',
|
||||||
|
"",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AccessItem extends StatelessWidget {
|
||||||
|
const _AccessItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListItem.open(
|
||||||
|
leading: const Icon(Icons.view_list),
|
||||||
|
title: Text(appLocalizations.accessControl),
|
||||||
|
subtitle: Text(appLocalizations.accessControlDesc),
|
||||||
|
delegate: OpenDelegate(
|
||||||
|
title: appLocalizations.appAccessControl,
|
||||||
|
widget: const AccessFragment(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverrideItem extends StatelessWidget {
|
||||||
|
const _OverrideItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListItem.open(
|
||||||
|
leading: const Icon(Icons.edit),
|
||||||
|
title: Text(appLocalizations.override),
|
||||||
|
subtitle: Text(appLocalizations.overrideDesc),
|
||||||
|
delegate: OpenDelegate(
|
||||||
|
title: appLocalizations.override,
|
||||||
|
widget: const ConfigFragment(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingItem extends StatelessWidget {
|
||||||
|
const _SettingItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListItem.open(
|
||||||
|
leading: const Icon(Icons.settings),
|
||||||
|
title: Text(appLocalizations.application),
|
||||||
|
subtitle: Text(appLocalizations.applicationDesc),
|
||||||
|
delegate: OpenDelegate(
|
||||||
|
title: appLocalizations.application,
|
||||||
|
widget: const ApplicationSettingFragment(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DisclaimerItem extends StatelessWidget {
|
||||||
|
const _DisclaimerItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListItem(
|
||||||
|
leading: const Icon(Icons.gavel),
|
||||||
|
title: Text(appLocalizations.disclaimer),
|
||||||
|
onTap: () async {
|
||||||
|
final isDisclaimerAccepted =
|
||||||
|
await globalState.appController.showDisclaimer();
|
||||||
|
if (!isDisclaimerAccepted) {
|
||||||
|
globalState.appController.handleExit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InfoItem extends StatelessWidget {
|
||||||
|
const _InfoItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListItem.open(
|
||||||
|
leading: const Icon(Icons.info),
|
||||||
|
title: Text(appLocalizations.about),
|
||||||
|
delegate: OpenDelegate(
|
||||||
|
title: appLocalizations.about,
|
||||||
|
widget: const AboutFragment(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -341,5 +341,7 @@
|
|||||||
"nullProxies": "No proxies",
|
"nullProxies": "No proxies",
|
||||||
"copySuccess": "Copy success",
|
"copySuccess": "Copy success",
|
||||||
"copyLink": "Copy link",
|
"copyLink": "Copy link",
|
||||||
"exportFile": "Export file"
|
"exportFile": "Export file",
|
||||||
|
"cacheCorrupt": "The cache is corrupt. Do you want to clear it?",
|
||||||
|
"detectionTip": "Relying on third-party api is for reference only"
|
||||||
}
|
}
|
||||||
@@ -341,5 +341,7 @@
|
|||||||
"nullProxies": "暂无代理",
|
"nullProxies": "暂无代理",
|
||||||
"copySuccess": "复制成功",
|
"copySuccess": "复制成功",
|
||||||
"copyLink": "复制链接",
|
"copyLink": "复制链接",
|
||||||
"exportFile": "导出文件"
|
"exportFile": "导出文件",
|
||||||
|
"cacheCorrupt": "缓存已损坏,是否清空?",
|
||||||
|
"detectionTip": "依赖第三方api仅供参考"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ MessageLookupByLibrary? _findExact(String localeName) {
|
|||||||
/// User programs should call this before using [localeName] for messages.
|
/// User programs should call this before using [localeName] for messages.
|
||||||
Future<bool> initializeMessages(String localeName) {
|
Future<bool> initializeMessages(String localeName) {
|
||||||
var availableLocale = Intl.verifiedLocale(
|
var availableLocale = Intl.verifiedLocale(
|
||||||
localeName, (locale) => _deferredLibraries[locale] != null,
|
localeName,
|
||||||
onFailure: (_) => null);
|
(locale) => _deferredLibraries[locale] != null,
|
||||||
|
onFailure: (_) => null,
|
||||||
|
);
|
||||||
if (availableLocale == null) {
|
if (availableLocale == null) {
|
||||||
return new SynchronousFuture(false);
|
return new SynchronousFuture(false);
|
||||||
}
|
}
|
||||||
@@ -60,8 +62,11 @@ bool _messagesExistFor(String locale) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
|
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
|
||||||
var actualLocale =
|
var actualLocale = Intl.verifiedLocale(
|
||||||
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
|
locale,
|
||||||
|
_messagesExistFor,
|
||||||
|
onFailure: (_) => null,
|
||||||
|
);
|
||||||
if (actualLocale == null) return null;
|
if (actualLocale == null) return null;
|
||||||
return _findExact(actualLocale);
|
return _findExact(actualLocale);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,390 +22,392 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
|
|
||||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||||
"about": MessageLookupByLibrary.simpleMessage("关于"),
|
"about": MessageLookupByLibrary.simpleMessage("关于"),
|
||||||
"accessControl": MessageLookupByLibrary.simpleMessage("访问控制"),
|
"accessControl": MessageLookupByLibrary.simpleMessage("访问控制"),
|
||||||
"accessControlAllowDesc":
|
"accessControlAllowDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
MessageLookupByLibrary.simpleMessage("只允许选中应用进入VPN"),
|
"只允许选中应用进入VPN",
|
||||||
"accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"),
|
),
|
||||||
"accessControlNotAllowDesc":
|
"accessControlDesc": MessageLookupByLibrary.simpleMessage("配置应用访问代理"),
|
||||||
MessageLookupByLibrary.simpleMessage("选中应用将会被排除在VPN之外"),
|
"accessControlNotAllowDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"account": MessageLookupByLibrary.simpleMessage("账号"),
|
"选中应用将会被排除在VPN之外",
|
||||||
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
|
),
|
||||||
"action": MessageLookupByLibrary.simpleMessage("操作"),
|
"account": MessageLookupByLibrary.simpleMessage("账号"),
|
||||||
"action_mode": MessageLookupByLibrary.simpleMessage("切换模式"),
|
"accountTip": MessageLookupByLibrary.simpleMessage("账号不能为空"),
|
||||||
"action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"),
|
"action": MessageLookupByLibrary.simpleMessage("操作"),
|
||||||
"action_start": MessageLookupByLibrary.simpleMessage("启动/停止"),
|
"action_mode": MessageLookupByLibrary.simpleMessage("切换模式"),
|
||||||
"action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
|
"action_proxy": MessageLookupByLibrary.simpleMessage("系统代理"),
|
||||||
"action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"),
|
"action_start": MessageLookupByLibrary.simpleMessage("启动/停止"),
|
||||||
"add": MessageLookupByLibrary.simpleMessage("添加"),
|
"action_tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
|
||||||
"address": MessageLookupByLibrary.simpleMessage("地址"),
|
"action_view": MessageLookupByLibrary.simpleMessage("显示/隐藏"),
|
||||||
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
|
"add": MessageLookupByLibrary.simpleMessage("添加"),
|
||||||
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
|
"address": MessageLookupByLibrary.simpleMessage("地址"),
|
||||||
"adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理员自启动"),
|
"addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"),
|
||||||
"adminAutoLaunchDesc":
|
"addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"),
|
||||||
MessageLookupByLibrary.simpleMessage("使用管理员模式开机自启动"),
|
"adminAutoLaunch": MessageLookupByLibrary.simpleMessage("管理员自启动"),
|
||||||
"ago": MessageLookupByLibrary.simpleMessage("前"),
|
"adminAutoLaunchDesc": MessageLookupByLibrary.simpleMessage("使用管理员模式开机自启动"),
|
||||||
"agree": MessageLookupByLibrary.simpleMessage("同意"),
|
"ago": MessageLookupByLibrary.simpleMessage("前"),
|
||||||
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
|
"agree": MessageLookupByLibrary.simpleMessage("同意"),
|
||||||
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
|
"allApps": MessageLookupByLibrary.simpleMessage("所有应用"),
|
||||||
"allowBypassDesc":
|
"allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"),
|
||||||
MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
|
"allowBypassDesc": MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"),
|
||||||
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
|
"allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"),
|
||||||
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
|
"allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"),
|
||||||
"app": MessageLookupByLibrary.simpleMessage("应用"),
|
"app": MessageLookupByLibrary.simpleMessage("应用"),
|
||||||
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
|
"appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"),
|
||||||
"appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"),
|
"appDesc": MessageLookupByLibrary.simpleMessage("处理应用相关设置"),
|
||||||
"application": MessageLookupByLibrary.simpleMessage("应用程序"),
|
"application": MessageLookupByLibrary.simpleMessage("应用程序"),
|
||||||
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
|
"applicationDesc": MessageLookupByLibrary.simpleMessage("修改应用程序相关设置"),
|
||||||
"auto": MessageLookupByLibrary.simpleMessage("自动"),
|
"auto": MessageLookupByLibrary.simpleMessage("自动"),
|
||||||
"autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"),
|
"autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"),
|
||||||
"autoCheckUpdateDesc":
|
"autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"),
|
||||||
MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"),
|
"autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"),
|
||||||
"autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"),
|
"autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"autoCloseConnectionsDesc":
|
"切换节点后自动关闭连接",
|
||||||
MessageLookupByLibrary.simpleMessage("切换节点后自动关闭连接"),
|
),
|
||||||
"autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"),
|
"autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"),
|
||||||
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"),
|
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"),
|
||||||
"autoRun": MessageLookupByLibrary.simpleMessage("自动运行"),
|
"autoRun": MessageLookupByLibrary.simpleMessage("自动运行"),
|
||||||
"autoRunDesc": MessageLookupByLibrary.simpleMessage("应用打开时自动运行"),
|
"autoRunDesc": MessageLookupByLibrary.simpleMessage("应用打开时自动运行"),
|
||||||
"autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"),
|
"autoUpdate": MessageLookupByLibrary.simpleMessage("自动更新"),
|
||||||
"autoUpdateInterval":
|
"autoUpdateInterval": MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"),
|
||||||
MessageLookupByLibrary.simpleMessage("自动更新间隔(分钟)"),
|
"backup": MessageLookupByLibrary.simpleMessage("备份"),
|
||||||
"backup": MessageLookupByLibrary.simpleMessage("备份"),
|
"backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"),
|
||||||
"backupAndRecovery": MessageLookupByLibrary.simpleMessage("备份与恢复"),
|
"backupAndRecoveryDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"backupAndRecoveryDesc":
|
"通过WebDAV或者文件同步数据",
|
||||||
MessageLookupByLibrary.simpleMessage("通过WebDAV或者文件同步数据"),
|
),
|
||||||
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
|
"backupSuccess": MessageLookupByLibrary.simpleMessage("备份成功"),
|
||||||
"bind": MessageLookupByLibrary.simpleMessage("绑定"),
|
"bind": MessageLookupByLibrary.simpleMessage("绑定"),
|
||||||
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
|
"blacklistMode": MessageLookupByLibrary.simpleMessage("黑名单模式"),
|
||||||
"bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"),
|
"bypassDomain": MessageLookupByLibrary.simpleMessage("排除域名"),
|
||||||
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"),
|
"bypassDomainDesc": MessageLookupByLibrary.simpleMessage("仅在系统代理启用时生效"),
|
||||||
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
"cacheCorrupt": MessageLookupByLibrary.simpleMessage("缓存已损坏,是否清空?"),
|
||||||
"cancelFilterSystemApp":
|
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
||||||
MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
|
"cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage("取消过滤系统应用"),
|
||||||
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
|
"cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"),
|
||||||
"checkError": MessageLookupByLibrary.simpleMessage("检测失败"),
|
"checkError": MessageLookupByLibrary.simpleMessage("检测失败"),
|
||||||
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
|
"checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"),
|
||||||
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
|
"checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"),
|
||||||
"checking": MessageLookupByLibrary.simpleMessage("检测中..."),
|
"checking": MessageLookupByLibrary.simpleMessage("检测中..."),
|
||||||
"clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"),
|
"clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"),
|
||||||
"clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"),
|
"clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"),
|
||||||
"columns": MessageLookupByLibrary.simpleMessage("列数"),
|
"columns": MessageLookupByLibrary.simpleMessage("列数"),
|
||||||
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
|
"compatible": MessageLookupByLibrary.simpleMessage("兼容模式"),
|
||||||
"compatibleDesc":
|
"compatibleDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力,获得全量的Clash的支持"),
|
"开启将失去部分应用能力,获得全量的Clash的支持",
|
||||||
"confirm": MessageLookupByLibrary.simpleMessage("确定"),
|
),
|
||||||
"connections": MessageLookupByLibrary.simpleMessage("连接"),
|
"confirm": MessageLookupByLibrary.simpleMessage("确定"),
|
||||||
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
|
"connections": MessageLookupByLibrary.simpleMessage("连接"),
|
||||||
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
|
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
|
||||||
"copy": MessageLookupByLibrary.simpleMessage("复制"),
|
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
|
||||||
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
|
"copy": MessageLookupByLibrary.simpleMessage("复制"),
|
||||||
"copyLink": MessageLookupByLibrary.simpleMessage("复制链接"),
|
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
|
||||||
"copySuccess": MessageLookupByLibrary.simpleMessage("复制成功"),
|
"copyLink": MessageLookupByLibrary.simpleMessage("复制链接"),
|
||||||
"core": MessageLookupByLibrary.simpleMessage("内核"),
|
"copySuccess": MessageLookupByLibrary.simpleMessage("复制成功"),
|
||||||
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
|
"core": MessageLookupByLibrary.simpleMessage("内核"),
|
||||||
"country": MessageLookupByLibrary.simpleMessage("区域"),
|
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
|
||||||
"create": MessageLookupByLibrary.simpleMessage("创建"),
|
"country": MessageLookupByLibrary.simpleMessage("区域"),
|
||||||
"cut": MessageLookupByLibrary.simpleMessage("剪切"),
|
"create": MessageLookupByLibrary.simpleMessage("创建"),
|
||||||
"dark": MessageLookupByLibrary.simpleMessage("深色"),
|
"cut": MessageLookupByLibrary.simpleMessage("剪切"),
|
||||||
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
|
"dark": MessageLookupByLibrary.simpleMessage("深色"),
|
||||||
"days": MessageLookupByLibrary.simpleMessage("天"),
|
"dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"),
|
||||||
"defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"),
|
"days": MessageLookupByLibrary.simpleMessage("天"),
|
||||||
"defaultNameserverDesc":
|
"defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"),
|
||||||
MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"),
|
"defaultNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"),
|
||||||
"defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"),
|
"defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"),
|
||||||
"defaultText": MessageLookupByLibrary.simpleMessage("默认"),
|
"defaultText": MessageLookupByLibrary.simpleMessage("默认"),
|
||||||
"delay": MessageLookupByLibrary.simpleMessage("延迟"),
|
"delay": MessageLookupByLibrary.simpleMessage("延迟"),
|
||||||
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
|
"delaySort": MessageLookupByLibrary.simpleMessage("按延迟排序"),
|
||||||
"delete": MessageLookupByLibrary.simpleMessage("删除"),
|
"delete": MessageLookupByLibrary.simpleMessage("删除"),
|
||||||
"deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"),
|
"deleteProfileTip": MessageLookupByLibrary.simpleMessage("确定要删除当前配置吗?"),
|
||||||
"desc": MessageLookupByLibrary.simpleMessage(
|
"desc": MessageLookupByLibrary.simpleMessage(
|
||||||
"基于ClashMeta的多平台代理客户端,简单易用,开源无广告。"),
|
"基于ClashMeta的多平台代理客户端,简单易用,开源无广告。",
|
||||||
"direct": MessageLookupByLibrary.simpleMessage("直连"),
|
),
|
||||||
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
|
"detectionTip": MessageLookupByLibrary.simpleMessage("依赖第三方api仅供参考"),
|
||||||
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
|
"direct": MessageLookupByLibrary.simpleMessage("直连"),
|
||||||
"本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。"),
|
"disclaimer": MessageLookupByLibrary.simpleMessage("免责声明"),
|
||||||
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
"disclaimerDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
"本软件仅供学习交流、科研等非商业性质的用途,严禁将本软件用于商业目的。如有任何商业行为,均与本软件无关。",
|
||||||
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
|
),
|
||||||
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"),
|
"discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
||||||
"doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"),
|
"discovery": MessageLookupByLibrary.simpleMessage("发现新版本"),
|
||||||
"domain": MessageLookupByLibrary.simpleMessage("域名"),
|
"dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"),
|
||||||
"download": MessageLookupByLibrary.simpleMessage("下载"),
|
"dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"),
|
||||||
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
|
"doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"),
|
||||||
"en": MessageLookupByLibrary.simpleMessage("英语"),
|
"domain": MessageLookupByLibrary.simpleMessage("域名"),
|
||||||
"entries": MessageLookupByLibrary.simpleMessage("个条目"),
|
"download": MessageLookupByLibrary.simpleMessage("下载"),
|
||||||
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
|
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
|
||||||
"excludeDesc":
|
"en": MessageLookupByLibrary.simpleMessage("英语"),
|
||||||
MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
|
"entries": MessageLookupByLibrary.simpleMessage("个条目"),
|
||||||
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
"exclude": MessageLookupByLibrary.simpleMessage("从最近任务中隐藏"),
|
||||||
"expand": MessageLookupByLibrary.simpleMessage("标准"),
|
"excludeDesc": MessageLookupByLibrary.simpleMessage("应用在后台时,从最近任务中隐藏应用"),
|
||||||
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
|
"exit": MessageLookupByLibrary.simpleMessage("退出"),
|
||||||
"exportFile": MessageLookupByLibrary.simpleMessage("导出文件"),
|
"expand": MessageLookupByLibrary.simpleMessage("标准"),
|
||||||
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
|
"expirationTime": MessageLookupByLibrary.simpleMessage("到期时间"),
|
||||||
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
|
"exportFile": MessageLookupByLibrary.simpleMessage("导出文件"),
|
||||||
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
|
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
|
||||||
"externalControllerDesc":
|
"exportSuccess": MessageLookupByLibrary.simpleMessage("导出成功"),
|
||||||
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
|
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
|
||||||
"externalLink": MessageLookupByLibrary.simpleMessage("外部链接"),
|
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
|
"开启后将可以通过9090端口控制Clash内核",
|
||||||
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"),
|
),
|
||||||
"fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"),
|
"externalLink": MessageLookupByLibrary.simpleMessage("外部链接"),
|
||||||
"fallback": MessageLookupByLibrary.simpleMessage("Fallback"),
|
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
|
||||||
"fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"),
|
"fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"),
|
||||||
"fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"),
|
"fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"),
|
||||||
"file": MessageLookupByLibrary.simpleMessage("文件"),
|
"fallback": MessageLookupByLibrary.simpleMessage("Fallback"),
|
||||||
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
|
"fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"),
|
||||||
"fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"),
|
"fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"),
|
||||||
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
|
"file": MessageLookupByLibrary.simpleMessage("文件"),
|
||||||
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
|
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
|
||||||
"findProcessModeDesc":
|
"fileIsUpdate": MessageLookupByLibrary.simpleMessage("文件有修改,是否保存修改"),
|
||||||
MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
|
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
|
||||||
"fontFamily": MessageLookupByLibrary.simpleMessage("字体"),
|
"findProcessMode": MessageLookupByLibrary.simpleMessage("查找进程"),
|
||||||
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
|
"findProcessModeDesc": MessageLookupByLibrary.simpleMessage("开启后存在闪退风险"),
|
||||||
"general": MessageLookupByLibrary.simpleMessage("基础"),
|
"fontFamily": MessageLookupByLibrary.simpleMessage("字体"),
|
||||||
"generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"),
|
"fourColumns": MessageLookupByLibrary.simpleMessage("四列"),
|
||||||
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
|
"general": MessageLookupByLibrary.simpleMessage("基础"),
|
||||||
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
|
"generalDesc": MessageLookupByLibrary.simpleMessage("覆写基础设置"),
|
||||||
"geodataLoaderDesc":
|
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
|
||||||
MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
|
"geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"),
|
||||||
"geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"),
|
"geodataLoaderDesc": MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
|
||||||
"global": MessageLookupByLibrary.simpleMessage("全局"),
|
"geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"),
|
||||||
"go": MessageLookupByLibrary.simpleMessage("前往"),
|
"global": MessageLookupByLibrary.simpleMessage("全局"),
|
||||||
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
|
"go": MessageLookupByLibrary.simpleMessage("前往"),
|
||||||
"hasCacheChange": MessageLookupByLibrary.simpleMessage("是否缓存修改"),
|
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
|
||||||
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
|
"hasCacheChange": MessageLookupByLibrary.simpleMessage("是否缓存修改"),
|
||||||
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"),
|
"hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"),
|
||||||
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"),
|
"hotkeyConflict": MessageLookupByLibrary.simpleMessage("快捷键冲突"),
|
||||||
"hotkeyManagementDesc":
|
"hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"),
|
||||||
MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"),
|
"hotkeyManagementDesc": MessageLookupByLibrary.simpleMessage("使用键盘控制应用程序"),
|
||||||
"hours": MessageLookupByLibrary.simpleMessage("小时"),
|
"hours": MessageLookupByLibrary.simpleMessage("小时"),
|
||||||
"icon": MessageLookupByLibrary.simpleMessage("图片"),
|
"icon": MessageLookupByLibrary.simpleMessage("图片"),
|
||||||
"iconConfiguration": MessageLookupByLibrary.simpleMessage("图片配置"),
|
"iconConfiguration": MessageLookupByLibrary.simpleMessage("图片配置"),
|
||||||
"iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"),
|
"iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"),
|
||||||
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
|
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
|
||||||
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
|
"infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"),
|
||||||
"init": MessageLookupByLibrary.simpleMessage("初始化"),
|
"init": MessageLookupByLibrary.simpleMessage("初始化"),
|
||||||
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"),
|
"inputCorrectHotkey": MessageLookupByLibrary.simpleMessage("请输入正确的快捷键"),
|
||||||
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
|
"intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"),
|
||||||
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
|
"intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"),
|
||||||
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
|
"ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"),
|
||||||
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
|
"ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"),
|
||||||
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
|
"ipv6InboundDesc": MessageLookupByLibrary.simpleMessage("允许IPv6入站"),
|
||||||
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
|
"just": MessageLookupByLibrary.simpleMessage("刚刚"),
|
||||||
"keepAliveIntervalDesc":
|
"keepAliveIntervalDesc": MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
|
||||||
MessageLookupByLibrary.simpleMessage("TCP保持活动间隔"),
|
"key": MessageLookupByLibrary.simpleMessage("键"),
|
||||||
"key": MessageLookupByLibrary.simpleMessage("键"),
|
"language": MessageLookupByLibrary.simpleMessage("语言"),
|
||||||
"language": MessageLookupByLibrary.simpleMessage("语言"),
|
"layout": MessageLookupByLibrary.simpleMessage("布局"),
|
||||||
"layout": MessageLookupByLibrary.simpleMessage("布局"),
|
"light": MessageLookupByLibrary.simpleMessage("浅色"),
|
||||||
"light": MessageLookupByLibrary.simpleMessage("浅色"),
|
"list": MessageLookupByLibrary.simpleMessage("列表"),
|
||||||
"list": MessageLookupByLibrary.simpleMessage("列表"),
|
"local": MessageLookupByLibrary.simpleMessage("本地"),
|
||||||
"local": MessageLookupByLibrary.simpleMessage("本地"),
|
"localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"),
|
||||||
"localBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到本地"),
|
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"),
|
||||||
"localRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过文件恢复数据"),
|
"logLevel": MessageLookupByLibrary.simpleMessage("日志等级"),
|
||||||
"logLevel": MessageLookupByLibrary.simpleMessage("日志等级"),
|
"logcat": MessageLookupByLibrary.simpleMessage("日志捕获"),
|
||||||
"logcat": MessageLookupByLibrary.simpleMessage("日志捕获"),
|
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
|
||||||
"logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"),
|
"logs": MessageLookupByLibrary.simpleMessage("日志"),
|
||||||
"logs": MessageLookupByLibrary.simpleMessage("日志"),
|
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
|
||||||
"logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"),
|
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
|
||||||
"loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"),
|
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
|
||||||
"loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"),
|
"loose": MessageLookupByLibrary.simpleMessage("宽松"),
|
||||||
"loose": MessageLookupByLibrary.simpleMessage("宽松"),
|
"memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"),
|
||||||
"memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"),
|
"min": MessageLookupByLibrary.simpleMessage("最小"),
|
||||||
"min": MessageLookupByLibrary.simpleMessage("最小"),
|
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
|
||||||
"minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"),
|
"minimizeOnExitDesc": MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
|
||||||
"minimizeOnExitDesc":
|
"minutes": MessageLookupByLibrary.simpleMessage("分钟"),
|
||||||
MessageLookupByLibrary.simpleMessage("修改系统默认退出事件"),
|
"mode": MessageLookupByLibrary.simpleMessage("模式"),
|
||||||
"minutes": MessageLookupByLibrary.simpleMessage("分钟"),
|
"months": MessageLookupByLibrary.simpleMessage("月"),
|
||||||
"mode": MessageLookupByLibrary.simpleMessage("模式"),
|
"more": MessageLookupByLibrary.simpleMessage("更多"),
|
||||||
"months": MessageLookupByLibrary.simpleMessage("月"),
|
"name": MessageLookupByLibrary.simpleMessage("名称"),
|
||||||
"more": MessageLookupByLibrary.simpleMessage("更多"),
|
"nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"),
|
||||||
"name": MessageLookupByLibrary.simpleMessage("名称"),
|
"nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"),
|
||||||
"nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"),
|
"nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域名"),
|
||||||
"nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"),
|
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"),
|
||||||
"nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域名"),
|
"nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"),
|
||||||
"nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"),
|
"network": MessageLookupByLibrary.simpleMessage("网络"),
|
||||||
"nameserverPolicyDesc":
|
"networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"),
|
||||||
MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"),
|
"networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"),
|
||||||
"network": MessageLookupByLibrary.simpleMessage("网络"),
|
"networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"),
|
||||||
"networkDesc": MessageLookupByLibrary.simpleMessage("修改网络相关设置"),
|
"noData": MessageLookupByLibrary.simpleMessage("暂无数据"),
|
||||||
"networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"),
|
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
|
||||||
"networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"),
|
"noIcon": MessageLookupByLibrary.simpleMessage("无图标"),
|
||||||
"noData": MessageLookupByLibrary.simpleMessage("暂无数据"),
|
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
|
||||||
"noHotKey": MessageLookupByLibrary.simpleMessage("暂无快捷键"),
|
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
|
||||||
"noIcon": MessageLookupByLibrary.simpleMessage("无图标"),
|
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"),
|
||||||
"noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"),
|
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
||||||
"noMoreInfoDesc": MessageLookupByLibrary.simpleMessage("暂无更多信息"),
|
"noProxyDesc": MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
|
||||||
"noNetwork": MessageLookupByLibrary.simpleMessage("无网络"),
|
"notEmpty": MessageLookupByLibrary.simpleMessage("不能为空"),
|
||||||
"noProxy": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
"notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"),
|
||||||
"noProxyDesc":
|
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"),
|
||||||
MessageLookupByLibrary.simpleMessage("请创建配置文件或者添加有效配置文件"),
|
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"),
|
||||||
"notEmpty": MessageLookupByLibrary.simpleMessage("不能为空"),
|
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
|
||||||
"notSelectedTip": MessageLookupByLibrary.simpleMessage("当前代理组无法选中"),
|
"nullProfileDesc": MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
|
||||||
"nullConnectionsDesc": MessageLookupByLibrary.simpleMessage("暂无连接"),
|
"nullProxies": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
||||||
"nullCoreInfoDesc": MessageLookupByLibrary.simpleMessage("无法获取内核信息"),
|
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
|
||||||
"nullLogsDesc": MessageLookupByLibrary.simpleMessage("暂无日志"),
|
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
|
||||||
"nullProfileDesc":
|
"onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"),
|
||||||
MessageLookupByLibrary.simpleMessage("没有配置文件,请先添加配置文件"),
|
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
|
||||||
"nullProxies": MessageLookupByLibrary.simpleMessage("暂无代理"),
|
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
|
||||||
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
|
"onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
|
"开启后,将只统计代理流量",
|
||||||
"onlyIcon": MessageLookupByLibrary.simpleMessage("仅图标"),
|
),
|
||||||
"onlyOtherApps": MessageLookupByLibrary.simpleMessage("仅第三方应用"),
|
"options": MessageLookupByLibrary.simpleMessage("选项"),
|
||||||
"onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"),
|
"other": MessageLookupByLibrary.simpleMessage("其他"),
|
||||||
"onlyStatisticsProxyDesc":
|
"otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"),
|
||||||
MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"),
|
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
|
||||||
"options": MessageLookupByLibrary.simpleMessage("选项"),
|
"override": MessageLookupByLibrary.simpleMessage("覆写"),
|
||||||
"other": MessageLookupByLibrary.simpleMessage("其他"),
|
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
|
||||||
"otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"),
|
"overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"),
|
||||||
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
|
"overrideDnsDesc": MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"),
|
||||||
"override": MessageLookupByLibrary.simpleMessage("覆写"),
|
"password": MessageLookupByLibrary.simpleMessage("密码"),
|
||||||
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
|
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
|
||||||
"overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"),
|
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
|
||||||
"overrideDnsDesc":
|
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
|
||||||
MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"),
|
"pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage(
|
||||||
"password": MessageLookupByLibrary.simpleMessage("密码"),
|
"请输入管理员密码",
|
||||||
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
|
),
|
||||||
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
|
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
|
||||||
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
|
"pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(
|
||||||
"pleaseInputAdminPassword":
|
"请上传有效的二维码",
|
||||||
MessageLookupByLibrary.simpleMessage("请输入管理员密码"),
|
),
|
||||||
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
|
"port": MessageLookupByLibrary.simpleMessage("端口"),
|
||||||
"pleaseUploadValidQrcode":
|
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
|
||||||
MessageLookupByLibrary.simpleMessage("请上传有效的二维码"),
|
"pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"),
|
||||||
"port": MessageLookupByLibrary.simpleMessage("端口"),
|
"preview": MessageLookupByLibrary.simpleMessage("预览"),
|
||||||
"preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"),
|
"profile": MessageLookupByLibrary.simpleMessage("配置"),
|
||||||
"pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下按键"),
|
"profileAutoUpdateIntervalInvalidValidationDesc":
|
||||||
"preview": MessageLookupByLibrary.simpleMessage("预览"),
|
MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"),
|
||||||
"profile": MessageLookupByLibrary.simpleMessage("配置"),
|
"profileAutoUpdateIntervalNullValidationDesc":
|
||||||
"profileAutoUpdateIntervalInvalidValidationDesc":
|
MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"),
|
||||||
MessageLookupByLibrary.simpleMessage("请输入有效间隔时间格式"),
|
"profileHasUpdate": MessageLookupByLibrary.simpleMessage(
|
||||||
"profileAutoUpdateIntervalNullValidationDesc":
|
"配置文件已经修改,是否关闭自动更新 ",
|
||||||
MessageLookupByLibrary.simpleMessage("请输入自动更新间隔时间"),
|
),
|
||||||
"profileHasUpdate":
|
"profileNameNullValidationDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
MessageLookupByLibrary.simpleMessage("配置文件已经修改,是否关闭自动更新 "),
|
"请输入配置名称",
|
||||||
"profileNameNullValidationDesc":
|
),
|
||||||
MessageLookupByLibrary.simpleMessage("请输入配置名称"),
|
"profileParseErrorDesc": MessageLookupByLibrary.simpleMessage("配置文件解析错误"),
|
||||||
"profileParseErrorDesc":
|
"profileUrlInvalidValidationDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
MessageLookupByLibrary.simpleMessage("配置文件解析错误"),
|
"请输入有效配置URL",
|
||||||
"profileUrlInvalidValidationDesc":
|
),
|
||||||
MessageLookupByLibrary.simpleMessage("请输入有效配置URL"),
|
"profileUrlNullValidationDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"profileUrlNullValidationDesc":
|
"请输入配置URL",
|
||||||
MessageLookupByLibrary.simpleMessage("请输入配置URL"),
|
),
|
||||||
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
|
"profiles": MessageLookupByLibrary.simpleMessage("配置"),
|
||||||
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
|
"profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"),
|
||||||
"project": MessageLookupByLibrary.simpleMessage("项目"),
|
"project": MessageLookupByLibrary.simpleMessage("项目"),
|
||||||
"providers": MessageLookupByLibrary.simpleMessage("提供者"),
|
"providers": MessageLookupByLibrary.simpleMessage("提供者"),
|
||||||
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
|
"proxies": MessageLookupByLibrary.simpleMessage("代理"),
|
||||||
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
|
"proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"),
|
||||||
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"),
|
"proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"),
|
||||||
"proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"),
|
"proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"),
|
||||||
"proxyNameserverDesc":
|
"proxyNameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"),
|
||||||
MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"),
|
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
|
||||||
"proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"),
|
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
|
||||||
"proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"),
|
"proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"),
|
||||||
"proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"),
|
"prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"),
|
||||||
"prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"),
|
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
|
||||||
"qrcode": MessageLookupByLibrary.simpleMessage("二维码"),
|
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
|
||||||
"qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"),
|
"recovery": MessageLookupByLibrary.simpleMessage("恢复"),
|
||||||
"recovery": MessageLookupByLibrary.simpleMessage("恢复"),
|
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
|
||||||
"recoveryAll": MessageLookupByLibrary.simpleMessage("恢复所有数据"),
|
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
|
||||||
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
|
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
|
||||||
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
|
"regExp": MessageLookupByLibrary.simpleMessage("正则"),
|
||||||
"regExp": MessageLookupByLibrary.simpleMessage("正则"),
|
"remote": MessageLookupByLibrary.simpleMessage("远程"),
|
||||||
"remote": MessageLookupByLibrary.simpleMessage("远程"),
|
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
|
||||||
"remoteBackupDesc": MessageLookupByLibrary.simpleMessage("备份数据到WebDAV"),
|
"remoteRecoveryDesc": MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
|
||||||
"remoteRecoveryDesc":
|
"remove": MessageLookupByLibrary.simpleMessage("移除"),
|
||||||
MessageLookupByLibrary.simpleMessage("通过WebDAV恢复数据"),
|
"requests": MessageLookupByLibrary.simpleMessage("请求"),
|
||||||
"remove": MessageLookupByLibrary.simpleMessage("移除"),
|
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
|
||||||
"requests": MessageLookupByLibrary.simpleMessage("请求"),
|
"reset": MessageLookupByLibrary.simpleMessage("重置"),
|
||||||
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
|
"resetTip": MessageLookupByLibrary.simpleMessage("确定要重置吗?"),
|
||||||
"reset": MessageLookupByLibrary.simpleMessage("重置"),
|
"resources": MessageLookupByLibrary.simpleMessage("资源"),
|
||||||
"resetTip": MessageLookupByLibrary.simpleMessage("确定要重置吗?"),
|
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
|
||||||
"resources": MessageLookupByLibrary.simpleMessage("资源"),
|
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
|
||||||
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
|
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"),
|
"DNS连接跟随rules,需配置proxy-server-nameserver",
|
||||||
"respectRulesDesc": MessageLookupByLibrary.simpleMessage(
|
),
|
||||||
"DNS连接跟随rules,需配置proxy-server-nameserver"),
|
"routeAddress": MessageLookupByLibrary.simpleMessage("路由地址"),
|
||||||
"routeAddress": MessageLookupByLibrary.simpleMessage("路由地址"),
|
"routeAddressDesc": MessageLookupByLibrary.simpleMessage("配置监听路由地址"),
|
||||||
"routeAddressDesc": MessageLookupByLibrary.simpleMessage("配置监听路由地址"),
|
"routeMode": MessageLookupByLibrary.simpleMessage("路由模式"),
|
||||||
"routeMode": MessageLookupByLibrary.simpleMessage("路由模式"),
|
"routeMode_bypassPrivate": MessageLookupByLibrary.simpleMessage("绕过私有路由地址"),
|
||||||
"routeMode_bypassPrivate":
|
"routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"),
|
||||||
MessageLookupByLibrary.simpleMessage("绕过私有路由地址"),
|
"rule": MessageLookupByLibrary.simpleMessage("规则"),
|
||||||
"routeMode_config": MessageLookupByLibrary.simpleMessage("使用配置"),
|
"ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"),
|
||||||
"rule": MessageLookupByLibrary.simpleMessage("规则"),
|
"save": MessageLookupByLibrary.simpleMessage("保存"),
|
||||||
"ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"),
|
"search": MessageLookupByLibrary.simpleMessage("搜索"),
|
||||||
"save": MessageLookupByLibrary.simpleMessage("保存"),
|
"seconds": MessageLookupByLibrary.simpleMessage("秒"),
|
||||||
"search": MessageLookupByLibrary.simpleMessage("搜索"),
|
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
|
||||||
"seconds": MessageLookupByLibrary.simpleMessage("秒"),
|
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
|
||||||
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
|
"settings": MessageLookupByLibrary.simpleMessage("设置"),
|
||||||
"selected": MessageLookupByLibrary.simpleMessage("已选择"),
|
"show": MessageLookupByLibrary.simpleMessage("显示"),
|
||||||
"settings": MessageLookupByLibrary.simpleMessage("设置"),
|
"shrink": MessageLookupByLibrary.simpleMessage("紧凑"),
|
||||||
"show": MessageLookupByLibrary.simpleMessage("显示"),
|
"silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"),
|
||||||
"shrink": MessageLookupByLibrary.simpleMessage("紧凑"),
|
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
|
||||||
"silentLaunch": MessageLookupByLibrary.simpleMessage("静默启动"),
|
"size": MessageLookupByLibrary.simpleMessage("尺寸"),
|
||||||
"silentLaunchDesc": MessageLookupByLibrary.simpleMessage("后台启动"),
|
"sort": MessageLookupByLibrary.simpleMessage("排序"),
|
||||||
"size": MessageLookupByLibrary.simpleMessage("尺寸"),
|
"source": MessageLookupByLibrary.simpleMessage("来源"),
|
||||||
"sort": MessageLookupByLibrary.simpleMessage("排序"),
|
"stackMode": MessageLookupByLibrary.simpleMessage("栈模式"),
|
||||||
"source": MessageLookupByLibrary.simpleMessage("来源"),
|
"standard": MessageLookupByLibrary.simpleMessage("标准"),
|
||||||
"stackMode": MessageLookupByLibrary.simpleMessage("栈模式"),
|
"start": MessageLookupByLibrary.simpleMessage("启动"),
|
||||||
"standard": MessageLookupByLibrary.simpleMessage("标准"),
|
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
|
||||||
"start": MessageLookupByLibrary.simpleMessage("启动"),
|
"status": MessageLookupByLibrary.simpleMessage("状态"),
|
||||||
"startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."),
|
"statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"),
|
||||||
"status": MessageLookupByLibrary.simpleMessage("状态"),
|
"stop": MessageLookupByLibrary.simpleMessage("暂停"),
|
||||||
"statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"),
|
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
|
||||||
"stop": MessageLookupByLibrary.simpleMessage("暂停"),
|
"style": MessageLookupByLibrary.simpleMessage("风格"),
|
||||||
"stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."),
|
"submit": MessageLookupByLibrary.simpleMessage("提交"),
|
||||||
"style": MessageLookupByLibrary.simpleMessage("风格"),
|
"sync": MessageLookupByLibrary.simpleMessage("同步"),
|
||||||
"submit": MessageLookupByLibrary.simpleMessage("提交"),
|
"system": MessageLookupByLibrary.simpleMessage("系统"),
|
||||||
"sync": MessageLookupByLibrary.simpleMessage("同步"),
|
"systemFont": MessageLookupByLibrary.simpleMessage("系统字体"),
|
||||||
"system": MessageLookupByLibrary.simpleMessage("系统"),
|
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
|
||||||
"systemFont": MessageLookupByLibrary.simpleMessage("系统字体"),
|
"systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"),
|
||||||
"systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"),
|
"tab": MessageLookupByLibrary.simpleMessage("标签页"),
|
||||||
"systemProxyDesc": MessageLookupByLibrary.simpleMessage("设置系统代理"),
|
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
|
||||||
"tab": MessageLookupByLibrary.simpleMessage("标签页"),
|
"tabAnimationDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"tabAnimation": MessageLookupByLibrary.simpleMessage("选项卡动画"),
|
"开启后,主页选项卡将添加切换动画",
|
||||||
"tabAnimationDesc":
|
),
|
||||||
MessageLookupByLibrary.simpleMessage("开启后,主页选项卡将添加切换动画"),
|
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
|
||||||
"tcpConcurrent": MessageLookupByLibrary.simpleMessage("TCP并发"),
|
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
|
||||||
"tcpConcurrentDesc": MessageLookupByLibrary.simpleMessage("开启后允许TCP并发"),
|
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
|
||||||
"testUrl": MessageLookupByLibrary.simpleMessage("测速链接"),
|
"theme": MessageLookupByLibrary.simpleMessage("主题"),
|
||||||
"theme": MessageLookupByLibrary.simpleMessage("主题"),
|
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
|
||||||
"themeColor": MessageLookupByLibrary.simpleMessage("主题色彩"),
|
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
|
||||||
"themeDesc": MessageLookupByLibrary.simpleMessage("设置深色模式,调整色彩"),
|
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
|
||||||
"themeMode": MessageLookupByLibrary.simpleMessage("主题模式"),
|
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
|
||||||
"threeColumns": MessageLookupByLibrary.simpleMessage("三列"),
|
"tight": MessageLookupByLibrary.simpleMessage("紧凑"),
|
||||||
"tight": MessageLookupByLibrary.simpleMessage("紧凑"),
|
"time": MessageLookupByLibrary.simpleMessage("时间"),
|
||||||
"time": MessageLookupByLibrary.simpleMessage("时间"),
|
"tip": MessageLookupByLibrary.simpleMessage("提示"),
|
||||||
"tip": MessageLookupByLibrary.simpleMessage("提示"),
|
"toggle": MessageLookupByLibrary.simpleMessage("切换"),
|
||||||
"toggle": MessageLookupByLibrary.simpleMessage("切换"),
|
"tools": MessageLookupByLibrary.simpleMessage("工具"),
|
||||||
"tools": MessageLookupByLibrary.simpleMessage("工具"),
|
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
|
||||||
"trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"),
|
"tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
|
||||||
"tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"),
|
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
|
||||||
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
|
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
|
||||||
"twoColumns": MessageLookupByLibrary.simpleMessage("两列"),
|
"unableToUpdateCurrentProfileDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"unableToUpdateCurrentProfileDesc":
|
"无法更新当前配置文件",
|
||||||
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"),
|
),
|
||||||
"unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"),
|
"unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"),
|
||||||
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"),
|
"unifiedDelayDesc": MessageLookupByLibrary.simpleMessage("去除握手等额外延迟"),
|
||||||
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
|
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
|
||||||
"update": MessageLookupByLibrary.simpleMessage("更新"),
|
"update": MessageLookupByLibrary.simpleMessage("更新"),
|
||||||
"upload": MessageLookupByLibrary.simpleMessage("上传"),
|
"upload": MessageLookupByLibrary.simpleMessage("上传"),
|
||||||
"url": MessageLookupByLibrary.simpleMessage("URL"),
|
"url": MessageLookupByLibrary.simpleMessage("URL"),
|
||||||
"urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"),
|
"urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"),
|
||||||
"useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"),
|
"useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"),
|
||||||
"useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"),
|
"useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"),
|
||||||
"value": MessageLookupByLibrary.simpleMessage("值"),
|
"value": MessageLookupByLibrary.simpleMessage("值"),
|
||||||
"view": MessageLookupByLibrary.simpleMessage("查看"),
|
"view": MessageLookupByLibrary.simpleMessage("查看"),
|
||||||
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
|
"vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"),
|
||||||
"vpnEnableDesc":
|
"vpnEnableDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
MessageLookupByLibrary.simpleMessage("通过VpnService自动路由系统所有流量"),
|
"通过VpnService自动路由系统所有流量",
|
||||||
"vpnSystemProxyDesc":
|
),
|
||||||
MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"),
|
"vpnSystemProxyDesc": MessageLookupByLibrary.simpleMessage(
|
||||||
"vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"),
|
"为VpnService附加HTTP代理",
|
||||||
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
|
),
|
||||||
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),
|
"vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"),
|
||||||
"years": MessageLookupByLibrary.simpleMessage("年"),
|
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
|
||||||
"zh_CN": MessageLookupByLibrary.simpleMessage("中文简体")
|
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),
|
||||||
};
|
"years": MessageLookupByLibrary.simpleMessage("年"),
|
||||||
|
"zh_CN": MessageLookupByLibrary.simpleMessage("中文简体"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
1182
lib/l10n/l10n.dart
1182
lib/l10n/l10n.dart
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user