diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailScreen.kt index 83e0a1f04..27f87acf4 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailScreen.kt @@ -509,7 +509,7 @@ private fun AdministrationSection( SettingsItem( text = stringResource(id = R.string.request_metadata), leadingIcon = Icons.Default.Memory, - trailingIcon = null, + trailingContent = {}, onClick = { onAction(NodeDetailAction.TriggerServiceAction(ServiceAction.GetDeviceMetadata(node.num))) }, ) SettingsItem( @@ -534,7 +534,7 @@ private fun AdministrationSection( SettingsItemDetail( text = stringResource(R.string.firmware_edition), icon = icon, - trailingText = it.name, + supportingText = it.name, ) } } @@ -548,21 +548,21 @@ private fun AdministrationSection( SettingsItemDetail( text = stringResource(R.string.installed_firmware_version), icon = Icons.Default.Memory, - trailingText = firmwareVersion.substringBeforeLast("."), + supportingText = firmwareVersion.substringBeforeLast("."), iconTint = statusColor, ) HorizontalDivider() SettingsItemDetail( text = stringResource(R.string.latest_stable_firmware), icon = Icons.Default.Memory, - trailingText = latestStable.id.substringBeforeLast(".").replace("v", ""), + supportingText = latestStable.id.substringBeforeLast(".").replace("v", ""), iconTint = colorScheme.StatusGreen, onClick = { onFirmwareSelected(latestStable) }, ) SettingsItemDetail( text = stringResource(R.string.latest_alpha_firmware), icon = Icons.Default.Memory, - trailingText = latestAlpha.id.substringBeforeLast(".").replace("v", ""), + supportingText = latestAlpha.id.substringBeforeLast(".").replace("v", ""), iconTint = colorScheme.StatusYellow, onClick = { onFirmwareSelected(latestAlpha) }, ) @@ -662,7 +662,7 @@ private fun DeviceActions( SettingsItem( text = stringResource(id = R.string.share_contact), leadingIcon = Icons.Rounded.QrCode2, - trailingIcon = null, + trailingContent = {}, onClick = { onAction(NodeDetailAction.ShareContact) }, ) if (!isLocal) { @@ -685,7 +685,7 @@ private fun DeviceActions( SettingsItem( text = stringResource(id = R.string.remove), leadingIcon = Icons.Rounded.Delete, - trailingIcon = null, + trailingContent = {}, onClick = { displayRemoveDialog = true }, ) } @@ -697,20 +697,20 @@ private fun RemoteDeviceActions(node: Node, lastTracerouteTime: Long?, onAction: SettingsItem( text = stringResource(id = R.string.direct_message), leadingIcon = Icons.AutoMirrored.TwoTone.Message, - trailingIcon = null, + trailingContent = {}, onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.DirectMessage(node))) }, ) } SettingsItem( text = stringResource(id = R.string.exchange_position), leadingIcon = Icons.Default.LocationOn, - trailingIcon = null, + trailingContent = {}, onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestPosition(node))) }, ) SettingsItem( text = stringResource(id = R.string.exchange_userinfo), leadingIcon = Icons.Default.Person, - trailingIcon = null, + trailingContent = {}, onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestUserInfo(node))) }, ) TracerouteButton( @@ -741,7 +741,7 @@ private fun ColumnScope.DeviceDetailsContent(state: MetricsState) { SettingsItemDetail( text = stringResource(R.string.hardware), icon = Icons.Default.Router, - trailingText = hwModelName, + supportingText = hwModelName, ) SettingsItemDetail( text = @@ -756,7 +756,7 @@ private fun ColumnScope.DeviceDetailsContent(state: MetricsState) { } else { ImageVector.vectorResource(com.geeksville.mesh.R.drawable.unverified) }, - trailingText = "", + supportingText = null, iconTint = if (isSupported) colorScheme.StatusGreen else colorScheme.StatusRed, ) } @@ -817,55 +817,60 @@ private fun MainNodeDetails(node: Node, ourNode: Node?, displayUnits: ConfigProt SettingsItemDetail( text = stringResource(R.string.long_name), icon = Icons.TwoTone.Person, - trailingText = node.user.longName.ifEmpty { "???" }, + supportingText = node.user.longName.ifEmpty { "???" }, ) SettingsItemDetail( text = stringResource(R.string.short_name), icon = Icons.Outlined.Person, - trailingText = node.user.shortName.ifEmpty { "???" }, + supportingText = node.user.shortName.ifEmpty { "???" }, ) SettingsItemDetail( text = stringResource(R.string.node_number), icon = Icons.Default.Numbers, - trailingText = node.num.toUInt().toString(), + supportingText = node.num.toUInt().toString(), ) SettingsItemDetail( text = stringResource(R.string.user_id), icon = Icons.Default.Person, - trailingText = node.user.id, + supportingText = node.user.id, ) SettingsItemDetail( text = stringResource(R.string.role), icon = Icons.Default.Work, - trailingText = node.user.role.name, + supportingText = node.user.role.name, ) if (node.isEffectivelyUnmessageable) { - SettingsItemDetail(text = stringResource(R.string.unmonitored_or_infrastructure), icon = Icons.Outlined.NoCell) + SettingsItemDetail( + text = stringResource(R.string.unmonitored_or_infrastructure), + icon = Icons.Outlined.NoCell, + supportingText = null, + ) } if (node.deviceMetrics.uptimeSeconds > 0) { SettingsItemDetail( text = stringResource(R.string.uptime), icon = Icons.Default.CheckCircle, - trailingText = formatUptime(node.deviceMetrics.uptimeSeconds), + supportingText = formatUptime(node.deviceMetrics.uptimeSeconds), ) } SettingsItemDetail( text = stringResource(R.string.node_sort_last_heard), icon = Icons.Default.History, - trailingText = formatAgo(node.lastHeard), + supportingText = formatAgo(node.lastHeard), ) val distance = ourNode?.distance(node)?.takeIf { it > 0 }?.toDistanceString(displayUnits) if (distance != null && distance.isNotEmpty()) { SettingsItemDetail( text = stringResource(R.string.node_sort_distance), icon = Icons.Default.SocialDistance, - trailingText = distance, + supportingText = distance, ) } + SettingsItemDetail( text = stringResource(R.string.last_position_update), icon = Icons.Default.LocationOn, - trailingText = formatAgo(node.position.time), + supportingText = formatAgo(node.position.time), ) } diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SettingsItem.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SettingsItem.kt index e9db8e4ff..b5a003954 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SettingsItem.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SettingsItem.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight import androidx.compose.material.icons.rounded.Android @@ -30,6 +31,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,50 +42,35 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.meshtastic.core.ui.theme.AppTheme -/** A clickable settings button item. */ +/** + * A clickable settings item with optional supporting text and trailing content. Defaults to a trailing arrow icon if no + * custom trailing content is provided. + */ @Composable fun SettingsItem( text: String, + supportingText: String? = null, textColor: Color = LocalContentColor.current, enabled: Boolean = true, leadingIcon: ImageVector? = null, leadingIconTint: Color = LocalContentColor.current, - trailingIcon: ImageVector? = Icons.AutoMirrored.Rounded.KeyboardArrowRight, - trailingIconTint: Color = LocalContentColor.current, + trailingContent: @Composable (() -> Unit)? = null, onClick: () -> Unit, ) { - SettingsItem( + val finalTrailingContent: @Composable (() -> Unit) = + trailingContent ?: { Icons.AutoMirrored.Rounded.KeyboardArrowRight.Icon(LocalContentColor.current) } + + SettingsListItem( text = text, textColor = textColor, enabled = enabled, - leadingIcon = leadingIcon, - leadingIconTint = leadingIconTint, - trailingContent = { trailingIcon.Icon(trailingIconTint) }, onClick = onClick, + leadingContent = { leadingIcon.Icon(leadingIconTint) }, + supportingContent = { supportingText?.let { Text(text = it, style = MaterialTheme.typography.titleMedium) } }, + trailingContent = finalTrailingContent, ) } -/** A clickable settings button item. */ -@Composable -fun SettingsItem( - text: String, - textColor: Color = LocalContentColor.current, - enabled: Boolean = true, - leadingIcon: ImageVector? = null, - leadingIconTint: Color = LocalContentColor.current, - trailingContent: @Composable (() -> Unit), - onClick: () -> Unit, -) { - ClickableWrapper(enabled = enabled, onClick = onClick) { - Content( - leading = { leadingIcon.Icon(leadingIconTint) }, - text = text, - textColor = textColor, - trailing = trailingContent, - ) - } -} - /** A toggleable settings switch item. */ @Composable fun SettingsItemSwitch( @@ -95,67 +82,84 @@ fun SettingsItemSwitch( leadingIconTint: Color = LocalContentColor.current, onClick: () -> Unit, ) { - ClickableWrapper(enabled = enabled, onClick = onClick) { - Content( - leading = { leadingIcon.Icon(leadingIconTint) }, - text = text, - textColor = textColor, - trailing = { Switch(checked = checked, enabled = enabled, onCheckedChange = null) }, - ) - } + SettingsListItem( + text = text, + textColor = textColor, + enabled = enabled, + onClick = onClick, + leadingContent = { leadingIcon.Icon(leadingIconTint) }, + trailingContent = { Switch(checked = checked, enabled = enabled, onCheckedChange = null) }, + ) } /** A settings detail item. */ @Composable fun SettingsItemDetail( text: String, + supportingText: String?, textColor: Color = LocalContentColor.current, icon: ImageVector? = null, iconTint: Color = LocalContentColor.current, - trailingText: String? = null, enabled: Boolean = true, onClick: (() -> Unit)? = null, ) { - val content: @Composable ColumnScope.() -> Unit = { - Content( - leading = { icon.Icon(iconTint) }, - text = text, - textColor = textColor, - trailing = { trailingText?.let { Text(text = it) } }, + SettingsListItem( + text = text, + textColor = textColor, + enabled = enabled, + onClick = onClick, + leadingContent = { icon.Icon(iconTint) }, + supportingContent = { + supportingText?.let { + Text( + text = it, + style = MaterialTheme.typography.titleLarge, + color = textColor, // Detail style explicitly sets color + ) + } + }, + trailingContent = {}, + ) +} + +/** + * Base composable for all settings screen list items. It handles the Material3 [ListItem] structure and the conditional + * click wrapper. + */ +@Composable +private fun SettingsListItem( + text: String, + textColor: Color, + enabled: Boolean, + onClick: (() -> Unit)?, + leadingContent: @Composable (() -> Unit)? = null, + supportingContent: @Composable (() -> Unit)? = null, + trailingContent: @Composable (() -> Unit)? = null, +) { + val listItemContent: @Composable ColumnScope.() -> Unit = { + ListItem( + modifier = Modifier.padding(horizontal = 8.dp), + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + headlineContent = { Text(text = text, color = textColor) }, + supportingContent = { SelectionContainer { supportingContent?.invoke() } }, + leadingContent = leadingContent, + trailingContent = trailingContent, ) } if (onClick != null) { - ClickableWrapper(enabled = enabled, onClick = onClick, content = content) + Card( + onClick = onClick, + enabled = enabled, + colors = + CardDefaults.cardColors(containerColor = Color.Transparent, disabledContainerColor = Color.Transparent), + content = listItemContent, + ) } else { - Column(content = content) + Column(content = listItemContent) } } -/** A clickable Card wrapper used for all clickable settings items. */ -@Composable -private fun ClickableWrapper(enabled: Boolean, onClick: () -> Unit, content: @Composable ColumnScope.() -> Unit) { - Card( - onClick = onClick, - enabled = enabled, - colors = - CardDefaults.cardColors(containerColor = Color.Transparent, disabledContainerColor = Color.Transparent), - content = content, - ) -} - -/** The row content to display for a settings item. */ -@Composable -private fun Content(leading: @Composable () -> Unit, text: String, textColor: Color, trailing: @Composable () -> Unit) { - ListItem( - modifier = Modifier.padding(horizontal = 8.dp), - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - headlineContent = { Text(text = text, color = textColor) }, - leadingContent = { leading() }, - trailingContent = { trailing() }, - ) -} - @Composable private fun ImageVector?.Icon(tint: Color = LocalContentColor.current) = this?.let { Icon(imageVector = it, contentDescription = null, modifier = Modifier.size(24.dp), tint = tint) } @@ -181,5 +185,5 @@ private fun SettingsItemSwitchPreview() { @Preview(showBackground = true) @Composable private fun SettingsItemDetailPreview() { - AppTheme { SettingsItemDetail(text = "Text 1", icon = Icons.Rounded.Android, trailingText = "Text2") } + AppTheme { SettingsItemDetail(text = "Text 1", icon = Icons.Rounded.Android, supportingText = "Text2") } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt index 75d30c5a9..003713670 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt @@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.rounded.AppSettingsAlt import androidx.compose.material.icons.rounded.FormatPaint @@ -288,7 +287,12 @@ fun SettingsScreen( SettingsItem( text = stringResource(R.string.preferences_language), leadingIcon = Icons.Rounded.Language, - trailingIcon = if (useInAppLangPicker) null else Icons.AutoMirrored.Rounded.KeyboardArrowRight, + trailingContent = + if (useInAppLangPicker) { + null + } else { + {} + }, ) { if (useInAppLangPicker) { showLanguagePickerDialog = true @@ -306,7 +310,7 @@ fun SettingsScreen( SettingsItem( text = stringResource(R.string.theme), leadingIcon = Icons.Rounded.FormatPaint, - trailingIcon = null, + trailingContent = {}, ) { showThemePickerDialog = true } @@ -321,7 +325,7 @@ fun SettingsScreen( SettingsItem( text = stringResource(R.string.save_rangetest), leadingIcon = Icons.Rounded.Output, - trailingIcon = null, + trailingContent = {}, ) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { @@ -341,7 +345,7 @@ fun SettingsScreen( SettingsItem( text = stringResource(R.string.export_data_csv), leadingIcon = Icons.Rounded.Output, - trailingIcon = null, + trailingContent = {}, ) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { @@ -355,7 +359,7 @@ fun SettingsScreen( SettingsItem( text = stringResource(R.string.intro_show), leadingIcon = Icons.Rounded.WavingHand, - trailingIcon = null, + trailingContent = {}, ) { settingsViewModel.showAppIntro() } @@ -363,7 +367,7 @@ fun SettingsScreen( SettingsItem( text = stringResource(R.string.system_settings), leadingIcon = Icons.Rounded.AppSettingsAlt, - trailingIcon = Icons.AutoMirrored.Rounded.KeyboardArrowRight, + trailingContent = null, ) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intent.data = Uri.fromParts("package", context.packageName, null) @@ -405,7 +409,7 @@ private fun AppVersionButton( SettingsItemDetail( text = stringResource(R.string.app_version), icon = Icons.Rounded.Memory, - trailingText = appVersionName, + supportingText = appVersionName, ) { clickCount = clickCount.inc().coerceIn(0, UNLOCK_CLICK_COUNT) diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt index b5b6361cc..e2c747a39 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt @@ -147,7 +147,7 @@ fun RadioConfigItemList( enabled = enabled, text = stringResource(route.title), leadingIcon = route.icon, - trailingIcon = null, + trailingContent = {}, ) { showDialog = true }