Refine settings states and selector rows

Improve settings UX consistency across Compose screens: disabled setting text now uses proper muted alpha for title/description, domain strategy and geo source pickers now use shared `SettingsListItem` instead of custom click/dialog state, routing rule row UI was extracted into a reusable composable, and several settings toggles were un-gated so they can be changed outside VPN-only conditions.
This commit is contained in:
2dust committed 2026-07-20 17:05:42 +08:00
1 parent 6a00dff160
commit 7213f9fa9f
4 files changed
+115 -144

No files matched your search

@@ -50,8 +50,10 @@ private fun SettingsItemRow(
modifier: Modifier = Modifier,
trailing: @Composable (() -> Unit)? = null
) {
val contentColor = if (enabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurfaceVariant
val titleColor = if (enabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
val descriptionColor = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
Row(
modifier = modifier
@@ -65,7 +67,7 @@ private fun SettingsItemRow(
painter = icon,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = contentColor
tint = titleColor
)
Spacer(modifier = Modifier.width(16.dp))
}
@@ -73,14 +75,14 @@ private fun SettingsItemRow(
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color = contentColor
color = titleColor
)
if (!description.isNullOrEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = descriptionColor
)
}
}
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -50,9 +49,11 @@ import com.v2ray.ang.compose.AppDivider
import com.v2ray.ang.compose.AppTopBar
import com.v2ray.ang.compose.ReorderableListItem
import com.v2ray.ang.compose.SelectListDialog
import com.v2ray.ang.compose.SettingsListItem
import com.v2ray.ang.compose.colorConfigType
import com.v2ray.ang.compose.colorFabActive
import com.v2ray.ang.compose.verticalScrollbar
import com.v2ray.ang.dto.entities.RulesetItem
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
@@ -188,7 +189,6 @@ fun RoutingSettingScreen(
val rulesets by viewModel.rulesetsFlow.collectAsStateWithLifecycle()
val domainStrategy by domainStrategyState.collectAsState()
var showMenu by remember { mutableStateOf(false) }
var showDomainDialog by remember { mutableStateOf(false) }
var showPresetDialog by remember { mutableStateOf(false) }
val domainStrategies = stringArrayResource(R.array.routing_domain_strategy).toList()
@@ -254,24 +254,20 @@ fun RoutingSettingScreen(
.verticalScrollbar(lazyListState)
) {
item(key = "domain_strategy") {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable { showDomainDialog = true }
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Text(
stringResource(R.string.routing_settings_domain_strategy),
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(4.dp))
Text(
domainStrategy,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
AppDivider(modifier = Modifier.padding(horizontal = 16.dp))
SettingsListItem(
title = stringResource(R.string.routing_settings_domain_strategy),
entries = domainStrategies,
values = domainStrategies,
selectedValue = domainStrategy,
onSelected = { onDomainStrategySelected(it) }
)
}
item {
Text(
text = stringResource(R.string.routing_settings_rule_title),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(16.dp)
)
}
itemsIndexed(
@@ -283,76 +279,14 @@ fun RoutingSettingScreen(
scope = this,
isDragging = isDragging
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = ruleset.remarks ?: "",
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (ruleset.locked == true) {
Spacer(modifier = Modifier.width(4.dp))
Icon(
painter = painterResource(R.drawable.ic_lock_24dp),
contentDescription = "Locked",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
val domainIpInfo = (ruleset.domain ?: ruleset.ip ?: ruleset.process ?: ruleset.port)?.toString() ?: ""
if (domainIpInfo.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = domainIpInfo,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (!ruleset.outboundTag.isNullOrEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = ruleset.outboundTag,
style = MaterialTheme.typography.labelMedium,
color = colorConfigType
)
}
RoutingRulesetItem(
ruleset = ruleset,
onEdit = { onEditRule(index) },
onEnabledChange = { checked ->
val updated = ruleset.copy(enabled = checked)
viewModel.update(index, updated)
}
Column(
horizontalAlignment = Alignment.End,
modifier = Modifier.padding(start = 8.dp)
) {
IconButton(onClick = { onEditRule(index) }) {
Icon(
painter = painterResource(R.drawable.ic_edit_24dp),
contentDescription = "Edit"
)
}
Spacer(modifier = Modifier.height(4.dp))
Switch(
checked = ruleset.enabled ?: false,
onCheckedChange = { checked ->
val updated = ruleset.copy(enabled = checked)
viewModel.update(index, updated)
},
modifier = Modifier.scale(0.7f),
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onSecondary,
checkedTrackColor = colorFabActive
)
)
}
}
)
}
AppDivider(modifier = Modifier.padding(horizontal = 14.dp))
}
@@ -360,19 +294,6 @@ fun RoutingSettingScreen(
}
}
if (showDomainDialog) {
SelectListDialog(
title = stringResource(R.string.routing_settings_domain_strategy),
options = domainStrategies,
selectedOption = domainStrategy,
showRadio = true,
onSelected = { _, value ->
onDomainStrategySelected(value)
showDomainDialog = false
},
onDismiss = { showDomainDialog = false }
)
}
if (showPresetDialog) {
SelectListDialog(
@@ -386,3 +307,78 @@ fun RoutingSettingScreen(
)
}
}
@Composable
private fun RoutingRulesetItem(
ruleset: RulesetItem,
onEdit: () -> Unit,
onEnabledChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = ruleset.remarks ?: "",
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (ruleset.locked == true) {
Spacer(modifier = Modifier.width(4.dp))
Icon(
painter = painterResource(R.drawable.ic_lock_24dp),
contentDescription = "Locked",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
val domainIpInfo = (ruleset.domain ?: ruleset.ip ?: ruleset.process ?: ruleset.port)?.toString() ?: ""
if (domainIpInfo.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = domainIpInfo,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (!ruleset.outboundTag.isNullOrEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = ruleset.outboundTag,
style = MaterialTheme.typography.labelMedium,
color = colorConfigType
)
}
}
Column(
horizontalAlignment = Alignment.End,
modifier = Modifier.padding(start = 8.dp)
) {
IconButton(onClick = onEdit) {
Icon(
painter = painterResource(R.drawable.ic_edit_24dp),
contentDescription = "Edit"
)
}
Spacer(modifier = Modifier.height(4.dp))
Switch(
checked = ruleset.enabled ?: false,
onCheckedChange = onEnabledChange,
modifier = Modifier.scale(0.7f),
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onSecondary,
checkedTrackColor = colorFabActive
)
)
}
}
}
@@ -231,14 +231,12 @@ fun SettingsScreen(
title = stringResource(R.string.title_pref_ipv6_enabled),
summary = stringResource(R.string.summary_pref_ipv6_enabled),
checked = ipv6Enabled,
enabled = isVpn,
onCheckedChange = { ipv6Enabled = it }
)
SettingsSwitchItem(
title = stringResource(R.string.title_pref_prefer_ipv6),
summary = stringResource(R.string.summary_pref_prefer_ipv6),
checked = preferIpv6,
enabled = isVpn && ipv6Enabled,
onCheckedChange = { preferIpv6 = it }
)
SettingsSwitchItem(
@@ -265,7 +263,7 @@ fun SettingsScreen(
title = stringResource(R.string.title_pref_append_http_proxy),
summary = stringResource(R.string.summary_pref_append_http_proxy),
checked = appendHttpProxy,
enabled = isVpn && effectiveLocalProxy,
enabled = effectiveLocalProxy,
onCheckedChange = { appendHttpProxy = it }
)
SettingsListItem(
@@ -5,7 +5,6 @@ import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import androidx.activity.viewModels
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -50,6 +49,7 @@ import com.v2ray.ang.compose.AppDivider
import com.v2ray.ang.compose.AppTopBar
import com.v2ray.ang.compose.ConfirmDialog
import com.v2ray.ang.compose.SelectListDialog
import com.v2ray.ang.compose.SettingsListItem
import com.v2ray.ang.compose.verticalScrollbar
import com.v2ray.ang.dto.entities.AssetUrlCache
import com.v2ray.ang.dto.entities.AssetUrlItem
@@ -263,7 +263,6 @@ fun UserAssetScreen(
val trigger by refreshTrigger.collectAsState()
var showAddMenu by remember { mutableStateOf(false) }
var showGeoSourceDialog by remember { mutableStateOf(false) }
var deleteTargetGuid by remember { mutableStateOf<String?>(null) }
val listState = rememberLazyListState()
@@ -315,24 +314,13 @@ fun UserAssetScreen(
.verticalScrollbar(listState)
) {
item(key = "geo_source_$trigger") {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable { showGeoSourceDialog = true }
.padding(16.dp)
) {
Text(
text = stringResource(R.string.asset_geo_files_sources),
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = geoFilesSource,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2
)
}
SettingsListItem(
title = stringResource(R.string.asset_geo_files_sources),
entries = geoFilesSourcesList,
values = geoFilesSourcesList,
selectedValue = geoFilesSource,
onSelected = { onGeoSourceSelected(it) }
)
}
item {
Text(
@@ -348,24 +336,11 @@ fun UserAssetScreen(
onEdit = { onEditAsset(item.guid) },
onDeleteClick = { deleteTargetGuid = item.guid }
)
AppDivider()
AppDivider(modifier = Modifier.padding(horizontal = 14.dp))
}
}
}
if (showGeoSourceDialog) {
SelectListDialog(
title = stringResource(R.string.asset_geo_files_sources),
options = geoFilesSourcesList,
selectedOption = geoFilesSource,
showRadio = true,
onSelected = { _, value ->
onGeoSourceSelected(value)
showGeoSourceDialog = false
},
onDismiss = { showGeoSourceDialog = false }
)
}
if (deleteTargetGuid != null) {
val guid = deleteTargetGuid!!