mirror of
https://github.com/2dust/v2rayNG.git
synced 2026-10-29 06:31:54 +00:00
Random and Round-Robin strategies can exclude dead outbounds (#5926)
* Add policy group fallback health checks * Refine policy group observatory defaults * Simplify policy group fallback * Localize policy group fallback settings * Update CoreConfigContextBuilder.kt * Update CoreConfigManager.kt * Restore implicit policy group fallback --------- Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
This commit is contained in:
1 parent
ce75aad55e
commit
2be47f37fe
14 files changed
+118
-11
No files matched your search
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.CoreConfigContext
|
import com.v2ray.ang.dto.CoreConfigContext
|
||||||
import com.v2ray.ang.dto.entities.ProfileItem
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
|
import com.v2ray.ang.enums.BalancerStrategyType
|
||||||
import com.v2ray.ang.enums.CoreResolvedType
|
import com.v2ray.ang.enums.CoreResolvedType
|
||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
import com.v2ray.ang.extension.isComplexType
|
import com.v2ray.ang.extension.isComplexType
|
||||||
@@ -43,12 +44,14 @@ object CoreConfigContextBuilder {
|
|||||||
|
|
||||||
// Step 2: Resolve all non-builtin routing outbound tags.
|
// Step 2: Resolve all non-builtin routing outbound tags.
|
||||||
val routingResolvedOutbounds = resolveRoutingOutbounds()
|
val routingResolvedOutbounds = resolveRoutingOutbounds()
|
||||||
|
val resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds
|
||||||
|
val fallbackResolvedOutbounds = resolveFallbackOutbounds(resolvedOutbounds)
|
||||||
val routingDomainRules = collectRoutingDomainRulesForDns()
|
val routingDomainRules = collectRoutingDomainRulesForDns()
|
||||||
|
|
||||||
return CoreConfigContext(
|
return CoreConfigContext(
|
||||||
context = context,
|
context = context,
|
||||||
guid = guid,
|
guid = guid,
|
||||||
resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds,
|
resolvedOutbounds = resolvedOutbounds + fallbackResolvedOutbounds,
|
||||||
routingDomainRules = routingDomainRules,
|
routingDomainRules = routingDomainRules,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -246,4 +249,25 @@ object CoreConfigContextBuilder {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve and collect fallback outbounds from all POLICYGROUP nodes.
|
||||||
|
*
|
||||||
|
* Fallback targets must not overlap with already resolved tags or builtin tags.
|
||||||
|
*/
|
||||||
|
private fun resolveFallbackOutbounds(resolvedOutbounds: List<CoreConfigContext.ResolvedOutbound>): List<CoreConfigContext.ResolvedOutbound> {
|
||||||
|
return resolvedOutbounds
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.resolvedType == CoreResolvedType.POLICYGROUP }
|
||||||
|
.filter { BalancerStrategyType.from(it.profile.policyGroupType).supportsObservatory && it.profile.policyGroupTestOutbounds != false }
|
||||||
|
.mapNotNull { it.profile.policyGroupFallbackTag }
|
||||||
|
.filter { it !in AppConfig.BUILTIN_OUTBOUND_TAGS && resolvedOutbounds.none { outbound -> outbound.tag == it } }
|
||||||
|
.distinct()
|
||||||
|
.mapNotNull { tag ->
|
||||||
|
SettingsManager.getServerViaRemarks(tag)
|
||||||
|
?.takeUnless { it.configType == EConfigType.CUSTOM || it.configType == EConfigType.POLICYGROUP }
|
||||||
|
?.let { resolveOutbound(tag, it) }
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -374,10 +374,19 @@ object CoreConfigManager {
|
|||||||
} else {
|
} else {
|
||||||
"${AppConfig.TAG_BALANCER_PRE}-${resolvedOutbound.tag}"
|
"${AppConfig.TAG_BALANCER_PRE}-${resolvedOutbound.tag}"
|
||||||
}
|
}
|
||||||
|
val strategyType = BalancerStrategyType.from(resolvedOutbound.profile.policyGroupType)
|
||||||
|
val fallbackTag = if (strategyType.supportsObservatory && resolvedOutbound.profile.policyGroupTestOutbounds != false) {
|
||||||
|
resolvedOutbound.profile.policyGroupFallbackTag
|
||||||
|
?.takeIf { it.isNotEmpty() && it != AppConfig.TAG_PROXY }
|
||||||
|
// Xray excludes dead random/roundRobin candidates only when fallbackTag is set;
|
||||||
|
// without this default, an enabled empty field creates no observatory.
|
||||||
|
?: membersToAdd.first().tag
|
||||||
|
} else null
|
||||||
val strategy = buildBalancerStrategy(
|
val strategy = buildBalancerStrategy(
|
||||||
policyGroupType = resolvedOutbound.profile.policyGroupType,
|
strategyType = strategyType,
|
||||||
selector = listOf(memberTagPrefix),
|
selector = listOf(memberTagPrefix),
|
||||||
balancerTag = balancerTag,
|
balancerTag = balancerTag,
|
||||||
|
fallbackTag = fallbackTag,
|
||||||
)
|
)
|
||||||
val existingBalancers = v2rayConfig.routing.balancers?.toMutableList() ?: mutableListOf()
|
val existingBalancers = v2rayConfig.routing.balancers?.toMutableList() ?: mutableListOf()
|
||||||
if (existingBalancers.none { it.tag == balancerTag }) {
|
if (existingBalancers.none { it.tag == balancerTag }) {
|
||||||
@@ -1180,9 +1189,10 @@ object CoreConfigManager {
|
|||||||
* Build balancer and probe settings from one policy-group strategy value.
|
* Build balancer and probe settings from one policy-group strategy value.
|
||||||
*/
|
*/
|
||||||
private fun buildBalancerStrategy(
|
private fun buildBalancerStrategy(
|
||||||
policyGroupType: String?,
|
strategyType: BalancerStrategyType,
|
||||||
selector: List<String>,
|
selector: List<String>,
|
||||||
balancerTag: String = AppConfig.TAG_BALANCER,
|
balancerTag: String = AppConfig.TAG_BALANCER,
|
||||||
|
fallbackTag: String? = null,
|
||||||
): BalancerStrategy {
|
): BalancerStrategy {
|
||||||
val probeUrl = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DELAY_TEST_URL
|
val probeUrl = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DELAY_TEST_URL
|
||||||
val leastPingInterval = decodeObservatoryDuration(AppConfig.PREF_OBSERVATORY_LEAST_PING_INTERVAL, AppConfig.OBSERVATORY_LEAST_PING_INTERVAL)
|
val leastPingInterval = decodeObservatoryDuration(AppConfig.PREF_OBSERVATORY_LEAST_PING_INTERVAL, AppConfig.OBSERVATORY_LEAST_PING_INTERVAL)
|
||||||
@@ -1190,13 +1200,13 @@ object CoreConfigManager {
|
|||||||
val leastLoadMethod = MmkvManager.decodeSettingsString(AppConfig.PREF_OBSERVATORY_LEAST_LOAD_METHOD, AppConfig.OBSERVATORY_LEAST_LOAD_METHOD)
|
val leastLoadMethod = MmkvManager.decodeSettingsString(AppConfig.PREF_OBSERVATORY_LEAST_LOAD_METHOD, AppConfig.OBSERVATORY_LEAST_LOAD_METHOD)
|
||||||
val leastLoadSampling = decodeObservatorySampling()
|
val leastLoadSampling = decodeObservatorySampling()
|
||||||
val leastLoadTimeout = decodeObservatoryDuration(AppConfig.PREF_OBSERVATORY_LEAST_LOAD_TIMEOUT, AppConfig.OBSERVATORY_LEAST_LOAD_TIMEOUT)
|
val leastLoadTimeout = decodeObservatoryDuration(AppConfig.PREF_OBSERVATORY_LEAST_LOAD_TIMEOUT, AppConfig.OBSERVATORY_LEAST_LOAD_TIMEOUT)
|
||||||
val strategyType = BalancerStrategyType.from(policyGroupType)
|
|
||||||
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
||||||
tag = balancerTag,
|
tag = balancerTag,
|
||||||
selector = selector,
|
selector = selector,
|
||||||
|
fallbackTag = fallbackTag,
|
||||||
strategy = V2rayConfig.RoutingBean.StrategyObject(type = strategyType.policyGroupType)
|
strategy = V2rayConfig.RoutingBean.StrategyObject(type = strategyType.policyGroupType)
|
||||||
)
|
)
|
||||||
val observatory = if (strategyType.requiresObservatory) {
|
val observatory = if (strategyType.requiresObservatory || fallbackTag != null) {
|
||||||
V2rayConfig.ObservatoryObject(
|
V2rayConfig.ObservatoryObject(
|
||||||
subjectSelector = selector,
|
subjectSelector = selector,
|
||||||
probeUrl = probeUrl,
|
probeUrl = probeUrl,
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ data class ProfileItem(
|
|||||||
var policyGroupType: String? = null,
|
var policyGroupType: String? = null,
|
||||||
var policyGroupSubscriptionId: String? = null,
|
var policyGroupSubscriptionId: String? = null,
|
||||||
var policyGroupFilter: String? = null,
|
var policyGroupFilter: String? = null,
|
||||||
|
var policyGroupTestOutbounds: Boolean? = null,
|
||||||
|
var policyGroupFallbackTag: String? = null,
|
||||||
var proxyChainProfiles: String? = null,
|
var proxyChainProfiles: String? = null,
|
||||||
|
|
||||||
var browserDialerMode: String? = null,
|
var browserDialerMode: String? = null,
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ package com.v2ray.ang.enums
|
|||||||
* @param policyGroupTypeValue The numeric string stored in [com.v2ray.ang.dto.entities.ProfileItem.policyGroupType].
|
* @param policyGroupTypeValue The numeric string stored in [com.v2ray.ang.dto.entities.ProfileItem.policyGroupType].
|
||||||
* @param requiresBurstObservatory Whether this strategy needs a burstObservatory (leastLoad).
|
* @param requiresBurstObservatory Whether this strategy needs a burstObservatory (leastLoad).
|
||||||
* @param requiresObservatory Whether this strategy needs an observatory (leastPing).
|
* @param requiresObservatory Whether this strategy needs an observatory (leastPing).
|
||||||
|
* @param supportsObservatory Whether an observatory can health-filter this strategy.
|
||||||
*/
|
*/
|
||||||
enum class BalancerStrategyType(
|
enum class BalancerStrategyType(
|
||||||
val policyGroupType: String,
|
val policyGroupType: String,
|
||||||
val policyGroupTypeValue: String,
|
val policyGroupTypeValue: String,
|
||||||
val requiresBurstObservatory: Boolean = false,
|
val requiresBurstObservatory: Boolean = false,
|
||||||
val requiresObservatory: Boolean = false,
|
val requiresObservatory: Boolean = false,
|
||||||
|
val supportsObservatory: Boolean = false,
|
||||||
) {
|
) {
|
||||||
LEAST_LOAD("leastLoad", "1", requiresBurstObservatory = true),
|
LEAST_LOAD("leastLoad", "1", requiresBurstObservatory = true),
|
||||||
RANDOM("random", "2"),
|
RANDOM("random", "2", supportsObservatory = true),
|
||||||
ROUND_ROBIN("roundRobin", "3"),
|
ROUND_ROBIN("roundRobin", "3", supportsObservatory = true),
|
||||||
LEAST_PING("leastPing", "", requiresObservatory = true); // default / else
|
LEAST_PING("leastPing", "", requiresObservatory = true); // default / else
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.consumeWindowInsets
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -21,17 +23,22 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.compose.AppTopBar
|
import com.v2ray.ang.compose.AppTopBar
|
||||||
import com.v2ray.ang.compose.ConfirmDialog
|
import com.v2ray.ang.compose.ConfirmDialog
|
||||||
import com.v2ray.ang.compose.FormDropdownField
|
import com.v2ray.ang.compose.FormDropdownField
|
||||||
import com.v2ray.ang.compose.FormTextField
|
import com.v2ray.ang.compose.FormTextField
|
||||||
|
import com.v2ray.ang.compose.SettingsSwitchItem
|
||||||
import com.v2ray.ang.dto.entities.ProfileItem
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
|
import com.v2ray.ang.enums.BalancerStrategyType
|
||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.ui.base.BaseComponentActivity
|
import com.v2ray.ang.ui.base.BaseComponentActivity
|
||||||
|
|
||||||
class ServerGroupActivity : BaseComponentActivity() {
|
class ServerGroupActivity : BaseComponentActivity() {
|
||||||
@@ -45,21 +52,32 @@ class ServerGroupActivity : BaseComponentActivity() {
|
|||||||
private val subscriptionId by lazy { intent.getStringExtra("subscriptionId") }
|
private val subscriptionId by lazy { intent.getStringExtra("subscriptionId") }
|
||||||
private val subIds = mutableListOf<String>()
|
private val subIds = mutableListOf<String>()
|
||||||
private val subDisplay = mutableListOf<String>()
|
private val subDisplay = mutableListOf<String>()
|
||||||
|
private lateinit var fallbackSuggestions: List<String>
|
||||||
|
|
||||||
private lateinit var initialRemarks: String
|
private lateinit var initialRemarks: String
|
||||||
private lateinit var initialFilter: String
|
private lateinit var initialFilter: String
|
||||||
private var initialType: Int = 0
|
private var initialType: Int = 0
|
||||||
private var initialSubIndex: Int = 0
|
private var initialSubIndex: Int = 0
|
||||||
|
private var initialTestOutbounds: Boolean = false
|
||||||
|
private lateinit var initialFallbackTag: String
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(editGuid)
|
val config = MmkvManager.decodeServerConfig(editGuid)
|
||||||
populateSubscriptionSpinner()
|
populateSubscriptionSpinner()
|
||||||
|
fallbackSuggestions = (
|
||||||
|
BUILTIN_OUTBOUND_TAGS + SettingsManager.getProfileRemarks(
|
||||||
|
excludeConfigTypes = setOf(EConfigType.CUSTOM, EConfigType.POLICYGROUP)
|
||||||
|
)
|
||||||
|
).filter { it != TAG_PROXY }
|
||||||
|
|
||||||
initialRemarks = config?.remarks ?: ""
|
initialRemarks = config?.remarks ?: ""
|
||||||
initialFilter = config?.policyGroupFilter ?: ""
|
initialFilter = config?.policyGroupFilter ?: ""
|
||||||
initialType = config?.policyGroupType?.toIntOrNull() ?: 0
|
initialType = config?.policyGroupType?.toIntOrNull() ?: 0
|
||||||
|
initialTestOutbounds = config == null || config.policyGroupTestOutbounds != false ||
|
||||||
|
!BalancerStrategyType.from(config.policyGroupType).supportsObservatory
|
||||||
|
initialFallbackTag = config?.policyGroupFallbackTag.orEmpty()
|
||||||
initialSubIndex = if (config != null) {
|
initialSubIndex = if (config != null) {
|
||||||
subIds.indexOf(config.policyGroupSubscriptionId ?: "").let { if (it >= 0) it else 0 }
|
subIds.indexOf(config.policyGroupSubscriptionId ?: "").let { if (it >= 0) it else 0 }
|
||||||
} else if (subscriptionId.isNotNullEmpty()) {
|
} else if (subscriptionId.isNotNullEmpty()) {
|
||||||
@@ -77,8 +95,13 @@ class ServerGroupActivity : BaseComponentActivity() {
|
|||||||
initialFilter = initialFilter,
|
initialFilter = initialFilter,
|
||||||
initialType = initialType,
|
initialType = initialType,
|
||||||
initialSubIndex = initialSubIndex,
|
initialSubIndex = initialSubIndex,
|
||||||
|
initialTestOutbounds = initialTestOutbounds,
|
||||||
|
initialFallbackTag = initialFallbackTag,
|
||||||
|
fallbackSuggestions = fallbackSuggestions,
|
||||||
onBackClick = { finish() },
|
onBackClick = { finish() },
|
||||||
onSave = { remarks, filter, typeIdx, subIdx -> saveServer(remarks, filter, typeIdx, subIdx) },
|
onSave = { remarks, filter, typeIdx, subIdx, testOutbounds, fallbackTag ->
|
||||||
|
saveServer(remarks, filter, typeIdx, subIdx, testOutbounds, fallbackTag)
|
||||||
|
},
|
||||||
onDelete = { deleteServer() }
|
onDelete = { deleteServer() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -87,7 +110,9 @@ class ServerGroupActivity : BaseComponentActivity() {
|
|||||||
remarks: String,
|
remarks: String,
|
||||||
filter: String,
|
filter: String,
|
||||||
typeIdx: Int,
|
typeIdx: Int,
|
||||||
subIdx: Int
|
subIdx: Int,
|
||||||
|
testOutbounds: Boolean,
|
||||||
|
fallbackTag: String,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (remarks.isBlank()) {
|
if (remarks.isBlank()) {
|
||||||
toast(R.string.server_lab_remarks)
|
toast(R.string.server_lab_remarks)
|
||||||
@@ -103,6 +128,8 @@ class ServerGroupActivity : BaseComponentActivity() {
|
|||||||
config.policyGroupType = typeIdx.toString()
|
config.policyGroupType = typeIdx.toString()
|
||||||
config.policyGroupSubscriptionId =
|
config.policyGroupSubscriptionId =
|
||||||
subIds.getOrNull(subIdx)
|
subIds.getOrNull(subIdx)
|
||||||
|
config.policyGroupTestOutbounds = testOutbounds
|
||||||
|
config.policyGroupFallbackTag = fallbackTag.trim().takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
if (
|
if (
|
||||||
config.subscriptionId.isEmpty() &&
|
config.subscriptionId.isEmpty() &&
|
||||||
@@ -189,8 +216,11 @@ fun ServerGroupScreen(
|
|||||||
initialFilter: String,
|
initialFilter: String,
|
||||||
initialType: Int,
|
initialType: Int,
|
||||||
initialSubIndex: Int,
|
initialSubIndex: Int,
|
||||||
|
initialTestOutbounds: Boolean,
|
||||||
|
initialFallbackTag: String,
|
||||||
|
fallbackSuggestions: List<String>,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onSave: (String, String, Int, Int) -> Boolean,
|
onSave: (String, String, Int, Int, Boolean, String) -> Boolean,
|
||||||
onDelete: () -> Unit
|
onDelete: () -> Unit
|
||||||
) {
|
) {
|
||||||
val typeEntries = stringArrayResource(R.array.policy_group_type).toList()
|
val typeEntries = stringArrayResource(R.array.policy_group_type).toList()
|
||||||
@@ -199,8 +229,12 @@ fun ServerGroupScreen(
|
|||||||
var filter by rememberSaveable { mutableStateOf(initialFilter) }
|
var filter by rememberSaveable { mutableStateOf(initialFilter) }
|
||||||
var typeValue by rememberSaveable { mutableStateOf(typeEntries.getOrNull(initialType).orEmpty()) }
|
var typeValue by rememberSaveable { mutableStateOf(typeEntries.getOrNull(initialType).orEmpty()) }
|
||||||
var subValue by rememberSaveable { mutableStateOf(subDisplay.getOrNull(initialSubIndex).orEmpty()) }
|
var subValue by rememberSaveable { mutableStateOf(subDisplay.getOrNull(initialSubIndex).orEmpty()) }
|
||||||
|
var testOutbounds by rememberSaveable { mutableStateOf(initialTestOutbounds) }
|
||||||
|
var fallbackTag by rememberSaveable { mutableStateOf(initialFallbackTag) }
|
||||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||||
val showDelete = editGuid.isNotEmpty() && !isRunning
|
val showDelete = editGuid.isNotEmpty() && !isRunning
|
||||||
|
val selectedType = typeEntries.indexOf(typeValue).coerceAtLeast(0).toString()
|
||||||
|
val supportsObservatory = BalancerStrategyType.from(selectedType).supportsObservatory
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
contentWindowInsets = ScaffoldDefaults.contentWindowInsets,
|
contentWindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||||
@@ -217,7 +251,7 @@ fun ServerGroupScreen(
|
|||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
val typeIdx = typeEntries.indexOf(typeValue).coerceAtLeast(0)
|
val typeIdx = typeEntries.indexOf(typeValue).coerceAtLeast(0)
|
||||||
val subIdx = subDisplay.indexOf(subValue).coerceAtLeast(0)
|
val subIdx = subDisplay.indexOf(subValue).coerceAtLeast(0)
|
||||||
onSave(remarks, filter, typeIdx, subIdx)
|
onSave(remarks, filter, typeIdx, subIdx, testOutbounds, fallbackTag)
|
||||||
}) {
|
}) {
|
||||||
Icon(painterResource(R.drawable.ic_fab_check), contentDescription = stringResource(R.string.menu_item_save_config))
|
Icon(painterResource(R.drawable.ic_fab_check), contentDescription = stringResource(R.string.menu_item_save_config))
|
||||||
}
|
}
|
||||||
@@ -232,6 +266,7 @@ fun ServerGroupScreen(
|
|||||||
.consumeWindowInsets(innerPadding)
|
.consumeWindowInsets(innerPadding)
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
FormTextField(stringResource(R.string.server_lab_remarks), remarks, { remarks = it })
|
FormTextField(stringResource(R.string.server_lab_remarks), remarks, { remarks = it })
|
||||||
FormDropdownField(
|
FormDropdownField(
|
||||||
@@ -247,6 +282,22 @@ fun ServerGroupScreen(
|
|||||||
onValueChange = { subValue = it }
|
onValueChange = { subValue = it }
|
||||||
)
|
)
|
||||||
FormTextField(stringResource(R.string.title_policy_group_subscription_filter), filter, { filter = it })
|
FormTextField(stringResource(R.string.title_policy_group_subscription_filter), filter, { filter = it })
|
||||||
|
if (supportsObservatory) {
|
||||||
|
SettingsSwitchItem(
|
||||||
|
title = stringResource(R.string.title_policy_group_test_outbounds),
|
||||||
|
checked = testOutbounds,
|
||||||
|
onCheckedChange = { testOutbounds = it }
|
||||||
|
)
|
||||||
|
if (testOutbounds) {
|
||||||
|
FormDropdownField(
|
||||||
|
label = stringResource(R.string.title_policy_group_fallback),
|
||||||
|
value = fallbackTag,
|
||||||
|
options = fallbackSuggestions,
|
||||||
|
onValueChange = { fallbackTag = it },
|
||||||
|
editable = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -387,6 +387,8 @@
|
|||||||
<string name="title_policy_group_type">Policy group type</string>
|
<string name="title_policy_group_type">Policy group type</string>
|
||||||
<string name="title_policy_group_subscription_id">From subscription group</string>
|
<string name="title_policy_group_subscription_id">From subscription group</string>
|
||||||
<string name="title_policy_group_subscription_filter">Remarks regular filter</string>
|
<string name="title_policy_group_subscription_filter">Remarks regular filter</string>
|
||||||
|
<string name="title_policy_group_test_outbounds">اختبار المخارج</string>
|
||||||
|
<string name="title_policy_group_fallback">المخرج الاحتياطي (اختياري)</string>
|
||||||
<string name="server_proxy_chain_members">اعضاء سلسلة الوكيل</string>
|
<string name="server_proxy_chain_members">اعضاء سلسلة الوكيل</string>
|
||||||
<string name="server_proxy_chain_pick_members">اضغط هنا لاختيار عضو</string>
|
<string name="server_proxy_chain_pick_members">اضغط هنا لاختيار عضو</string>
|
||||||
<string name="server_proxy_chain_member_unselected">اختر عضوا</string>
|
<string name="server_proxy_chain_member_unselected">اختر عضوا</string>
|
||||||
|
|||||||
@@ -386,6 +386,8 @@
|
|||||||
<string name="title_policy_group_type">Policy group type</string>
|
<string name="title_policy_group_type">Policy group type</string>
|
||||||
<string name="title_policy_group_subscription_id">From subscription group</string>
|
<string name="title_policy_group_subscription_id">From subscription group</string>
|
||||||
<string name="title_policy_group_subscription_filter">Remarks regular filter</string>
|
<string name="title_policy_group_subscription_filter">Remarks regular filter</string>
|
||||||
|
<string name="title_policy_group_test_outbounds">আউটবাউন্ড পরীক্ষা করুন</string>
|
||||||
|
<string name="title_policy_group_fallback">ফলব্যাক আউটবাউন্ড (ঐচ্ছিক)</string>
|
||||||
<string name="server_proxy_chain_members">Proxy chain members</string>
|
<string name="server_proxy_chain_members">Proxy chain members</string>
|
||||||
<string name="server_proxy_chain_pick_members">Tap here to pick member</string>
|
<string name="server_proxy_chain_pick_members">Tap here to pick member</string>
|
||||||
<string name="server_proxy_chain_member_unselected">Select a member</string>
|
<string name="server_proxy_chain_member_unselected">Select a member</string>
|
||||||
|
|||||||
@@ -392,6 +392,8 @@
|
|||||||
<string name="title_policy_group_type">نوع بونکۊ سیاست</string>
|
<string name="title_policy_group_type">نوع بونکۊ سیاست</string>
|
||||||
<string name="title_policy_group_subscription_id">ز بونکۊ اشتراک</string>
|
<string name="title_policy_group_subscription_id">ز بونکۊ اشتراک</string>
|
||||||
<string name="title_policy_group_subscription_filter">توزیهات فیلتر معمۊلی</string>
|
<string name="title_policy_group_subscription_filter">توزیهات فیلتر معمۊلی</string>
|
||||||
|
<string name="title_policy_group_test_outbounds">آزمایش بارتا</string>
|
||||||
|
<string name="title_policy_group_fallback">بارت جایگزین (اختیاری)</string>
|
||||||
<string name="server_proxy_chain_members">ٱئزا زنجیره پروکسی</string>
|
<string name="server_proxy_chain_members">ٱئزا زنجیره پروکسی</string>
|
||||||
<string name="server_proxy_chain_pick_members">سی پسند عوزو ایچو بزنین</string>
|
<string name="server_proxy_chain_pick_members">سی پسند عوزو ایچو بزنین</string>
|
||||||
<string name="server_proxy_chain_member_unselected">ی عوزو پسند کۊنین</string>
|
<string name="server_proxy_chain_member_unselected">ی عوزو پسند کۊنین</string>
|
||||||
|
|||||||
@@ -383,6 +383,8 @@
|
|||||||
<string name="title_policy_group_type">نوع گروه خط مشی</string>
|
<string name="title_policy_group_type">نوع گروه خط مشی</string>
|
||||||
<string name="title_policy_group_subscription_id">از گروه اشتراک</string>
|
<string name="title_policy_group_subscription_id">از گروه اشتراک</string>
|
||||||
<string name="title_policy_group_subscription_filter">توضیحات فیلتر معمولی</string>
|
<string name="title_policy_group_subscription_filter">توضیحات فیلتر معمولی</string>
|
||||||
|
<string name="title_policy_group_test_outbounds">آزمایش خروجیها</string>
|
||||||
|
<string name="title_policy_group_fallback">خروجی جایگزین (اختیاری)</string>
|
||||||
<string name="server_proxy_chain_members">اعضای زنجیره پروکسی</string>
|
<string name="server_proxy_chain_members">اعضای زنجیره پروکسی</string>
|
||||||
<string name="server_proxy_chain_pick_members">برای انتخاب عضو اینجا بزنید</string>
|
<string name="server_proxy_chain_pick_members">برای انتخاب عضو اینجا بزنید</string>
|
||||||
<string name="server_proxy_chain_member_unselected">یک عضو را انتخاب کنید</string>
|
<string name="server_proxy_chain_member_unselected">یک عضو را انتخاب کنید</string>
|
||||||
|
|||||||
@@ -398,6 +398,8 @@
|
|||||||
<string name="title_policy_group_type">Тип политики группы</string>
|
<string name="title_policy_group_type">Тип политики группы</string>
|
||||||
<string name="title_policy_group_subscription_id">Из группы подписки</string>
|
<string name="title_policy_group_subscription_id">Из группы подписки</string>
|
||||||
<string name="title_policy_group_subscription_filter">Название фильтра</string>
|
<string name="title_policy_group_subscription_filter">Название фильтра</string>
|
||||||
|
<string name="title_policy_group_test_outbounds">Проверять исходящие соединения</string>
|
||||||
|
<string name="title_policy_group_fallback">Резервное исходящее соединение (необязательно)</string>
|
||||||
|
|
||||||
<string name="server_proxy_chain_members">Участники цепочки прокси</string>
|
<string name="server_proxy_chain_members">Участники цепочки прокси</string>
|
||||||
<string name="server_proxy_chain_pick_members">Нажмите здесь, чтобы выбрать участника</string>
|
<string name="server_proxy_chain_pick_members">Нажмите здесь, чтобы выбрать участника</string>
|
||||||
|
|||||||
@@ -388,6 +388,8 @@
|
|||||||
<string name="title_policy_group_type">Policy group type</string>
|
<string name="title_policy_group_type">Policy group type</string>
|
||||||
<string name="title_policy_group_subscription_id">From subscription group</string>
|
<string name="title_policy_group_subscription_id">From subscription group</string>
|
||||||
<string name="title_policy_group_subscription_filter">Remarks regular filter</string>
|
<string name="title_policy_group_subscription_filter">Remarks regular filter</string>
|
||||||
|
<string name="title_policy_group_test_outbounds">Kiểm tra outbound</string>
|
||||||
|
<string name="title_policy_group_fallback">Outbound dự phòng (tùy chọn)</string>
|
||||||
<string name="server_proxy_chain_members">Proxy chain members</string>
|
<string name="server_proxy_chain_members">Proxy chain members</string>
|
||||||
<string name="server_proxy_chain_pick_members">Tap here to pick member</string>
|
<string name="server_proxy_chain_pick_members">Tap here to pick member</string>
|
||||||
<string name="server_proxy_chain_member_unselected">Select a member</string>
|
<string name="server_proxy_chain_member_unselected">Select a member</string>
|
||||||
|
|||||||
@@ -402,6 +402,8 @@
|
|||||||
<string name="title_policy_group_type">策略组类型</string>
|
<string name="title_policy_group_type">策略组类型</string>
|
||||||
<string name="title_policy_group_subscription_id">来自订阅分组</string>
|
<string name="title_policy_group_subscription_id">来自订阅分组</string>
|
||||||
<string name="title_policy_group_subscription_filter">别名正则过滤</string>
|
<string name="title_policy_group_subscription_filter">别名正则过滤</string>
|
||||||
|
<string name="title_policy_group_test_outbounds">测试出站</string>
|
||||||
|
<string name="title_policy_group_fallback">备用出站(可选)</string>
|
||||||
<string name="server_proxy_chain_members">代理链成员</string>
|
<string name="server_proxy_chain_members">代理链成员</string>
|
||||||
<string name="server_proxy_chain_pick_members">点此选择成员</string>
|
<string name="server_proxy_chain_pick_members">点此选择成员</string>
|
||||||
<string name="server_proxy_chain_member_unselected">请选择成员</string>
|
<string name="server_proxy_chain_member_unselected">请选择成员</string>
|
||||||
|
|||||||
@@ -400,6 +400,8 @@
|
|||||||
<string name="title_policy_group_type">策略群組類型</string>
|
<string name="title_policy_group_type">策略群組類型</string>
|
||||||
<string name="title_policy_group_subscription_id">來自訂閱分組</string>
|
<string name="title_policy_group_subscription_id">來自訂閱分組</string>
|
||||||
<string name="title_policy_group_subscription_filter">別名正規過濾</string>
|
<string name="title_policy_group_subscription_filter">別名正規過濾</string>
|
||||||
|
<string name="title_policy_group_test_outbounds">測試出口</string>
|
||||||
|
<string name="title_policy_group_fallback">備用出口(選填)</string>
|
||||||
<string name="server_proxy_chain_members">代理鏈成員</string>
|
<string name="server_proxy_chain_members">代理鏈成員</string>
|
||||||
<string name="server_proxy_chain_pick_members">點此選擇成員</string>
|
<string name="server_proxy_chain_pick_members">點此選擇成員</string>
|
||||||
<string name="server_proxy_chain_member_unselected">請選擇成員</string>
|
<string name="server_proxy_chain_member_unselected">請選擇成員</string>
|
||||||
|
|||||||
@@ -407,6 +407,8 @@
|
|||||||
<string name="title_policy_group_type">Policy group type</string>
|
<string name="title_policy_group_type">Policy group type</string>
|
||||||
<string name="title_policy_group_subscription_id">From subscription group</string>
|
<string name="title_policy_group_subscription_id">From subscription group</string>
|
||||||
<string name="title_policy_group_subscription_filter">Remarks regular filter</string>
|
<string name="title_policy_group_subscription_filter">Remarks regular filter</string>
|
||||||
|
<string name="title_policy_group_test_outbounds">Test outbounds</string>
|
||||||
|
<string name="title_policy_group_fallback">Fallback outbound (optional)</string>
|
||||||
|
|
||||||
<string name="server_proxy_chain_members">Proxy chain members</string>
|
<string name="server_proxy_chain_members">Proxy chain members</string>
|
||||||
<string name="server_proxy_chain_pick_members">Tap here to pick member</string>
|
<string name="server_proxy_chain_pick_members">Tap here to pick member</string>
|
||||||
|
|||||||
Reference in new issue
Block a user