mirror of
https://github.com/2dust/v2rayNG.git
synced 2026-10-29 06:31:54 +00:00
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:
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!!
|
||||
|
||||
Reference in new issue
Block a user