feat(ui): Redesign NodeItem for improved clarity and density (#4475)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich
2026-02-05 23:01:42 -06:00
committed by GitHub
parent 96551761c8
commit 10df4d47f1
23 changed files with 438 additions and 231 deletions

View File

@@ -190,7 +190,7 @@
<string name="elevation_suffix" translatable="false">MSL</string>
<string name="channel_air_util" translatable="false">ChUtil %.1f%% AirUtilTX %.1f%%</string>
<string name="channel">Channel</string>
<string name="channel">Ch</string>
<string name="channel_name">Channel Name</string>
<string name="qr_code">QR code</string>
<string name="unknown_username">Unknown Username</string>
@@ -392,12 +392,12 @@
<string name="wifi_qr_code_error">Invalid WiFi Credential QR code format</string>
<string name="navigate_back">Navigate Back</string>
<string name="battery">Battery</string>
<string name="channel_utilization">Channel Utilization</string>
<string name="air_utilization">Air Utilization</string>
<string name="temperature">Temperature</string>
<string name="humidity">Humidity</string>
<string name="soil_temperature">Soil Temperature</string>
<string name="soil_moisture">Soil Moisture</string>
<string name="channel_utilization">ChUtil</string>
<string name="air_utilization">AirUtil</string>
<string name="temperature">Temp</string>
<string name="humidity">Hum</string>
<string name="soil_temperature">Soil Temp</string>
<string name="soil_moisture">Soil Moist</string>
<string name="logs">Logs</string>
<string name="hops_away">Hops Away</string>
<string name="hops_away_template">Hops Away: %1$d</string>
@@ -479,7 +479,7 @@
<string name="meshtastic_low_battery_notifications">Low battery notifications</string>
<string name="low_battery_title">Low battery: %1$s</string>
<string name="meshtastic_low_battery_temporary_remote_notifications">Low battery notifications (favorite nodes)</string>
<string name="baro_pressure">Barometric Pressure</string>
<string name="baro_pressure">Baro</string>
<string name="udp_enabled">Enabled</string>
<string name="udp_broadcast">UDP Broadcast</string>
<string name="udp_config">UDP Config</string>

View File

@@ -38,6 +38,7 @@ fun DistanceInfo(
modifier = modifier,
icon = MeshtasticIcons.Distance,
contentDescription = stringResource(Res.string.distance),
label = stringResource(Res.string.distance),
text = distance,
contentColor = contentColor,
)

View File

@@ -43,6 +43,7 @@ fun ElevationInfo(
modifier = modifier,
icon = MeshtasticIcons.Elevation,
contentDescription = stringResource(Res.string.altitude),
label = stringResource(Res.string.altitude),
text = altitude.metersIn(system).toString(system) + " " + suffix,
contentColor = contentColor,
)

View File

@@ -34,6 +34,7 @@ fun HopsInfo(hops: Int, modifier: Modifier = Modifier, contentColor: Color = Mat
modifier = modifier,
icon = MeshtasticIcons.Hops,
contentDescription = stringResource(Res.string.hops_away),
label = stringResource(Res.string.hops_away),
text = hops.toString(),
contentColor = contentColor,
)

View File

@@ -28,18 +28,22 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.meshtastic.core.ui.icon.Elevation
import org.meshtastic.core.ui.icon.MeshtasticIcons
private const val SIZE_ICON = 20
private const val SIZE_ICON = 14
@Composable
fun IconInfo(
icon: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
label: String? = null,
text: String? = null,
style: TextStyle = MaterialTheme.typography.labelMedium,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
@@ -54,9 +58,31 @@ fun IconInfo(
modifier = Modifier.size(SIZE_ICON.dp),
imageVector = icon,
contentDescription = contentDescription,
tint = contentColor,
tint = contentColor.copy(alpha = 0.65f),
)
text?.let { Text(text = it, style = style, color = contentColor) }
if (label != null || text != null) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
label?.let {
Text(
text = it,
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp, letterSpacing = 0.sp),
color = contentColor.copy(alpha = 0.55f),
maxLines = 1,
overflow = TextOverflow.Clip,
softWrap = false,
)
}
text?.let {
Text(
text = it,
style = style.copy(fontWeight = FontWeight.SemiBold, fontSize = 12.sp),
color = contentColor.copy(alpha = 0.95f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
content()
}
}
@@ -65,6 +91,6 @@ fun IconInfo(
@Preview
private fun IconInfoPreview() {
MaterialTheme {
IconInfo(icon = MeshtasticIcons.Elevation, contentDescription = "Elevation", content = { Text(text = "100") })
IconInfo(icon = MeshtasticIcons.Elevation, contentDescription = "Elevation", label = "Elevation", text = "100m")
}
}

View File

@@ -34,12 +34,14 @@ import org.meshtastic.core.ui.util.formatAgo
fun LastHeardInfo(
modifier: Modifier = Modifier,
lastHeard: Int,
showLabel: Boolean = true,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = ImageVector.vectorResource(id = R.drawable.ic_antenna_24),
contentDescription = stringResource(Res.string.node_sort_last_heard),
label = if (showLabel) stringResource(Res.string.node_sort_last_heard) else null,
text = formatAgo(lastHeard),
contentColor = contentColor,
)

View File

@@ -98,7 +98,7 @@ fun NodeSignalQuality(snr: Float, rssi: Int, modifier: Modifier = Modifier) {
maxLines = 1,
)
Icon(
modifier = Modifier.size(20.dp),
modifier = Modifier.size(SIZE_ICON_DP.dp),
imageVector = quality.imageVector,
contentDescription = stringResource(Res.string.signal_quality),
tint = quality.color.invoke(),
@@ -106,6 +106,8 @@ fun NodeSignalQuality(snr: Float, rssi: Int, modifier: Modifier = Modifier) {
}
}
private const val SIZE_ICON_DP = 16
/** Displays the `snr` and `rssi` with color depending on the values respectively. */
@Composable
fun SnrAndRssi(snr: Float, rssi: Int) {
@@ -125,7 +127,7 @@ fun LoraSignalIndicator(snr: Float, rssi: Int, contentColor: Color = MaterialThe
modifier = Modifier.fillMaxSize().padding(8.dp),
) {
Icon(
modifier = Modifier.size(20.dp),
modifier = Modifier.size(SIZE_ICON_DP.dp),
imageVector = quality.imageVector,
contentDescription = stringResource(Res.string.signal_quality),
tint = quality.color.invoke(),
@@ -139,7 +141,7 @@ fun LoraSignalIndicator(snr: Float, rssi: Int, contentColor: Color = MaterialThe
}
@Composable
fun Snr(snr: Float) {
fun Snr(snr: Float, modifier: Modifier = Modifier) {
val color: Color =
if (snr > SNR_GOOD_THRESHOLD) {
Quality.GOOD.color.invoke()
@@ -150,6 +152,7 @@ fun Snr(snr: Float) {
}
Text(
modifier = modifier,
text = "%s %.2fdB".format(stringResource(Res.string.snr), snr),
color = color,
style = MaterialTheme.typography.labelSmall,
@@ -157,7 +160,7 @@ fun Snr(snr: Float) {
}
@Composable
fun Rssi(rssi: Int) {
fun Rssi(rssi: Int, modifier: Modifier = Modifier) {
val color: Color =
if (rssi > RSSI_GOOD_THRESHOLD) {
Quality.GOOD.color.invoke()
@@ -167,6 +170,7 @@ fun Rssi(rssi: Int) {
Quality.BAD.color.invoke()
}
Text(
modifier = modifier,
text = "%s %ddBm".format(stringResource(Res.string.rssi), rssi),
color = color,
style = MaterialTheme.typography.labelSmall,

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 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
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.ui.component
import androidx.compose.foundation.layout.Arrangement
@@ -33,10 +32,12 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.unknown
@@ -49,7 +50,7 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
private const val FORMAT = "%d%%"
private const val SIZE_ICON = 20
private const val SIZE_ICON = 16
@Suppress("MagicNumber", "LongMethod")
@Composable
@@ -64,24 +65,28 @@ fun MaterialBatteryInfo(
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
horizontalArrangement = Arrangement.spacedBy(1.dp),
) {
if (level == null || level < 0) {
Icon(
modifier = Modifier.size(SIZE_ICON.dp),
imageVector = MeshtasticIcons.BatteryUnknown,
tint = contentColor,
tint = contentColor.copy(alpha = 0.65f),
contentDescription = stringResource(Res.string.unknown),
)
} else if (level > 100) {
Icon(
modifier = Modifier.size(SIZE_ICON.dp).rotate(90f),
imageVector = Icons.Rounded.Power,
tint = contentColor,
tint = contentColor.copy(alpha = 0.65f),
contentDescription = levelString,
)
Text(text = "PWD", color = contentColor, style = MaterialTheme.typography.labelMedium)
Text(
text = "PWR",
color = contentColor.copy(alpha = 0.95f),
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold, fontSize = 12.sp),
)
} else {
// Map battery percentage to color
val fillColor =
@@ -111,16 +116,24 @@ fun MaterialBatteryInfo(
)
},
imageVector = MeshtasticIcons.BatteryEmpty,
tint = contentColor,
tint = contentColor.copy(alpha = 0.65f),
contentDescription = levelString,
)
Text(text = levelString, color = contentColor, style = MaterialTheme.typography.labelMedium)
Text(
text = levelString,
color = contentColor.copy(alpha = 0.95f),
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold, fontSize = 12.sp),
)
}
voltage
?.takeIf { it > 0 }
?.let {
Text(text = "%.2fV".format(it), color = contentColor, style = MaterialTheme.typography.labelMedium)
Text(
text = "%.2fV".format(it),
color = contentColor.copy(alpha = 0.8f),
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp),
)
}
}
}

View File

@@ -50,7 +50,7 @@ fun NodeChip(modifier: Modifier = Modifier, node: Node, onClick: ((Node) -> Unit
Box(
modifier =
Modifier.width(IntrinsicSize.Min)
.defaultMinSize(minWidth = 72.dp, minHeight = 32.dp)
.defaultMinSize(minWidth = 64.dp, minHeight = 28.dp)
.padding(horizontal = 8.dp)
.semantics { contentDescription = node.user.short_name.ifEmpty { "Node" } },
contentAlignment = Alignment.Center,

View File

@@ -38,6 +38,7 @@ fun SatelliteCountInfo(
modifier = modifier,
icon = MeshtasticIcons.Satellites,
contentDescription = stringResource(Res.string.sats),
label = stringResource(Res.string.sats),
text = "$satCount",
contentColor = contentColor,
)

View File

@@ -18,83 +18,62 @@ package org.meshtastic.core.ui.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
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.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.air_utilization
import org.meshtastic.core.strings.channel_utilization
import org.meshtastic.core.strings.signal
import org.meshtastic.core.strings.signal_quality
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.icon.AirUtilization
import org.meshtastic.core.ui.icon.ChannelUtilization
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
const val MAX_VALID_SNR = 100F
const val MAX_VALID_RSSI = 0
@Suppress("LongMethod")
@Composable
fun SignalInfo(
modifier: Modifier = Modifier,
node: Node,
isThisNode: Boolean,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
@Suppress("UNUSED_PARAMETER") contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
if (isThisNode) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
IconInfo(
icon = MeshtasticIcons.ChannelUtilization,
contentDescription = stringResource(Res.string.channel_utilization),
text = "%.1f%%".format(node.deviceMetrics.channel_utilization),
contentColor = contentColor,
)
IconInfo(
icon = MeshtasticIcons.AirUtilization,
contentDescription = stringResource(Res.string.air_utilization),
text = "%.1f%%".format(node.deviceMetrics.air_util_tx),
contentColor = contentColor,
)
}
} else {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
if (node.channel > 0) {
ChannelInfo(channel = node.channel, contentColor = contentColor)
}
if (node.hopsAway > 0) {
HopsInfo(hops = node.hopsAway, contentColor = contentColor)
} else {
Row(verticalAlignment = Alignment.CenterVertically) {
if (node.snr < MAX_VALID_SNR && node.rssi < MAX_VALID_RSSI) {
val quality = determineSignalQuality(node.snr, node.rssi)
Snr(node.snr)
Rssi(node.rssi)
IconInfo(
icon = quality.imageVector,
contentDescription = stringResource(Res.string.signal_quality),
contentColor = quality.color.invoke(),
text = "${stringResource(Res.string.signal)} ${stringResource(quality.nameRes)}",
)
}
}
}
}
if (node.snr < MAX_VALID_SNR && node.rssi < MAX_VALID_RSSI) {
val quality = determineSignalQuality(node.snr, node.rssi)
val signalColor = quality.color.invoke()
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
imageVector = quality.imageVector,
contentDescription = stringResource(Res.string.signal_quality),
modifier = Modifier.size(16.dp),
tint = signalColor,
)
Text(
text = "%.1fdB · %ddBm · %s".format(node.snr, node.rssi, stringResource(quality.nameRes)),
style =
MaterialTheme.typography.labelSmall.copy(
fontWeight = FontWeight.Bold,
fontSize = 10.sp,
letterSpacing = 0.sp,
),
color = signalColor,
maxLines = 1,
softWrap = false,
)
}
}
}
@@ -102,22 +81,11 @@ fun SignalInfo(
@Composable
@Preview(showBackground = true)
fun SignalInfoSimplePreview() {
AppTheme {
SignalInfo(
node = Node(num = 1, lastHeard = 0, channel = 0, snr = 12.5F, rssi = -42, hopsAway = 0),
isThisNode = false,
)
}
AppTheme { SignalInfo(node = Node(num = 1, lastHeard = 0, channel = 0, snr = 12.5F, rssi = -42, hopsAway = 0)) }
}
@PreviewLightDark
@Composable
fun SignalInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) node: Node) {
AppTheme { SignalInfo(node = node, isThisNode = false) }
}
@Composable
@PreviewLightDark
fun SignalInfoSelfPreview(@PreviewParameter(NodePreviewParameterProvider::class) node: Node) {
AppTheme { SignalInfo(node = node, isThisNode = true) }
AppTheme { SignalInfo(node = node) }
}

View File

@@ -34,13 +34,23 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.baro_pressure
import org.meshtastic.core.strings.env_metrics_log
import org.meshtastic.core.strings.humidity
import org.meshtastic.core.strings.iaq
import org.meshtastic.core.strings.node_id
import org.meshtastic.core.strings.pax
import org.meshtastic.core.strings.pax_metrics_log
import org.meshtastic.core.strings.role
import org.meshtastic.core.strings.soil_moisture
import org.meshtastic.core.strings.soil_temperature
import org.meshtastic.core.strings.temperature
import org.meshtastic.core.strings.uptime
import org.meshtastic.core.ui.icon.AirQuality
import org.meshtastic.core.ui.icon.ArrowCircleUp
@@ -55,7 +65,7 @@ import org.meshtastic.core.ui.icon.Role
import org.meshtastic.core.ui.icon.Soil
import org.meshtastic.core.ui.icon.Temperature
private const val SIZE_ICON = 20
private const val SIZE_ICON = 14
@Composable
fun TemperatureInfo(
@@ -67,6 +77,7 @@ fun TemperatureInfo(
modifier = modifier,
icon = MeshtasticIcons.Temperature,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.temperature),
text = temp,
contentColor = contentColor,
)
@@ -82,6 +93,7 @@ fun HumidityInfo(
modifier = modifier,
icon = MeshtasticIcons.Humidity,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.humidity),
text = humidity,
contentColor = contentColor,
)
@@ -97,6 +109,7 @@ fun PressureInfo(
modifier = modifier,
icon = MeshtasticIcons.Pressure,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.baro_pressure),
text = pressure,
contentColor = contentColor,
)
@@ -113,6 +126,7 @@ fun SoilTemperatureInfo(
icon = MeshtasticIcons.Soil,
overlayIcon = MeshtasticIcons.Temperature,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.soil_temperature),
text = temp,
contentColor = contentColor,
)
@@ -129,6 +143,7 @@ fun SoilMoistureInfo(
icon = MeshtasticIcons.Soil,
overlayIcon = MeshtasticIcons.Humidity,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.soil_moisture),
text = moisture,
contentColor = contentColor,
)
@@ -144,6 +159,7 @@ fun PaxcountInfo(
modifier = modifier,
icon = MeshtasticIcons.Paxcount,
contentDescription = stringResource(Res.string.pax_metrics_log),
label = stringResource(Res.string.pax),
text = pax,
contentColor = contentColor,
)
@@ -159,17 +175,24 @@ fun AirQualityInfo(
modifier = modifier,
icon = MeshtasticIcons.AirQuality,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.iaq),
text = iaq,
contentColor = contentColor,
)
}
@Composable
fun PowerInfo(value: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
fun PowerInfo(
value: String,
modifier: Modifier = Modifier,
label: String? = null,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Power,
contentDescription = stringResource(Res.string.env_metrics_log),
label = label,
text = value,
contentColor = contentColor,
)
@@ -185,6 +208,7 @@ fun UptimeInfo(
modifier = modifier,
icon = MeshtasticIcons.ArrowCircleUp,
contentDescription = stringResource(Res.string.uptime),
label = stringResource(Res.string.uptime),
text = uptime,
contentColor = contentColor,
)
@@ -237,6 +261,7 @@ fun OverlayIconInfo(
overlayIcon: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
label: String? = null,
text: String? = null,
style: TextStyle = MaterialTheme.typography.labelMedium,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
@@ -250,7 +275,7 @@ fun OverlayIconInfo(
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = contentColor,
tint = contentColor.copy(alpha = 0.65f),
modifier =
Modifier.size(SIZE_ICON.dp).drawWithContent {
drawContent()
@@ -260,6 +285,24 @@ fun OverlayIconInfo(
}
},
)
text?.let { Text(text = it, style = style, color = contentColor) }
label?.let {
Text(
text = it,
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp, letterSpacing = 0.sp),
color = contentColor.copy(alpha = 0.55f),
maxLines = 1,
overflow = TextOverflow.Clip,
softWrap = false,
)
}
text?.let {
Text(
text = it,
style = style.copy(fontWeight = FontWeight.SemiBold, fontSize = 12.sp),
color = contentColor.copy(alpha = 0.95f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}

View File

@@ -21,6 +21,7 @@ import androidx.compose.material.icons.rounded.Air
import androidx.compose.material.icons.rounded.DataArray
import androidx.compose.material.icons.rounded.ElectricBolt
import androidx.compose.material.icons.rounded.Grass
import androidx.compose.material.icons.rounded.LineAxis
import androidx.compose.material.icons.rounded.People
import androidx.compose.material.icons.rounded.SocialDistance
import androidx.compose.material.icons.rounded.Speed
@@ -54,3 +55,6 @@ val MeshtasticIcons.Speed: ImageVector
get() = Icons.Rounded.Speed
val MeshtasticIcons.Chart: ImageVector
get() = Icons.Rounded.StackedLineChart
val MeshtasticIcons.LineAxis: ImageVector
get() = Icons.Rounded.LineAxis

View File

@@ -20,6 +20,7 @@ import android.text.format.DateUtils
import com.meshtastic.core.strings.getString
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.now
import org.meshtastic.core.strings.unknown
import java.lang.System.currentTimeMillis
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
@@ -36,6 +37,8 @@ import kotlin.time.Duration.Companion.seconds
* @return A [String] representing the relative time that has passed.
*/
fun formatAgo(lastSeenUnixSeconds: Int): String {
if (lastSeenUnixSeconds <= 0) return getString(Res.string.unknown)
val lastSeenDuration = lastSeenUnixSeconds.seconds
val currentDuration = currentTimeMillis().milliseconds
val diff = (currentDuration - lastSeenDuration).absoluteValue

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 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
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.component
import androidx.compose.material.icons.Icons
@@ -39,6 +38,7 @@ fun DistanceInfo(
modifier = modifier,
icon = Icons.Rounded.SocialDistance,
contentDescription = stringResource(Res.string.distance),
label = stringResource(Res.string.distance),
text = distance,
contentColor = contentColor,
)

View File

@@ -43,6 +43,7 @@ fun ElevationInfo(
modifier = modifier,
icon = MeshtasticIcons.Elevation,
contentDescription = stringResource(Res.string.altitude),
label = stringResource(Res.string.altitude),
text = altitude.metersIn(system).toString(system) + " " + suffix,
contentColor = contentColor,
)

View File

@@ -34,6 +34,7 @@ fun HopsInfo(hops: Int, modifier: Modifier = Modifier, contentColor: Color = Mat
modifier = modifier,
icon = Icons.Rounded.CrueltyFree,
contentDescription = stringResource(Res.string.hops_away),
label = stringResource(Res.string.hops_away),
text = hops.toString(),
contentColor = contentColor,
)

View File

@@ -40,6 +40,7 @@ fun IconInfo(
icon: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
label: String? = null,
text: String? = null,
style: TextStyle = MaterialTheme.typography.labelMedium,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
@@ -56,6 +57,7 @@ fun IconInfo(
contentDescription = contentDescription,
tint = contentColor,
)
label?.let { Text(text = it, style = style, color = contentColor) }
text?.let { Text(text = it, style = style, color = contentColor) }
content()
}

View File

@@ -40,6 +40,7 @@ fun LastHeardInfo(
modifier = modifier,
icon = ImageVector.vectorResource(id = R.drawable.ic_antenna_24),
contentDescription = stringResource(Res.string.node_sort_last_heard),
label = stringResource(Res.string.node_sort_last_heard),
text = formatAgo(lastHeard),
contentColor = contentColor,
)

View File

@@ -14,11 +14,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("MagicNumber")
package org.meshtastic.feature.node.component
import android.content.res.Configuration
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
@@ -40,6 +43,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -50,13 +54,21 @@ import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.air_utilization
import org.meshtastic.core.strings.channel_utilization
import org.meshtastic.core.strings.current
import org.meshtastic.core.strings.elevation_suffix
import org.meshtastic.core.strings.signal_quality
import org.meshtastic.core.strings.unknown_username
import org.meshtastic.core.strings.voltage
import org.meshtastic.core.ui.component.AirQualityInfo
import org.meshtastic.core.ui.component.ChannelInfo
import org.meshtastic.core.ui.component.DistanceInfo
import org.meshtastic.core.ui.component.ElevationInfo
import org.meshtastic.core.ui.component.HardwareInfo
import org.meshtastic.core.ui.component.HopsInfo
import org.meshtastic.core.ui.component.HumidityInfo
import org.meshtastic.core.ui.component.IconInfo
import org.meshtastic.core.ui.component.LastHeardInfo
import org.meshtastic.core.ui.component.MaterialBatteryInfo
import org.meshtastic.core.ui.component.NodeChip
@@ -66,21 +78,27 @@ import org.meshtastic.core.ui.component.PaxcountInfo
import org.meshtastic.core.ui.component.PowerInfo
import org.meshtastic.core.ui.component.PressureInfo
import org.meshtastic.core.ui.component.RoleInfo
import org.meshtastic.core.ui.component.Rssi
import org.meshtastic.core.ui.component.SatelliteCountInfo
import org.meshtastic.core.ui.component.SignalInfo
import org.meshtastic.core.ui.component.Snr
import org.meshtastic.core.ui.component.SoilMoistureInfo
import org.meshtastic.core.ui.component.SoilTemperatureInfo
import org.meshtastic.core.ui.component.TemperatureInfo
import org.meshtastic.core.ui.component.determineSignalQuality
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.icon.AirUtilization
import org.meshtastic.core.ui.icon.ChannelUtilization
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.Config
private const val ACTIVE_ALPHA = 0.5f
private const val INACTIVE_ALPHA = 0.2f
private const val GRID_COLUMNS = 3
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
@Suppress("LongMethod")
fun NodeItem(
thisNode: Node?,
thatNode: Node,
@@ -97,15 +115,6 @@ fun NodeItem(
val isIgnored = thatNode.isIgnored
val originalLongName = (thatNode.user.long_name ?: "").ifEmpty { stringResource(Res.string.unknown_username) }
@Suppress("MagicNumber")
val longName =
remember(originalLongName) {
if (originalLongName.length > 20) {
"${originalLongName.take(20)}"
} else {
originalLongName
}
}
val isThisNode = remember(thatNode) { thisNode?.num == thatNode.num }
val system =
remember(distanceUnits) {
@@ -146,13 +155,13 @@ fun NodeItem(
Card(modifier = modifier.fillMaxWidth(), colors = cardColors) {
Column(
modifier =
Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick).fillMaxWidth().padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick).fillMaxWidth().padding(12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
NodeItemHeader(
thatNode = thatNode,
isThisNode = isThisNode,
longName = longName,
longName = originalLongName,
style = style,
isIgnored = isIgnored,
isFavorite = isFavorite,
@@ -162,17 +171,201 @@ fun NodeItem(
contentColor = contentColor,
)
NodeItemMetrics(thatNode = thatNode, distance = distance, system = system, contentColor = contentColor)
NodeBatteryPositionRow(
thatNode = thatNode,
distance = distance,
system = system,
contentColor = contentColor,
)
SignalInfo(node = thatNode, isThisNode = isThisNode, contentColor = contentColor)
NodeSignalRow(thatNode = thatNode, isThisNode = isThisNode, contentColor = contentColor)
NodeItemEnvironment(thatNode = thatNode, tempInFahrenheit = tempInFahrenheit, contentColor = contentColor)
val sensorItems = gatherSensors(thatNode, tempInFahrenheit, contentColor)
if (sensorItems.isNotEmpty()) {
MetricsGrid(sensorItems)
}
NodeItemFooter(thatNode = thatNode, contentColor = contentColor)
}
}
}
@Composable
private fun NodeBatteryPositionRow(
thatNode: Node,
distance: String?,
system: Config.DisplayConfig.DisplayUnits,
contentColor: Color,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
MaterialBatteryInfo(
level = thatNode.batteryLevel ?: 0,
voltage = thatNode.voltage ?: 0f,
contentColor = contentColor,
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
if (distance != null) {
DistanceInfo(distance = distance, contentColor = contentColor)
}
thatNode.validPosition?.let { position ->
ElevationInfo(
altitude = position.altitude ?: 0,
system = system,
suffix = stringResource(Res.string.elevation_suffix),
contentColor = contentColor,
)
}
}
}
}
@Composable
private fun NodeSignalRow(thatNode: Node, isThisNode: Boolean, contentColor: Color) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
if (isThisNode) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
IconInfo(
icon = MeshtasticIcons.ChannelUtilization,
contentDescription = stringResource(Res.string.channel_utilization),
label = stringResource(Res.string.channel_utilization),
text = "%.1f%%".format(thatNode.deviceMetrics.channel_utilization),
contentColor = contentColor,
)
IconInfo(
icon = MeshtasticIcons.AirUtilization,
contentDescription = stringResource(Res.string.air_utilization),
label = stringResource(Res.string.air_utilization),
text = "%.1f%%".format(thatNode.deviceMetrics.air_util_tx),
contentColor = contentColor,
)
}
} else {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
if (thatNode.hopsAway > 0) {
HopsInfo(hops = thatNode.hopsAway, contentColor = contentColor)
} else {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (thatNode.snr < 100f) Snr(thatNode.snr)
if (thatNode.rssi < 0) Rssi(thatNode.rssi)
if (thatNode.snr < 100f && thatNode.rssi < 0) {
val quality = determineSignalQuality(thatNode.snr, thatNode.rssi)
IconInfo(
icon = quality.imageVector,
contentDescription = stringResource(Res.string.signal_quality),
contentColor = quality.color.invoke(),
text = stringResource(quality.nameRes),
)
}
}
}
if (thatNode.channel > 0) {
ChannelInfo(channel = thatNode.channel, contentColor = contentColor)
}
}
}
val satCount = thatNode.validPosition?.sats_in_view ?: 0
if (satCount > 0) {
SatelliteCountInfo(satCount = satCount, contentColor = contentColor)
} else {
Spacer(Modifier)
}
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
private fun gatherSensors(node: Node, tempInFahrenheit: Boolean, contentColor: Color): List<@Composable () -> Unit> {
val items = mutableListOf<@Composable () -> Unit>()
val env = node.environmentMetrics
val pax = node.paxcounter
if ((pax.ble ?: 0) != 0 || (pax.wifi ?: 0) != 0) {
items.add { PaxcountInfo(pax = "B:${pax.ble ?: 0} W:${pax.wifi ?: 0}", contentColor = contentColor) }
}
if ((env.temperature ?: 0f) != 0f) {
val temp =
if (tempInFahrenheit) {
"%.1f°F".format(celsiusToFahrenheit(env.temperature ?: 0f))
} else {
"%.1f°C".format(env.temperature ?: 0f)
}
items.add { TemperatureInfo(temp = temp, contentColor = contentColor) }
}
if ((env.relative_humidity ?: 0f) != 0f) {
items.add { HumidityInfo(humidity = "%.0f%%".format(env.relative_humidity ?: 0f), contentColor = contentColor) }
}
if ((env.barometric_pressure ?: 0f) != 0f) {
items.add {
PressureInfo(pressure = "%.1fhPa".format(env.barometric_pressure ?: 0f), contentColor = contentColor)
}
}
if ((env.soil_temperature ?: 0f) != 0f) {
val temp =
if (tempInFahrenheit) {
"%.1f°F".format(celsiusToFahrenheit(env.soil_temperature ?: 0f))
} else {
"%.1f°C".format(env.soil_temperature ?: 0f)
}
items.add { SoilTemperatureInfo(temp = temp, contentColor = contentColor) }
}
if ((env.soil_moisture ?: 0) != 0 && (env.soil_temperature ?: 0f) != 0f) {
items.add { SoilMoistureInfo(moisture = "${env.soil_moisture}%", contentColor = contentColor) }
}
if ((env.voltage ?: 0f) != 0f) {
items.add {
PowerInfo(
value = "%.2fV".format(env.voltage ?: 0f),
label = stringResource(Res.string.voltage),
contentColor = contentColor,
)
}
}
if ((env.current ?: 0f) != 0f) {
items.add {
PowerInfo(
value = "%.1fmA".format(env.current ?: 0f),
label = stringResource(Res.string.current),
contentColor = contentColor,
)
}
}
if ((env.iaq ?: 0) != 0) {
items.add { AirQualityInfo(iaq = "${env.iaq}", contentColor = contentColor) }
}
return items
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun MetricsGrid(items: List<@Composable () -> Unit>) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
maxItemsInEachRow = GRID_COLUMNS,
horizontalArrangement = Arrangement.SpaceBetween,
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
val remainder = items.size % GRID_COLUMNS
items.forEach { item -> Box(Modifier.weight(1f)) { item() } }
if (remainder != 0) {
repeat(GRID_COLUMNS - remainder) { Spacer(Modifier.weight(1f)) }
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun NodeItemHeader(
@@ -187,23 +380,31 @@ private fun NodeItemHeader(
connectionState: ConnectionState,
contentColor: Color,
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
NodeChip(node = thatNode)
NodeKeyStatusIcon(
hasPKC = thatNode.hasPKC,
mismatchKey = thatNode.mismatchKey,
publicKey = thatNode.user.public_key,
modifier = Modifier.size(32.dp),
modifier = Modifier.size(24.dp),
)
Text(
modifier = Modifier.weight(1f),
text = longName,
style = MaterialTheme.typography.titleMediumEmphasized.copy(fontStyle = style),
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
softWrap = true,
)
LastHeardInfo(lastHeard = thatNode.lastHeard, contentColor = contentColor)
Column(modifier = Modifier.weight(1f)) {
Text(
text = longName,
style = MaterialTheme.typography.titleMediumEmphasized.copy(fontStyle = style),
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
LastHeardInfo(lastHeard = thatNode.lastHeard, showLabel = false, contentColor = contentColor)
}
NodeStatusIcons(
isThisNode = isThisNode,
isFavorite = isFavorite,
@@ -215,102 +416,6 @@ private fun NodeItemHeader(
}
}
@Composable
private fun NodeItemMetrics(
thatNode: Node,
distance: String?,
system: Config.DisplayConfig.DisplayUnits,
contentColor: Color,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
if ((thatNode.batteryLevel ?: 0) > 0 || (thatNode.voltage ?: 0f) > 0f) {
MaterialBatteryInfo(
level = thatNode.batteryLevel ?: 0,
voltage = thatNode.voltage ?: 0f,
contentColor = contentColor,
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
if (distance != null) {
DistanceInfo(distance = distance, contentColor = contentColor)
}
thatNode.validPosition?.let { position ->
ElevationInfo(
altitude = position.altitude ?: 0,
system = system,
suffix = stringResource(Res.string.elevation_suffix),
contentColor = contentColor,
)
}
val satCount = thatNode.validPosition?.sats_in_view ?: 0
if (satCount > 0) {
SatelliteCountInfo(satCount = satCount, contentColor = contentColor)
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
@Suppress("CyclomaticComplexMethod")
private fun NodeItemEnvironment(thatNode: Node, tempInFahrenheit: Boolean, contentColor: Color) {
val env = thatNode.environmentMetrics
val pax = thatNode.paxcounter
if (thatNode.hasEnvironmentMetrics || (pax.ble ?: 0) != 0 || (pax.wifi ?: 0) != 0) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if ((pax.ble ?: 0) != 0 || (pax.wifi ?: 0) != 0) {
PaxcountInfo(pax = "B:${pax.ble ?: 0} W:${pax.wifi ?: 0}", contentColor = contentColor)
}
if ((env.temperature ?: 0f) != 0f) {
val temp =
if (tempInFahrenheit) {
"%.1f°F".format(celsiusToFahrenheit(env.temperature ?: 0f))
} else {
"%.1f°C".format(env.temperature ?: 0f)
}
TemperatureInfo(temp = temp, contentColor = contentColor)
}
if ((env.relative_humidity ?: 0f) != 0f) {
HumidityInfo(humidity = "%.0f%%".format(env.relative_humidity ?: 0f), contentColor = contentColor)
}
if ((env.barometric_pressure ?: 0f) != 0f) {
PressureInfo(pressure = "%.1fhPa".format(env.barometric_pressure ?: 0f), contentColor = contentColor)
}
if ((env.soil_temperature ?: 0f) != 0f) {
val temp =
if (tempInFahrenheit) {
"%.1f°F".format(celsiusToFahrenheit(env.soil_temperature ?: 0f))
} else {
"%.1f°C".format(env.soil_temperature ?: 0f)
}
SoilTemperatureInfo(temp = temp, contentColor = contentColor)
}
if ((env.soil_moisture ?: 0) != 0 && (env.soil_temperature ?: 0f) != 0f) {
SoilMoistureInfo(moisture = "${env.soil_moisture}%", contentColor = contentColor)
}
if ((env.voltage ?: 0f) != 0f) {
PowerInfo(value = "%.2fV".format(env.voltage ?: 0f), contentColor = contentColor)
}
if ((env.current ?: 0f) != 0f) {
PowerInfo(value = "%.1fmA".format(env.current ?: 0f), contentColor = contentColor)
}
if ((env.iaq ?: 0) != 0) {
AirQualityInfo(iaq = "${env.iaq}", contentColor = contentColor)
}
}
}
}
@Composable
private fun NodeItemFooter(thatNode: Node, contentColor: Color) {
Row(
@@ -318,9 +423,9 @@ private fun NodeItemFooter(thatNode: Node, contentColor: Color) {
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
HardwareInfo(hwModel = thatNode.user.hw_model?.name ?: "", contentColor = contentColor)
RoleInfo(role = thatNode.user.role?.name ?: "", contentColor = contentColor)
NodeIdInfo(id = (thatNode.user.id ?: "").ifEmpty { "???" }, contentColor = contentColor)
HardwareInfo(hwModel = thatNode.user.hw_model.name, contentColor = contentColor)
RoleInfo(role = thatNode.user.role.name, contentColor = contentColor)
NodeIdInfo(id = thatNode.user.id.ifEmpty { "???" }, contentColor = contentColor)
}
}
@@ -329,7 +434,17 @@ private fun NodeItemFooter(thatNode: Node, contentColor: Color) {
fun NodeInfoSimplePreview() {
AppTheme {
val thisNode = NodePreviewParameterProvider().values.first()
val thatNode = NodePreviewParameterProvider().values.last()
val thatNode = NodePreviewParameterProvider().values.last().copy(lastHeard = 0)
NodeItem(thisNode = thisNode, thatNode = thatNode, 0, true, connectionState = ConnectionState.Connected)
}
}
@Composable
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
fun NodeInfoSignalPreview() {
AppTheme {
val thisNode = NodePreviewParameterProvider().values.first()
val thatNode = NodePreviewParameterProvider().values.last().copy(hopsAway = 0, snr = 5.5f, rssi = -100)
NodeItem(thisNode = thisNode, thatNode = thatNode, 0, true, connectionState = ConnectionState.Connected)
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-2026 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
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.component
import androidx.compose.material.icons.Icons
@@ -39,6 +38,7 @@ fun SatelliteCountInfo(
modifier = modifier,
icon = Icons.TwoTone.SatelliteAlt,
contentDescription = stringResource(Res.string.sats),
label = stringResource(Res.string.sats),
text = "$satCount",
contentColor = contentColor,
)

View File

@@ -56,7 +56,7 @@ import org.meshtastic.core.strings.request_telemetry
import org.meshtastic.core.strings.telemetry
import org.meshtastic.core.strings.userinfo
import org.meshtastic.core.ui.icon.AirQuality
import org.meshtastic.core.ui.icon.Chart
import org.meshtastic.core.ui.icon.LineAxis
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Person
import org.meshtastic.core.ui.icon.Refresh
@@ -232,7 +232,7 @@ private fun FeatureRow(node: Node, feature: TelemetricFeature, hasLogs: Boolean,
},
) {
Icon(
MeshtasticIcons.Chart,
MeshtasticIcons.LineAxis,
contentDescription = logsDescription,
modifier = Modifier.size(IconButtonDefaults.mediumIconSize),
tint = MaterialTheme.colorScheme.primary,

View File

@@ -14,6 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("TooManyFunctions")
package org.meshtastic.feature.node.component
import androidx.compose.material.icons.Icons
@@ -33,9 +35,15 @@ import androidx.compose.ui.graphics.Color
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.env_metrics_log
import org.meshtastic.core.strings.humidity
import org.meshtastic.core.strings.iaq
import org.meshtastic.core.strings.node_id
import org.meshtastic.core.strings.pax
import org.meshtastic.core.strings.pax_metrics_log
import org.meshtastic.core.strings.role
import org.meshtastic.core.strings.soil_moisture
import org.meshtastic.core.strings.soil_temperature
import org.meshtastic.core.strings.temperature
@Composable
fun TemperatureInfo(
@@ -47,6 +55,7 @@ fun TemperatureInfo(
modifier = modifier,
icon = Icons.Rounded.Thermostat,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.temperature),
text = temp,
contentColor = contentColor,
)
@@ -62,6 +71,7 @@ fun HumidityInfo(
modifier = modifier,
icon = Icons.Rounded.WaterDrop,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.humidity),
text = humidity,
contentColor = contentColor,
)
@@ -77,6 +87,7 @@ fun SoilTemperatureInfo(
modifier = modifier,
icon = Icons.Rounded.Grass,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.soil_temperature),
text = temp,
contentColor = contentColor,
)
@@ -92,6 +103,7 @@ fun SoilMoistureInfo(
modifier = modifier,
icon = Icons.Rounded.Grass,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.soil_moisture),
text = moisture,
contentColor = contentColor,
)
@@ -107,6 +119,7 @@ fun PaxcountInfo(
modifier = modifier,
icon = Icons.Rounded.People,
contentDescription = stringResource(Res.string.pax_metrics_log),
label = stringResource(Res.string.pax),
text = pax,
contentColor = contentColor,
)
@@ -122,17 +135,24 @@ fun AirQualityInfo(
modifier = modifier,
icon = Icons.Rounded.Air,
contentDescription = stringResource(Res.string.env_metrics_log),
label = stringResource(Res.string.iaq),
text = iaq,
contentColor = contentColor,
)
}
@Composable
fun PowerInfo(value: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
fun PowerInfo(
value: String,
modifier: Modifier = Modifier,
label: String? = null,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.ElectricBolt,
contentDescription = stringResource(Res.string.env_metrics_log),
label = label,
text = value,
contentColor = contentColor,
)