From 73b37c17dccce48b9114dbd31a0e1f03bb67eac6 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Tue, 14 Oct 2025 22:06:45 -0400 Subject: [PATCH] Add dividers to node details (#3466) --- .../geeksville/mesh/ui/node/NodeDetailList.kt | 5 +- .../core/ui/component/InsetDivider.kt | 52 +++++++++++++++++++ feature/node/detekt-baseline.xml | 1 + .../node/component/AdministrationSection.kt | 14 ++++- .../feature/node/component/DeviceActions.kt | 11 ++++ .../node/component/DeviceDetailsSection.kt | 8 +++ .../node/component/NodeDetailsSection.kt | 21 ++++++++ .../feature/node/component/PositionSection.kt | 9 ++++ .../node/component/RemoteDeviceActions.kt | 7 +++ 9 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 core/ui/src/main/kotlin/org/meshtastic/core/ui/component/InsetDivider.kt 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))) },