mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(ui): Refactor SettingsItem component (#3364)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
4d7ad96a09
commit
3f923ae5c6
4 changed files with 114 additions and 101 deletions
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ fun RadioConfigItemList(
|
|||
enabled = enabled,
|
||||
text = stringResource(route.title),
|
||||
leadingIcon = route.icon,
|
||||
trailingIcon = null,
|
||||
trailingContent = {},
|
||||
) {
|
||||
showDialog = true
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue