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 81b327589..f16ac8c35 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
@@ -32,15 +32,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.model.Node
-import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SharedContactDialog
-import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.feature.node.component.AdministrationSection
@@ -116,7 +113,7 @@ fun NodeDetailList(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
if (metricsState.deviceHardware != null) {
- TitledCard(title = stringResource(R.string.device)) { DeviceDetailsSection(metricsState) }
+ DeviceDetailsSection(metricsState)
}
NodeDetailsSection(node)
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/InsetDivider.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/InsetDivider.kt
new file mode 100644
index 000000000..97ace57c1
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/InsetDivider.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.padding
+import androidx.compose.material3.DividerDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun InsetDivider(
+ modifier: Modifier = Modifier,
+ inset: Dp = 16.dp,
+ thickness: Dp = DividerDefaults.Thickness,
+ color: Color = DividerDefaults.color,
+) {
+ InsetDivider(modifier = modifier, startInset = inset, endInset = inset, thickness = thickness, color = color)
+}
+
+@Composable
+fun InsetDivider(
+ modifier: Modifier = Modifier,
+ startInset: Dp = 0.dp,
+ endInset: Dp = 0.dp,
+ thickness: Dp = DividerDefaults.Thickness,
+ color: Color = DividerDefaults.color,
+) {
+ HorizontalDivider(
+ modifier = modifier.padding(start = startInset, end = endInset),
+ thickness = thickness,
+ color = color,
+ )
+}
diff --git a/feature/node/detekt-baseline.xml b/feature/node/detekt-baseline.xml
index c0e8546a6..2238abd36 100644
--- a/feature/node/detekt-baseline.xml
+++ b/feature/node/detekt-baseline.xml
@@ -8,6 +8,7 @@
ComposableParamOrder:NodeItem.kt$NodeItem
ComposableParamOrder:SatelliteCountInfo.kt$SatelliteCountInfo
ComposableParamOrder:TracerouteButton.kt$TracerouteButton
+ LongMethod:NodeDetailsSection.kt$@Composable private fun MainNodeDetails(node: Node)
ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons
MultipleEmitters:NodeDetailsSection.kt$MainNodeDetails
MultipleEmitters:RemoteDeviceActions.kt$RemoteDeviceActions
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 fdc5ea9e0..bc81a7e6e 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
@@ -22,7 +22,6 @@ import androidx.compose.material.icons.filled.ForkLeft
import androidx.compose.material.icons.filled.Icecream
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -35,6 +34,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.InsetDivider
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
@@ -61,6 +61,9 @@ fun AdministrationSection(
trailingIcon = null,
onClick = { onAction(NodeDetailAction.TriggerServiceAction(ServiceAction.GetDeviceMetadata(node.num))) },
)
+
+ InsetDivider()
+
ListItem(
text = stringResource(id = R.string.remote_admin),
leadingIcon = Icons.Default.Settings,
@@ -95,6 +98,8 @@ fun AdministrationSection(
val deviceVersion = DeviceVersion(firmwareVersion.substringBeforeLast("."))
val statusColor = deviceVersion.determineFirmwareStatusColor(latestStable, latestAlpha)
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.installed_firmware_version),
leadingIcon = Icons.Default.Memory,
@@ -103,7 +108,9 @@ fun AdministrationSection(
leadingIconTint = statusColor,
trailingIcon = null,
)
- HorizontalDivider()
+
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.latest_stable_firmware),
leadingIcon = Icons.Default.Memory,
@@ -113,6 +120,9 @@ fun AdministrationSection(
trailingIcon = null,
onClick = { onFirmwareSelect(latestStable) },
)
+
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.latest_alpha_firmware),
leadingIcon = Icons.Default.Memory,
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 3402f9e98..acc3aec79 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,6 +35,7 @@ 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.InsetDivider
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.SwitchListItem
import org.meshtastic.core.ui.component.TitledCard
@@ -74,8 +75,12 @@ fun DeviceActions(
onClick = { onAction(NodeDetailAction.ShareContact) },
)
if (!isLocal) {
+ InsetDivider()
RemoteDeviceActions(node = node, lastTracerouteTime = lastTracerouteTime, onAction = onAction)
}
+
+ InsetDivider()
+
SwitchListItem(
text = stringResource(R.string.favorite),
leadingIcon = if (node.isFavorite) Icons.Default.Star else Icons.Default.StarBorder,
@@ -83,6 +88,9 @@ fun DeviceActions(
checked = node.isFavorite,
onClick = { displayFavoriteDialog = true },
)
+
+ InsetDivider()
+
SwitchListItem(
text = stringResource(R.string.ignore),
leadingIcon =
@@ -90,6 +98,9 @@ fun DeviceActions(
checked = node.isIgnored,
onClick = { displayIgnoreDialog = true },
)
+
+ InsetDivider()
+
ListItem(
text = stringResource(id = R.string.remove),
leadingIcon = Icons.Rounded.Delete,
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 319a9950e..29efc1320 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,6 +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.InsetDivider
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
@@ -58,6 +59,8 @@ fun DeviceDetailsSection(state: MetricsState, modifier: Modifier = Modifier) {
val hwModelName = deviceHardware.displayName
val isSupported = deviceHardware.activelySupported
TitledCard(stringResource(R.string.device), modifier = modifier) {
+ Spacer(modifier = Modifier.height(16.dp))
+
Box(
modifier =
Modifier.align(Alignment.CenterHorizontally)
@@ -71,6 +74,8 @@ fun DeviceDetailsSection(state: MetricsState, modifier: Modifier = Modifier) {
Spacer(modifier = Modifier.height(16.dp))
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.hardware),
leadingIcon = Icons.Default.Router,
@@ -78,6 +83,9 @@ fun DeviceDetailsSection(state: MetricsState, modifier: Modifier = Modifier) {
copyable = true,
trailingIcon = null,
)
+
+ InsetDivider()
+
ListItem(
text =
if (isSupported) {
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 450b18f28..ea4e4d0e7 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,6 +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.InsetDivider
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
@@ -87,6 +88,9 @@ private fun MainNodeDetails(node: Node) {
copyable = true,
trailingIcon = null,
)
+
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.short_name),
leadingIcon = Icons.Outlined.Person,
@@ -94,6 +98,9 @@ private fun MainNodeDetails(node: Node) {
copyable = true,
trailingIcon = null,
)
+
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.node_number),
leadingIcon = Icons.Default.Numbers,
@@ -101,6 +108,9 @@ private fun MainNodeDetails(node: Node) {
copyable = true,
trailingIcon = null,
)
+
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.user_id),
leadingIcon = Icons.Default.Person,
@@ -108,13 +118,19 @@ private fun MainNodeDetails(node: Node) {
copyable = true,
trailingIcon = null,
)
+
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.role),
leadingIcon = Icons.Default.Work,
supportingText = node.user.role.name,
trailingIcon = null,
)
+
if (node.isEffectivelyUnmessageable) {
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.unmonitored_or_infrastructure),
leadingIcon = Icons.Outlined.NoCell,
@@ -122,6 +138,8 @@ private fun MainNodeDetails(node: Node) {
)
}
if (node.deviceMetrics.uptimeSeconds > 0) {
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.uptime),
leadingIcon = Icons.Default.CheckCircle,
@@ -129,6 +147,9 @@ private fun MainNodeDetails(node: Node) {
trailingIcon = null,
)
}
+
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.node_sort_last_heard),
leadingIcon = Icons.Default.History,
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 9712dc5be..985af9380 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
@@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.InsetDivider
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.node.model.LogsType
@@ -60,6 +61,8 @@ fun PositionSection(
// Distance (if available)
if (distance != null && distance.isNotEmpty()) {
+ InsetDivider()
+
ListItem(
text = stringResource(R.string.node_sort_distance),
leadingIcon = Icons.Default.SocialDistance,
@@ -69,6 +72,8 @@ fun PositionSection(
)
}
+ InsetDivider()
+
// Exchange position action
ListItem(
text = stringResource(id = R.string.exchange_position),
@@ -79,6 +84,8 @@ fun PositionSection(
// Node Map log
if (availableLogs.contains(LogsType.NODE_MAP)) {
+ InsetDivider()
+
ListItem(text = stringResource(LogsType.NODE_MAP.titleRes), leadingIcon = LogsType.NODE_MAP.icon) {
onAction(NodeDetailAction.Navigate(LogsType.NODE_MAP.route))
}
@@ -86,6 +93,8 @@ fun PositionSection(
// Positions Log
if (availableLogs.contains(LogsType.POSITIONS)) {
+ InsetDivider()
+
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 dae9cc32a..ed40dd88e 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,6 +24,7 @@ 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.InsetDivider
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.feature.node.model.NodeDetailAction
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
@@ -37,13 +38,19 @@ internal fun RemoteDeviceActions(node: Node, lastTracerouteTime: Long?, onAction
trailingIcon = null,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.DirectMessage(node))) },
)
+
+ InsetDivider()
}
+
ListItem(
text = stringResource(id = R.string.exchange_userinfo),
leadingIcon = Icons.Default.Person,
trailingIcon = null,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestUserInfo(node))) },
)
+
+ InsetDivider()
+
TracerouteButton(
lastTracerouteTime = lastTracerouteTime,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.TraceRoute(node))) },