From 7213f9fa9fce13d0ecfbe1e4370ff2b8c161fc7d Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:05:42 +0800 Subject: [PATCH] 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. --- .../com/v2ray/ang/compose/SettingsItem.kt | 12 +- .../v2ray/ang/ui/RoutingSettingActivity.kt | 200 +++++++++--------- .../java/com/v2ray/ang/ui/SettingsActivity.kt | 4 +- .../com/v2ray/ang/ui/UserAssetActivity.kt | 43 +--- 4 files changed, 115 insertions(+), 144 deletions(-) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/compose/SettingsItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/compose/SettingsItem.kt index 143ea61c5..7809ce536 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/compose/SettingsItem.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/compose/SettingsItem.kt @@ -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 ) } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt index aab0b8278..75abb600e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt @@ -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 + ) + ) + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt index 90040ecd5..d818935ac 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt @@ -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( diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt index 4be681cb1..493aa4bc7 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt @@ -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(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!!