diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
index 899a0a4f7..dcd4b3301 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
@@ -65,8 +65,8 @@ import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.MainAppBar
-import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
@@ -197,7 +197,7 @@ fun ConnectionsScreen(
if (regionUnset && selectedDevice != "m") {
TitledCard(title = null) {
- SettingsItem(
+ ListItem(
leadingIcon = Icons.Rounded.Language,
text = stringResource(id = R.string.set_your_region),
) {
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailList.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailList.kt
index 07415b55d..81b327589 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailList.kt
@@ -120,6 +120,7 @@ fun NodeDetailList(
}
NodeDetailsSection(node)
+
NotesSection(node = node, onSaveNotes = onSaveNotes)
DeviceActions(
diff --git a/core/ui/detekt-baseline.xml b/core/ui/detekt-baseline.xml
index 259bc856c..aa0daf951 100644
--- a/core/ui/detekt-baseline.xml
+++ b/core/ui/detekt-baseline.xml
@@ -35,9 +35,6 @@
ModifierMissing:LoraSignalIndicator.kt$SnrAndRssi
ModifierMissing:PreferenceDivider.kt$PreferenceDivider
ModifierMissing:SecurityIcon.kt$SecurityIcon
- ModifierMissing:SettingsItem.kt$SettingsItem
- ModifierMissing:SettingsItem.kt$SettingsItemDetail
- ModifierMissing:SettingsItem.kt$SettingsItemSwitch
ModifierMissing:SharedContactDialog.kt$SharedContactDialog
ModifierMissing:SimpleAlertDialog.kt$SimpleAlertDialog
ModifierMissing:SlidingSelector.kt$OptionLabel
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ListItem.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ListItem.kt
new file mode 100644
index 000000000..b2d1d2f80
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/ListItem.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.core.ui.component
+
+import android.content.ClipData
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
+import androidx.compose.material.icons.rounded.Android
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.ClipEntry
+import androidx.compose.ui.platform.Clipboard
+import androidx.compose.ui.platform.LocalClipboard
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+import org.meshtastic.core.ui.theme.AppTheme
+
+/**
+ * A list item with an optional [leadingIcon], headline [text], optional [supportingText], and optional [trailingIcon].
+ */
+@Composable
+fun ListItem(
+ text: String,
+ modifier: Modifier = Modifier,
+ supportingText: String? = null,
+ textColor: Color = LocalContentColor.current,
+ supportingTextColor: Color = LocalContentColor.current,
+ copyable: Boolean = false,
+ enabled: Boolean = true,
+ leadingIcon: ImageVector? = null,
+ leadingIconTint: Color = LocalContentColor.current,
+ trailingIcon: ImageVector? = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ trailingIconTint: Color = LocalContentColor.current,
+ onClick: (() -> Unit)? = null,
+) {
+ val clipboard: Clipboard = LocalClipboard.current
+ val coroutineScope = rememberCoroutineScope()
+
+ BasicListItem(
+ text = text,
+ modifier = modifier,
+ textColor = textColor,
+ supportingText = supportingText,
+ supportingTextColor = supportingTextColor,
+ enabled = enabled,
+ leadingIcon = leadingIcon,
+ leadingIconTint = leadingIconTint,
+ trailingContent = trailingIcon.icon(trailingIconTint),
+ onClick = onClick,
+ onLongClick =
+ if (!supportingText.isNullOrBlank() && copyable) {
+ {
+ coroutineScope.launch {
+ clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", supportingText)))
+ }
+ }
+ } else {
+ null
+ },
+ )
+}
+
+/** A toggleable switch list item. */
+@Composable
+fun SwitchListItem(
+ checked: Boolean,
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ textColor: Color = LocalContentColor.current,
+ enabled: Boolean = true,
+ leadingIcon: ImageVector? = null,
+ leadingIconTint: Color = LocalContentColor.current,
+) {
+ BasicListItem(
+ text = text,
+ modifier = modifier,
+ textColor = textColor,
+ enabled = enabled,
+ leadingIcon = leadingIcon,
+ leadingIconTint = leadingIconTint,
+ trailingContent = { Switch(checked = checked, enabled = enabled, onCheckedChange = null) },
+ onClick = onClick,
+ )
+}
+
+/**
+ * The foundational list item. It supports a [leadingIcon] (optional), headline [text] and [supportingText] (optional),
+ * and a [trailingContent] composable (optional).
+ *
+ * This is a core component that should facilitate most list item use cases. Please carefully consider if modifying this
+ * is really necessary before doing so.
+ *
+ * @see [LinkedCoordinatesItem] for example usage
+ */
+@Suppress("UnusedParameter")
+@Composable
+fun BasicListItem(
+ text: String,
+ modifier: Modifier = Modifier,
+ textColor: Color = LocalContentColor.current,
+ supportingText: String? = null,
+ supportingTextColor: Color = LocalContentColor.current,
+ enabled: Boolean = true,
+ leadingIcon: ImageVector? = null,
+ leadingIconTint: Color = LocalContentColor.current,
+ trailingContent: @Composable (() -> Unit)? = null,
+ onClick: (() -> Unit)? = null,
+ onLongClick: (() -> Unit)? = null,
+) {
+ ListItem(
+ modifier =
+ if (onLongClick != null || onClick != null) {
+ modifier.combinedClickable(onLongClick = onLongClick, onClick = onClick ?: {})
+ } else {
+ modifier
+ },
+ colors = ListItemDefaults.colors(containerColor = Color.Transparent),
+ headlineContent = { Text(text = text, color = textColor) },
+ supportingContent = supportingText?.let { { Text(text = it, color = supportingTextColor) } },
+ leadingContent = leadingIcon.icon(leadingIconTint),
+ trailingContent = trailingContent,
+ )
+}
+
+@Composable
+fun ImageVector?.icon(tint: Color = LocalContentColor.current): @Composable (() -> Unit)? =
+ this?.let { { Icon(imageVector = it, contentDescription = null, modifier = Modifier.size(24.dp), tint = tint) } }
+
+@Preview(showBackground = true)
+@Composable
+private fun ListItemPreview() {
+ AppTheme { ListItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = true) {} }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun ListItemDisabledPreview() {
+ AppTheme { ListItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = false) {} }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SwitchListItemPreview() {
+ AppTheme { SwitchListItem(text = "Text", leadingIcon = Icons.Rounded.Android, checked = true, onClick = {}) }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun ListItemPreviewSupportingText() {
+ AppTheme {
+ ListItem(text = "Text 1", leadingIcon = Icons.Rounded.Android, supportingText = "Text2", trailingIcon = null)
+ }
+}
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
deleted file mode 100644
index 7460b2cc3..000000000
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SettingsItem.kt
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.meshtastic.core.ui.component
-
-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
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-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
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import org.meshtastic.core.ui.theme.AppTheme
-
-/**
- * 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,
- trailingContent: @Composable (() -> Unit)? = null,
- onClick: () -> Unit,
-) {
- val finalTrailingContent: @Composable (() -> Unit) =
- trailingContent ?: { Icons.AutoMirrored.Rounded.KeyboardArrowRight.Icon(LocalContentColor.current) }
-
- SettingsListItem(
- text = text,
- textColor = textColor,
- enabled = enabled,
- onClick = onClick,
- leadingContent = { leadingIcon.Icon(leadingIconTint) },
- supportingContent = { supportingText?.let { Text(text = it, style = MaterialTheme.typography.titleMedium) } },
- trailingContent = finalTrailingContent,
- )
-}
-
-/** A toggleable settings switch item. */
-@Composable
-fun SettingsItemSwitch(
- checked: Boolean,
- text: String,
- textColor: Color = LocalContentColor.current,
- enabled: Boolean = true,
- leadingIcon: ImageVector? = null,
- leadingIconTint: Color = LocalContentColor.current,
- onClick: () -> Unit,
-) {
- 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,
- enabled: Boolean = true,
- onClick: (() -> Unit)? = null,
-) {
- 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 = {},
- )
-}
-
-/** A settings detail item with composable content for the detail. */
-@Composable
-fun SettingsItemDetail(
- text: String,
- textColor: Color = LocalContentColor.current,
- icon: ImageVector? = null,
- iconTint: Color = LocalContentColor.current,
- enabled: Boolean = true,
- onClick: (() -> Unit)? = null,
- supportingContent: @Composable () -> Unit,
-) {
- SettingsListItem(
- text = text,
- textColor = textColor,
- enabled = enabled,
- onClick = onClick,
- leadingContent = { icon.Icon(iconTint) },
- supportingContent = supportingContent,
- 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) {
- Card(
- onClick = onClick,
- enabled = enabled,
- colors =
- CardDefaults.cardColors(containerColor = Color.Transparent, disabledContainerColor = Color.Transparent),
- content = listItemContent,
- )
- } else {
- Column(content = listItemContent)
- }
-}
-
-@Composable
-private fun ImageVector?.Icon(tint: Color = LocalContentColor.current) =
- this?.let { Icon(imageVector = it, contentDescription = null, modifier = Modifier.size(24.dp), tint = tint) }
-
-@Preview(showBackground = true)
-@Composable
-private fun SettingsItemPreview() {
- AppTheme { SettingsItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = true) {} }
-}
-
-@Preview(showBackground = true)
-@Composable
-private fun SettingsItemDisabledPreview() {
- AppTheme { SettingsItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = false) {} }
-}
-
-@Preview(showBackground = true)
-@Composable
-private fun SettingsItemSwitchPreview() {
- AppTheme { SettingsItemSwitch(text = "Text", leadingIcon = Icons.Rounded.Android, checked = true) {} }
-}
-
-@Preview(showBackground = true)
-@Composable
-private fun SettingsItemDetailPreview() {
- AppTheme { SettingsItemDetail(text = "Text 1", icon = Icons.Rounded.Android, supportingText = "Text2") }
-}
diff --git a/feature/node/detekt-baseline.xml b/feature/node/detekt-baseline.xml
index 921b8a5c5..c0e8546a6 100644
--- a/feature/node/detekt-baseline.xml
+++ b/feature/node/detekt-baseline.xml
@@ -4,12 +4,13 @@
ComposableParamOrder:ElevationInfo.kt$ElevationInfo
ComposableParamOrder:LastHeardInfo.kt$LastHeardInfo
- ComposableParamOrder:LinkedCoordinates.kt$LinkedCoordinates
ComposableParamOrder:NodeFilterTextField.kt$NodeFilterTextField
ComposableParamOrder:NodeItem.kt$NodeItem
ComposableParamOrder:SatelliteCountInfo.kt$SatelliteCountInfo
ComposableParamOrder:TracerouteButton.kt$TracerouteButton
ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons
+ MultipleEmitters:NodeDetailsSection.kt$MainNodeDetails
+ MultipleEmitters:RemoteDeviceActions.kt$RemoteDeviceActions
ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored
PreviewPublic:NodeItem.kt$NodeInfoPreview
PreviewPublic:NodeItem.kt$NodeInfoSimplePreview
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt
index 19f58552a..fdc5ea9e0 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt
@@ -35,8 +35,7 @@ import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.SettingsItem
-import org.meshtastic.core.ui.component.SettingsItemDetail
+import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
@@ -56,13 +55,13 @@ fun AdministrationSection(
modifier: Modifier = Modifier,
) {
TitledCard(stringResource(id = R.string.administration), modifier = modifier) {
- SettingsItem(
+ ListItem(
text = stringResource(id = R.string.request_metadata),
leadingIcon = Icons.Default.Memory,
- trailingContent = {},
+ trailingIcon = null,
onClick = { onAction(NodeDetailAction.TriggerServiceAction(ServiceAction.GetDeviceMetadata(node.num))) },
)
- SettingsItem(
+ ListItem(
text = stringResource(id = R.string.remote_admin),
leadingIcon = Icons.Default.Settings,
enabled = metricsState.isLocal || node.metadata != null,
@@ -81,10 +80,12 @@ fun AdministrationSection(
else -> Icons.Default.ForkLeft
}
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.firmware_edition),
- icon = icon,
+ leadingIcon = icon,
supportingText = it.name,
+ copyable = true,
+ trailingIcon = null,
)
}
firmwareVersion?.let { firmwareVersion ->
@@ -94,25 +95,31 @@ fun AdministrationSection(
val deviceVersion = DeviceVersion(firmwareVersion.substringBeforeLast("."))
val statusColor = deviceVersion.determineFirmwareStatusColor(latestStable, latestAlpha)
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.installed_firmware_version),
- icon = Icons.Default.Memory,
+ leadingIcon = Icons.Default.Memory,
supportingText = firmwareVersion.substringBeforeLast("."),
- iconTint = statusColor,
+ copyable = true,
+ leadingIconTint = statusColor,
+ trailingIcon = null,
)
HorizontalDivider()
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.latest_stable_firmware),
- icon = Icons.Default.Memory,
+ leadingIcon = Icons.Default.Memory,
supportingText = latestStable.id.substringBeforeLast(".").replace("v", ""),
- iconTint = MaterialTheme.colorScheme.StatusGreen,
+ copyable = true,
+ leadingIconTint = MaterialTheme.colorScheme.StatusGreen,
+ trailingIcon = null,
onClick = { onFirmwareSelect(latestStable) },
)
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.latest_alpha_firmware),
- icon = Icons.Default.Memory,
+ leadingIcon = Icons.Default.Memory,
supportingText = latestAlpha.id.substringBeforeLast(".").replace("v", ""),
- iconTint = MaterialTheme.colorScheme.StatusYellow,
+ copyable = true,
+ leadingIconTint = MaterialTheme.colorScheme.StatusYellow,
+ trailingIcon = null,
onClick = { onFirmwareSelect(latestAlpha) },
)
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
index de4a76204..3402f9e98 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt
@@ -35,8 +35,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.SettingsItem
-import org.meshtastic.core.ui.component.SettingsItemSwitch
+import org.meshtastic.core.ui.component.ListItem
+import org.meshtastic.core.ui.component.SwitchListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.node.model.NodeDetailAction
@@ -67,33 +67,33 @@ fun DeviceActions(
onConfirmRemove = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Remove(it))) },
)
TitledCard(title = stringResource(R.string.actions), modifier = modifier) {
- SettingsItem(
+ ListItem(
text = stringResource(id = R.string.share_contact),
leadingIcon = Icons.Rounded.QrCode2,
- trailingContent = {},
+ trailingIcon = null,
onClick = { onAction(NodeDetailAction.ShareContact) },
)
if (!isLocal) {
RemoteDeviceActions(node = node, lastTracerouteTime = lastTracerouteTime, onAction = onAction)
}
- SettingsItemSwitch(
+ SwitchListItem(
text = stringResource(R.string.favorite),
leadingIcon = if (node.isFavorite) Icons.Default.Star else Icons.Default.StarBorder,
leadingIconTint = if (node.isFavorite) Color.Yellow else LocalContentColor.current,
checked = node.isFavorite,
onClick = { displayFavoriteDialog = true },
)
- SettingsItemSwitch(
+ SwitchListItem(
text = stringResource(R.string.ignore),
leadingIcon =
if (node.isIgnored) Icons.AutoMirrored.Outlined.VolumeMute else Icons.AutoMirrored.Default.VolumeUp,
checked = node.isIgnored,
onClick = { displayIgnoreDialog = true },
)
- SettingsItem(
+ ListItem(
text = stringResource(id = R.string.remove),
leadingIcon = Icons.Rounded.Delete,
- trailingContent = {},
+ trailingIcon = null,
onClick = { displayRemoveDialog = true },
)
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt
index 8bb4d7fc6..319a9950e 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt
@@ -45,7 +45,7 @@ import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.SettingsItemDetail
+import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@@ -71,26 +71,28 @@ fun DeviceDetailsSection(state: MetricsState, modifier: Modifier = Modifier) {
Spacer(modifier = Modifier.height(16.dp))
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.hardware),
- icon = Icons.Default.Router,
+ leadingIcon = Icons.Default.Router,
supportingText = hwModelName,
+ copyable = true,
+ trailingIcon = null,
)
- SettingsItemDetail(
+ ListItem(
text =
if (isSupported) {
stringResource(R.string.supported)
} else {
stringResource(R.string.supported_by_community)
},
- icon =
+ leadingIcon =
if (isSupported) {
Icons.TwoTone.Verified
} else {
ImageVector.vectorResource(org.meshtastic.feature.node.R.drawable.unverified)
},
- supportingText = null,
- iconTint = if (isSupported) colorScheme.StatusGreen else colorScheme.StatusRed,
+ leadingIconTint = if (isSupported) colorScheme.StatusGreen else colorScheme.StatusRed,
+ trailingIcon = null,
)
}
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinates.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinates.kt
deleted file mode 100644
index 2cc69f50f..000000000
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinates.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.meshtastic.feature.node.component
-
-import android.content.ActivityNotFoundException
-import android.content.ClipData
-import android.content.Context
-import android.content.Intent
-import android.widget.Toast
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.ClipEntry
-import androidx.compose.ui.platform.Clipboard
-import androidx.compose.ui.platform.LocalClipboard
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.style.TextDecoration
-import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.tooling.preview.PreviewLightDark
-import androidx.core.net.toUri
-import kotlinx.coroutines.launch
-import org.meshtastic.core.model.util.GPSFormat
-import org.meshtastic.core.ui.theme.AppTheme
-import org.meshtastic.core.ui.theme.HyperlinkBlue
-import timber.log.Timber
-import java.net.URLEncoder
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun LinkedCoordinates(modifier: Modifier = Modifier, latitude: Double, longitude: Double, nodeName: String) {
- val context = LocalContext.current
- val clipboard: Clipboard = LocalClipboard.current
- val coroutineScope = rememberCoroutineScope()
- val style =
- SpanStyle(
- color = HyperlinkBlue,
- fontStyle = MaterialTheme.typography.titleLarge.fontStyle,
- textDecoration = TextDecoration.Underline,
- )
-
- val annotatedString = rememberAnnotatedString(latitude, longitude, nodeName, style)
-
- Text(
- modifier =
- modifier.combinedClickable(
- onClick = { handleClick(context, annotatedString) },
- onLongClick = {
- coroutineScope.launch {
- clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", annotatedString)))
- Timber.d("Copied to clipboard")
- }
- },
- ),
- text = annotatedString,
- style = MaterialTheme.typography.titleLarge,
- )
-}
-
-@Composable
-private fun rememberAnnotatedString(latitude: Double, longitude: Double, nodeName: String, style: SpanStyle) =
- buildAnnotatedString {
- pushStringAnnotation(
- tag = "gps",
- annotation =
- "geo:0,0?q=$latitude,$longitude&z=17&label=${
- URLEncoder.encode(nodeName, "utf-8")
- }",
- )
- withStyle(style = style) {
- val gpsString = GPSFormat.toDec(latitude, longitude)
- append(gpsString)
- }
- pop()
- }
-
-private fun handleClick(context: Context, annotatedString: AnnotatedString) {
- annotatedString.getStringAnnotations(tag = "gps", start = 0, end = annotatedString.length).firstOrNull()?.let {
- val uri = it.item.toUri()
- val intent = Intent(Intent.ACTION_VIEW, uri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
-
- try {
- if (intent.resolveActivity(context.packageManager) != null) {
- context.startActivity(intent)
- } else {
- Toast.makeText(context, "No application available to open this location!", Toast.LENGTH_LONG).show()
- }
- } catch (ex: ActivityNotFoundException) {
- Timber.d("Failed to open geo intent: $ex")
- }
- }
-}
-
-@PreviewLightDark
-@Composable
-private fun LinkedCoordinatesPreview() {
- AppTheme { LinkedCoordinates(latitude = 37.7749, longitude = -122.4194, nodeName = "Test Node Name") }
-}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt
new file mode 100644
index 000000000..3552646e1
--- /dev/null
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.feature.node.component
+
+import android.content.ActivityNotFoundException
+import android.content.ClipData
+import android.content.Intent
+import android.widget.Toast
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.ClipEntry
+import androidx.compose.ui.platform.Clipboard
+import androidx.compose.ui.platform.LocalClipboard
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.core.net.toUri
+import kotlinx.coroutines.launch
+import org.meshtastic.core.database.model.Node
+import org.meshtastic.core.model.util.GPSFormat
+import org.meshtastic.core.model.util.formatAgo
+import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.BasicListItem
+import org.meshtastic.core.ui.component.icon
+import org.meshtastic.core.ui.theme.AppTheme
+import timber.log.Timber
+import java.net.URLEncoder
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun LinkedCoordinatesItem(node: Node) {
+ val context = LocalContext.current
+ val clipboard: Clipboard = LocalClipboard.current
+ val coroutineScope = rememberCoroutineScope()
+
+ val ago = formatAgo(node.position.time)
+ val coordinates = GPSFormat.toDec(node.latitude, node.longitude)
+
+ BasicListItem(
+ text = stringResource(R.string.last_position_update),
+ leadingIcon = Icons.Default.LocationOn,
+ supportingText = "$ago • $coordinates",
+ trailingContent = Icons.AutoMirrored.Rounded.KeyboardArrowRight.icon(),
+ onClick = {
+ val label = URLEncoder.encode(node.user.longName, "utf-8")
+ val uri = "geo:0,0?q=${node.latitude},${node.longitude}&z=17&label=$label".toUri()
+ val intent = Intent(Intent.ACTION_VIEW, uri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
+
+ try {
+ if (intent.resolveActivity(context.packageManager) != null) {
+ context.startActivity(intent)
+ } else {
+ Toast.makeText(context, "No application available to open this location!", Toast.LENGTH_LONG).show()
+ }
+ } catch (ex: ActivityNotFoundException) {
+ Timber.d("Failed to open geo intent: $ex")
+ }
+ },
+ onLongClick = {
+ coroutineScope.launch { clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", coordinates))) }
+ },
+ )
+}
+
+@PreviewLightDark
+@Composable
+private fun LinkedCoordinatesPreview() {
+ AppTheme { LinkedCoordinatesItem(Node(0)) }
+}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/MetricsSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/MetricsSection.kt
index 1d31c1744..0c3e8a933 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/MetricsSection.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/MetricsSection.kt
@@ -25,7 +25,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.SettingsItem
+import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.node.model.LogsType
import org.meshtastic.feature.node.model.MetricsState
@@ -57,7 +57,7 @@ fun MetricsSection(
if (nonPositionLogs.isNotEmpty()) {
TitledCard(title = stringResource(id = R.string.logs), modifier = modifier) {
nonPositionLogs.forEach { type ->
- SettingsItem(text = stringResource(type.titleRes), leadingIcon = type.icon) {
+ ListItem(text = stringResource(type.titleRes), leadingIcon = type.icon) {
onAction(NodeDetailAction.Navigate(type.route))
}
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
index e17e6f9f7..450b18f28 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
@@ -45,7 +45,7 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.SettingsItemDetail
+import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
@@ -80,48 +80,59 @@ fun NodeDetailsSection(node: Node, modifier: Modifier = Modifier) {
@Composable
private fun MainNodeDetails(node: Node) {
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.long_name),
- icon = Icons.TwoTone.Person,
+ leadingIcon = Icons.TwoTone.Person,
supportingText = node.user.longName.ifEmpty { "???" },
+ copyable = true,
+ trailingIcon = null,
)
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.short_name),
- icon = Icons.Outlined.Person,
+ leadingIcon = Icons.Outlined.Person,
supportingText = node.user.shortName.ifEmpty { "???" },
+ copyable = true,
+ trailingIcon = null,
)
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.node_number),
- icon = Icons.Default.Numbers,
+ leadingIcon = Icons.Default.Numbers,
supportingText = node.num.toUInt().toString(),
+ copyable = true,
+ trailingIcon = null,
)
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.user_id),
- icon = Icons.Default.Person,
+ leadingIcon = Icons.Default.Person,
supportingText = node.user.id,
+ copyable = true,
+ trailingIcon = null,
)
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.role),
- icon = Icons.Default.Work,
+ leadingIcon = Icons.Default.Work,
supportingText = node.user.role.name,
+ trailingIcon = null,
)
if (node.isEffectivelyUnmessageable) {
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.unmonitored_or_infrastructure),
- icon = Icons.Outlined.NoCell,
- supportingText = null,
+ leadingIcon = Icons.Outlined.NoCell,
+ trailingIcon = null,
)
}
if (node.deviceMetrics.uptimeSeconds > 0) {
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.uptime),
- icon = Icons.Default.CheckCircle,
+ leadingIcon = Icons.Default.CheckCircle,
supportingText = formatUptime(node.deviceMetrics.uptimeSeconds),
+ trailingIcon = null,
)
}
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.node_sort_last_heard),
- icon = Icons.Default.History,
+ leadingIcon = Icons.Default.History,
supportingText = formatAgo(node.lastHeard),
+ trailingIcon = null,
)
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt
index 0e05963aa..9712dc5be 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/PositionSection.kt
@@ -17,26 +17,19 @@
package org.meshtastic.feature.node.component
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.SocialDistance
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.meshtastic.core.database.model.Node
-import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.SettingsItem
-import org.meshtastic.core.ui.component.SettingsItemDetail
+import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.node.model.LogsType
import org.meshtastic.feature.node.model.MetricsState
@@ -61,52 +54,39 @@ fun PositionSection(
// Current position coordinates (linked)
if (hasValidPosition) {
InlineMap(node = node, Modifier.fillMaxWidth().height(200.dp))
- SettingsItemDetail(
- text = stringResource(R.string.last_position_update),
- icon = Icons.Default.LocationOn,
- supportingContent = {
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(formatAgo(node.position.time), style = MaterialTheme.typography.titleLarge)
- LinkedCoordinates(
- latitude = node.latitude,
- longitude = node.longitude,
- nodeName = node.user.longName,
- )
- }
- },
- )
+
+ LinkedCoordinatesItem(node)
}
// Distance (if available)
if (distance != null && distance.isNotEmpty()) {
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.node_sort_distance),
- icon = Icons.Default.SocialDistance,
+ leadingIcon = Icons.Default.SocialDistance,
supportingText = distance,
+ copyable = true,
+ trailingIcon = null,
)
}
// Exchange position action
- SettingsItem(
+ ListItem(
text = stringResource(id = R.string.exchange_position),
leadingIcon = Icons.Default.LocationOn,
- trailingContent = {},
+ trailingIcon = null,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestPosition(node))) },
)
// Node Map log
if (availableLogs.contains(LogsType.NODE_MAP)) {
- SettingsItem(text = stringResource(LogsType.NODE_MAP.titleRes), leadingIcon = LogsType.NODE_MAP.icon) {
+ ListItem(text = stringResource(LogsType.NODE_MAP.titleRes), leadingIcon = LogsType.NODE_MAP.icon) {
onAction(NodeDetailAction.Navigate(LogsType.NODE_MAP.route))
}
}
// Positions Log
if (availableLogs.contains(LogsType.POSITIONS)) {
- SettingsItem(text = stringResource(LogsType.POSITIONS.titleRes), leadingIcon = LogsType.POSITIONS.icon) {
+ ListItem(text = stringResource(LogsType.POSITIONS.titleRes), leadingIcon = LogsType.POSITIONS.icon) {
onAction(NodeDetailAction.Navigate(LogsType.POSITIONS.route))
}
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/RemoteDeviceActions.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/RemoteDeviceActions.kt
index e1c00f6d6..dae9cc32a 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/RemoteDeviceActions.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/RemoteDeviceActions.kt
@@ -24,24 +24,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.SettingsItem
+import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.feature.node.model.NodeDetailAction
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
@Composable
internal fun RemoteDeviceActions(node: Node, lastTracerouteTime: Long?, onAction: (NodeDetailAction) -> Unit) {
if (!node.isEffectivelyUnmessageable) {
- SettingsItem(
+ ListItem(
text = stringResource(id = R.string.direct_message),
leadingIcon = Icons.AutoMirrored.TwoTone.Message,
- trailingContent = {},
+ trailingIcon = null,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.DirectMessage(node))) },
)
}
- SettingsItem(
+ ListItem(
text = stringResource(id = R.string.exchange_userinfo),
leadingIcon = Icons.Default.Person,
- trailingContent = {},
+ trailingIcon = null,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestUserInfo(node))) },
)
TracerouteButton(
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TracerouteButton.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TracerouteButton.kt
index 76efe380b..65f9d2702 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TracerouteButton.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/TracerouteButton.kt
@@ -35,7 +35,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.SettingsItem
+import org.meshtastic.core.ui.component.BasicListItem
import org.meshtastic.core.ui.theme.AppTheme
private const val COOL_DOWN_TIME_MS = 30000L
@@ -70,7 +70,7 @@ private fun TracerouteButton(text: String, progress: Float, onClick: () -> Unit)
val stroke = Stroke(width = with(LocalDensity.current) { 2.dp.toPx() }, cap = StrokeCap.Round)
- SettingsItem(
+ BasicListItem(
text = text,
enabled = !isCoolingDown,
leadingIcon = Icons.Default.Route,
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 e6eac39c7..8ded4d2ad 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,6 +34,7 @@ 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
@@ -52,6 +53,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
@@ -63,11 +65,10 @@ import kotlinx.coroutines.delay
import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
-import org.meshtastic.core.ui.component.SettingsItem
-import org.meshtastic.core.ui.component.SettingsItemDetail
-import org.meshtastic.core.ui.component.SettingsItemSwitch
+import org.meshtastic.core.ui.component.SwitchListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
@@ -230,11 +231,12 @@ fun SettingsScreen(
)
val context = LocalContext.current
+ val resources = LocalResources.current
TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
if (state.analyticsAvailable) {
val allowed by viewModel.analyticsAllowedFlow.collectAsStateWithLifecycle(false)
- SettingsItemSwitch(
+ SwitchListItem(
text = stringResource(R.string.analytics_okay),
checked = allowed,
leadingIcon = Icons.Default.BugReport,
@@ -255,7 +257,7 @@ fun SettingsScreen(
} else {
Toast.makeText(
context,
- context.getString(R.string.location_disabled),
+ resources.getString(R.string.location_disabled),
Toast.LENGTH_LONG,
)
.show()
@@ -269,14 +271,13 @@ fun SettingsScreen(
}
}
- SettingsItemSwitch(
+ SwitchListItem(
text = stringResource(R.string.provide_location_to_mesh),
leadingIcon = Icons.Rounded.LocationOn,
enabled = !isGpsDisabled,
checked = provideLocation,
- ) {
- settingsViewModel.setProvideLocation(!provideLocation)
- }
+ onClick = { settingsViewModel.setProvideLocation(!provideLocation) },
+ )
val settingsLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {}
@@ -284,15 +285,10 @@ fun SettingsScreen(
// On Android 12 and below, system app settings for language are not available. Use the in-app language
// picker for these devices.
val useInAppLangPicker = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
- SettingsItem(
+ ListItem(
text = stringResource(R.string.preferences_language),
leadingIcon = Icons.Rounded.Language,
- trailingContent =
- if (useInAppLangPicker) {
- null
- } else {
- {}
- },
+ trailingIcon = if (useInAppLangPicker) null else Icons.AutoMirrored.Rounded.KeyboardArrowRight,
) {
if (useInAppLangPicker) {
showLanguagePickerDialog = true
@@ -307,10 +303,10 @@ fun SettingsScreen(
}
}
- SettingsItem(
+ ListItem(
text = stringResource(R.string.theme),
leadingIcon = Icons.Rounded.FormatPaint,
- trailingContent = {},
+ trailingIcon = null,
) {
showThemePickerDialog = true
}
@@ -322,10 +318,10 @@ fun SettingsScreen(
it.data?.data?.let { uri -> settingsViewModel.saveDataCsv(uri) }
}
}
- SettingsItem(
+ ListItem(
text = stringResource(R.string.save_rangetest),
leadingIcon = Icons.Rounded.Output,
- trailingContent = {},
+ trailingIcon = null,
) {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
@@ -342,10 +338,10 @@ fun SettingsScreen(
it.data?.data?.let { uri -> settingsViewModel.saveDataCsv(uri) }
}
}
- SettingsItem(
+ ListItem(
text = stringResource(R.string.export_data_csv),
leadingIcon = Icons.Rounded.Output,
- trailingContent = {},
+ trailingIcon = null,
) {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
@@ -356,18 +352,18 @@ fun SettingsScreen(
exportDataLauncher.launch(intent)
}
- SettingsItem(
+ ListItem(
text = stringResource(R.string.intro_show),
leadingIcon = Icons.Rounded.WavingHand,
- trailingContent = {},
+ trailingIcon = null,
) {
settingsViewModel.showAppIntro()
}
- SettingsItem(
+ ListItem(
text = stringResource(R.string.system_settings),
leadingIcon = Icons.Rounded.AppSettingsAlt,
- trailingContent = null,
+ trailingIcon = null,
) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", context.packageName, null)
@@ -397,6 +393,7 @@ private fun AppVersionButton(
onUnlockExcludedModules: () -> Unit,
) {
val context = LocalContext.current
+ val resources = LocalResources.current
var clickCount by remember { mutableIntStateOf(0) }
LaunchedEffect(clickCount) {
@@ -406,23 +403,25 @@ private fun AppVersionButton(
}
}
- SettingsItemDetail(
+ ListItem(
text = stringResource(R.string.app_version),
- icon = Icons.Rounded.Memory,
+ leadingIcon = Icons.Rounded.Memory,
supportingText = appVersionName,
+ trailingIcon = null,
) {
clickCount = clickCount.inc().coerceIn(0, UNLOCK_CLICK_COUNT)
when {
clickCount == UNLOCKED_CLICK_COUNT && excludedModulesUnlocked -> {
clickCount = 0
- Toast.makeText(context, context.getString(R.string.modules_already_unlocked), Toast.LENGTH_LONG).show()
+ Toast.makeText(context, resources.getString(R.string.modules_already_unlocked), Toast.LENGTH_LONG)
+ .show()
}
clickCount == UNLOCK_CLICK_COUNT -> {
clickCount = 0
onUnlockExcludedModules()
- Toast.makeText(context, context.getString(R.string.modules_unlocked), Toast.LENGTH_LONG).show()
+ Toast.makeText(context, resources.getString(R.string.modules_unlocked), Toast.LENGTH_LONG).show()
}
}
}
@@ -448,13 +447,13 @@ private fun LanguagePickerDialog(onDismiss: () -> Unit) {
@Composable
private fun ThemePickerDialog(onClickTheme: (Int) -> Unit, onDismiss: () -> Unit) {
- val context = LocalContext.current
+ val resources = LocalResources.current
val themeMap = remember {
mapOf(
- context.getString(R.string.dynamic) to MODE_DYNAMIC,
- context.getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO,
- context.getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES,
- context.getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
+ resources.getString(R.string.dynamic) to MODE_DYNAMIC,
+ resources.getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO,
+ resources.getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES,
+ resources.getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
)
.mapValues { (_, value) -> { onClickTheme(value) } }
}
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 e2c747a39..d93d2cf19 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
@@ -45,7 +45,7 @@ import androidx.compose.ui.unit.dp
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.SettingsItem
+import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@@ -81,9 +81,7 @@ fun RadioConfigItemList(
ManagedMessage()
}
ConfigRoute.radioConfigRoutes.forEach {
- SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) {
- onRouteClick(it)
- }
+ ListItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { onRouteClick(it) }
}
}
@@ -92,9 +90,7 @@ fun RadioConfigItemList(
ManagedMessage()
}
ConfigRoute.deviceConfigRoutes(state.metadata).forEach {
- SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) {
- onRouteClick(it)
- }
+ ListItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { onRouteClick(it) }
}
}
@@ -104,9 +100,7 @@ fun RadioConfigItemList(
}
modules.forEach {
- SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) {
- onRouteClick(it)
- }
+ ListItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { onRouteClick(it) }
}
}
}
@@ -117,13 +111,13 @@ fun RadioConfigItemList(
ManagedMessage()
}
- SettingsItem(
+ ListItem(
text = stringResource(R.string.import_configuration),
leadingIcon = Icons.Default.Download,
enabled = enabled,
onClick = onImport,
)
- SettingsItem(
+ ListItem(
text = stringResource(R.string.export_configuration),
leadingIcon = Icons.Default.Upload,
enabled = enabled,
@@ -143,11 +137,11 @@ fun RadioConfigItemList(
)
}
- SettingsItem(
+ ListItem(
enabled = enabled,
text = stringResource(route.title),
leadingIcon = route.icon,
- trailingContent = {},
+ trailingIcon = null,
) {
showDialog = true
}
@@ -159,14 +153,14 @@ fun RadioConfigItemList(
ManagedMessage()
}
- SettingsItem(
+ ListItem(
text = stringResource(R.string.clean_node_database_title),
leadingIcon = Icons.Rounded.CleaningServices,
enabled = enabled,
onClick = { onNavigate(SettingsRoutes.CleanNodeDb) },
)
- SettingsItem(
+ ListItem(
text = stringResource(R.string.debug_panel),
leadingIcon = Icons.Rounded.BugReport,
enabled = enabled,