refactor(ui): Icon audit and node list item refactor (#4313)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-01-25 16:43:23 -06:00 committed by GitHub
parent 5db2c9d69c
commit a28aa4d52e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 2178 additions and 702 deletions

View file

@ -18,10 +18,10 @@ package org.meshtastic.feature.node.component
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
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.material.icons.rounded.ForkLeft
import androidx.compose.material.icons.rounded.Icecream
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -63,7 +63,7 @@ fun AdministrationSection(
Column {
ListItem(
text = stringResource(Res.string.request_metadata),
leadingIcon = Icons.Default.Memory,
leadingIcon = Icons.Rounded.Memory,
trailingIcon = null,
onClick = {
onAction(NodeDetailAction.TriggerServiceAction(ServiceAction.GetDeviceMetadata(node.num)))
@ -74,7 +74,7 @@ fun AdministrationSection(
ListItem(
text = stringResource(Res.string.remote_admin),
leadingIcon = Icons.Default.Settings,
leadingIcon = Icons.Rounded.Settings,
enabled = metricsState.isLocal || node.metadata != null,
) {
onAction(NodeDetailAction.Navigate(SettingsRoutes.Settings(node.num)))
@ -101,8 +101,8 @@ private fun FirmwareSection(
firmwareEdition?.let { edition ->
val icon =
when (edition) {
MeshProtos.FirmwareEdition.VANILLA -> Icons.Default.Icecream
else -> Icons.Default.ForkLeft
MeshProtos.FirmwareEdition.VANILLA -> Icons.Rounded.Icecream
else -> Icons.Rounded.ForkLeft
}
ListItem(
@ -138,7 +138,7 @@ private fun FirmwareVersionItems(
ListItem(
text = stringResource(Res.string.installed_firmware_version),
leadingIcon = Icons.Default.Memory,
leadingIcon = Icons.Rounded.Memory,
supportingText = version.substringBeforeLast("."),
copyable = true,
leadingIconTint = statusColor,
@ -149,7 +149,7 @@ private fun FirmwareVersionItems(
ListItem(
text = stringResource(Res.string.latest_stable_firmware),
leadingIcon = Icons.Default.Memory,
leadingIcon = Icons.Rounded.Memory,
supportingText = latestStable.id.substringBeforeLast(".").replace("v", ""),
copyable = true,
leadingIconTint = MaterialTheme.colorScheme.StatusGreen,
@ -161,7 +161,7 @@ private fun FirmwareVersionItems(
ListItem(
text = stringResource(Res.string.latest_alpha_firmware),
leadingIcon = Icons.Default.Memory,
leadingIcon = Icons.Rounded.Memory,
supportingText = latestAlpha.id.substringBeforeLast(".").replace("v", ""),
copyable = true,
leadingIconTint = MaterialTheme.colorScheme.StatusYellow,

View file

@ -0,0 +1,47 @@
/*
* 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Tsunami
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewLightDark
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun ChannelInfo(
channel: Int,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Tsunami,
contentDescription = "Channel",
text = channel.toString(),
contentColor = contentColor,
)
}
@PreviewLightDark
@Composable
private fun ChannelInfoPreview() {
AppTheme { ChannelInfo(channel = 2) }
}

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.foundation.Canvas
@ -27,8 +26,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ErrorOutline
import androidx.compose.material.icons.filled.GpsFixed
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.GpsFixed
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -153,7 +152,7 @@ fun CompassSheetContent(
)
// Quick way to re-request a fresh fix without leaving the compass sheet
Button(onClick = onRequestPosition, modifier = Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.GpsFixed, contentDescription = null)
Icon(imageVector = Icons.Rounded.GpsFixed, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(Res.string.exchange_position))
}
@ -190,7 +189,7 @@ private fun WarningList(
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Icon(
imageVector = Icons.Default.ErrorOutline,
imageVector = Icons.Rounded.ErrorOutline,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
)
@ -205,13 +204,13 @@ private fun WarningList(
if (warnings.contains(CompassWarning.NO_LOCATION_PERMISSION)) {
Button(onClick = onRequestPermission, modifier = Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.GpsFixed, contentDescription = null)
Icon(imageVector = Icons.Rounded.GpsFixed, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(Res.string.compass_no_location_permission))
}
} else if (warnings.contains(CompassWarning.LOCATION_DISABLED)) {
Button(onClick = onOpenLocationSettings, modifier = Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.GpsFixed, contentDescription = null)
Icon(imageVector = Icons.Rounded.GpsFixed, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(Res.string.compass_location_disabled))
}

View file

@ -19,8 +19,6 @@ package org.meshtastic.feature.node.component
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
@ -36,6 +34,8 @@ import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.theme.AppTheme
internal const val COOL_DOWN_TIME_MS = 30000L
@ -147,7 +147,7 @@ fun CooldownOutlinedIconButton(
private fun CooldownOutlinedIconButtonPreview() {
AppTheme {
CooldownOutlinedIconButton(onClick = {}, cooldownTimestamp = System.currentTimeMillis() - 15000L) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
}

View file

@ -28,10 +28,10 @@ import androidx.compose.material.icons.automirrored.filled.Message
import androidx.compose.material.icons.automirrored.filled.VolumeOff
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.automirrored.outlined.VolumeMute
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.StarBorder
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.QrCode2
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarBorder
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
@ -170,7 +170,7 @@ private fun PrimaryActionsRow(
IconToggleButton(checked = node.isFavorite, onCheckedChange = { onFavoriteClick() }) {
Icon(
imageVector = if (node.isFavorite) Icons.Default.Star else Icons.Default.StarBorder,
imageVector = if (node.isFavorite) Icons.Rounded.Star else Icons.Rounded.StarBorder,
contentDescription = stringResource(Res.string.favorite),
tint = if (node.isFavorite) Color.Yellow else LocalContentColor.current,
)

View file

@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Router
import androidx.compose.material.icons.rounded.Router
import androidx.compose.material.icons.twotone.Verified
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.runtime.Composable
@ -78,7 +78,7 @@ fun DeviceDetailsSection(state: MetricsState, modifier: Modifier = Modifier) {
?: deviceHardware.displayName
ListItem(
text = stringResource(Res.string.hardware),
leadingIcon = Icons.Default.Router,
leadingIcon = Icons.Rounded.Router,
supportingText = deviceText,
copyable = true,
trailingIcon = null,

View file

@ -20,17 +20,17 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Air
import androidx.compose.material.icons.filled.BlurOn
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Height
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Scale
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Thermostat
import androidx.compose.material.icons.filled.WaterDrop
import androidx.compose.material.icons.outlined.Navigation
import androidx.compose.material.icons.rounded.Air
import androidx.compose.material.icons.rounded.BlurOn
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Height
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.Power
import androidx.compose.material.icons.rounded.Scale
import androidx.compose.material.icons.rounded.Speed
import androidx.compose.material.icons.rounded.Thermostat
import androidx.compose.material.icons.rounded.WaterDrop
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -77,7 +77,7 @@ internal fun EnvironmentMetrics(
VectorMetricInfo(
Res.string.temperature,
temperature.toTempString(isFahrenheit),
Icons.Default.Thermostat,
Icons.Rounded.Thermostat,
),
)
}
@ -86,7 +86,7 @@ internal fun EnvironmentMetrics(
VectorMetricInfo(
Res.string.humidity,
"%.0f%%".format(relativeHumidity),
Icons.Default.WaterDrop,
Icons.Rounded.WaterDrop,
),
)
}
@ -95,7 +95,7 @@ internal fun EnvironmentMetrics(
VectorMetricInfo(
Res.string.pressure,
"%.0f hPa".format(barometricPressure),
Icons.Default.Speed,
Icons.Rounded.Speed,
),
)
}
@ -104,29 +104,29 @@ internal fun EnvironmentMetrics(
VectorMetricInfo(
Res.string.gas_resistance,
"%.0f MΩ".format(gasResistance),
Icons.Default.BlurOn,
Icons.Rounded.BlurOn,
),
)
}
if (hasVoltage()) {
add(VectorMetricInfo(Res.string.voltage, "%.2fV".format(voltage), Icons.Default.Bolt))
add(VectorMetricInfo(Res.string.voltage, "%.2fV".format(voltage), Icons.Rounded.Bolt))
}
if (hasCurrent()) {
add(VectorMetricInfo(Res.string.current, "%.1fmA".format(current), Icons.Default.Power))
add(VectorMetricInfo(Res.string.current, "%.1fmA".format(current), Icons.Rounded.Power))
}
if (hasIaq()) add(VectorMetricInfo(Res.string.iaq, iaq.toString(), Icons.Default.Air))
if (hasIaq()) add(VectorMetricInfo(Res.string.iaq, iaq.toString(), Icons.Rounded.Air))
if (hasDistance()) {
add(
VectorMetricInfo(
Res.string.distance,
distance.toSmallDistanceString(displayUnits),
Icons.Default.Height,
Icons.Rounded.Height,
),
)
}
if (hasLux()) add(VectorMetricInfo(Res.string.lux, "%.0f lx".format(lux), Icons.Default.LightMode))
if (hasLux()) add(VectorMetricInfo(Res.string.lux, "%.0f lx".format(lux), Icons.Rounded.LightMode))
if (hasUvLux()) {
add(VectorMetricInfo(Res.string.uv_lux, "%.0f lx".format(uvLux), Icons.Default.LightMode))
add(VectorMetricInfo(Res.string.uv_lux, "%.0f lx".format(uvLux), Icons.Rounded.LightMode))
}
if (hasWindSpeed()) {
@Suppress("MagicNumber")
@ -141,7 +141,7 @@ internal fun EnvironmentMetrics(
)
}
if (hasWeight()) {
add(VectorMetricInfo(Res.string.weight, "%.2f kg".format(weight), Icons.Default.Scale))
add(VectorMetricInfo(Res.string.weight, "%.2f kg".format(weight), Icons.Rounded.Scale))
}
if (hasTemperature() && hasRelativeHumidity()) {
val dewPoint = UnitConversions.calculateDewPoint(temperature, relativeHumidity)

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 android.content.ActivityNotFoundException
@ -29,8 +28,8 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.rounded.Download
import androidx.compose.material.icons.rounded.Link
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -77,7 +76,7 @@ fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease, modifier: Modi
},
modifier = Modifier.weight(1f),
) {
Icon(imageVector = Icons.Default.Link, contentDescription = stringResource(Res.string.view_release))
Icon(imageVector = Icons.Rounded.Link, contentDescription = stringResource(Res.string.view_release))
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(Res.string.view_release))
}
@ -93,7 +92,7 @@ fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease, modifier: Modi
},
modifier = Modifier.weight(1f),
) {
Icon(imageVector = Icons.Default.Download, contentDescription = stringResource(Res.string.download))
Icon(imageVector = Icons.Rounded.Download, contentDescription = stringResource(Res.string.download))
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(Res.string.download))
}

View file

@ -0,0 +1,46 @@
/*
* 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CrueltyFree
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewLightDark
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.hops_away
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun HopsInfo(hops: Int, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.CrueltyFree,
contentDescription = stringResource(Res.string.hops_away),
text = hops.toString(),
contentColor = contentColor,
)
}
@PreviewLightDark
@Composable
private fun HopsInfoPreview() {
AppTheme { HopsInfo(hops = 3) }
}

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.foundation.layout.Arrangement
@ -28,6 +27,7 @@ import androidx.compose.ui.Alignment
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.icon.Elevation
@ -41,6 +41,7 @@ fun IconInfo(
contentDescription: String,
modifier: Modifier = Modifier,
text: String? = null,
style: TextStyle = MaterialTheme.typography.labelMedium,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
content: @Composable () -> Unit = {},
) {
@ -55,7 +56,7 @@ fun IconInfo(
contentDescription = contentDescription,
tint = contentColor,
)
text?.let { Text(text = it, style = MaterialTheme.typography.labelMedium, color = contentColor) }
text?.let { Text(text = it, style = style, color = contentColor) }
content()
}
}

View file

@ -22,7 +22,7 @@ import android.content.Intent
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.material.icons.rounded.LocationOn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
@ -87,7 +87,7 @@ fun LinkedCoordinatesItem(node: Node, displayUnits: DisplayUnits = DisplayUnits.
)
},
text = stringResource(Res.string.last_position_update),
leadingIcon = Icons.Default.LocationOn,
leadingIcon = Icons.Rounded.LocationOn,
supportingText = "$ago$coordinates$elevationText",
trailingContent = Icons.AutoMirrored.Rounded.KeyboardArrowRight.icon(),
onClick = {

View file

@ -30,17 +30,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.KeyOff
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Numbers
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.SignalCellularAlt
import androidx.compose.material.icons.filled.Verified
import androidx.compose.material.icons.filled.Work
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material.icons.rounded.Numbers
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -82,6 +72,17 @@ import org.meshtastic.core.strings.supported
import org.meshtastic.core.strings.uptime
import org.meshtastic.core.strings.user_id
import org.meshtastic.core.strings.via_mqtt
import org.meshtastic.core.ui.icon.ArrowCircleUp
import org.meshtastic.core.ui.icon.ChannelUtilization
import org.meshtastic.core.ui.icon.Cloud
import org.meshtastic.core.ui.icon.History
import org.meshtastic.core.ui.icon.Hops
import org.meshtastic.core.ui.icon.KeyOff
import org.meshtastic.core.ui.icon.Lock
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Person
import org.meshtastic.core.ui.icon.Role
import org.meshtastic.core.ui.icon.Verified
import org.meshtastic.core.ui.util.formatAgo
@Composable
@ -107,7 +108,7 @@ private fun MismatchKeyWarning(modifier: Modifier = Modifier) {
Column(modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.KeyOff,
imageVector = MeshtasticIcons.KeyOff,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
)
@ -129,7 +130,6 @@ private fun MismatchKeyWarning(modifier: Modifier = Modifier) {
}
}
@Suppress("LongMethod")
@Composable
private fun MainNodeDetails(node: Node) {
Column {
@ -151,19 +151,6 @@ private fun MainNodeDetails(node: Node) {
SectionDivider()
PublicKeyItem(publicKey.toByteArray())
}
if (!node.nodeStatus.isNullOrEmpty()) {
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
Row(modifier = Modifier.fillMaxWidth()) {
InfoItem(
label = "Status",
value = node.nodeStatus!!,
icon = Icons.Default.CheckCircle,
modifier = Modifier.weight(1f),
)
}
}
}
}
@ -173,13 +160,13 @@ private fun NameAndRoleRow(node: Node) {
InfoItem(
label = stringResource(Res.string.short_name),
value = node.user.shortName.ifEmpty { "???" },
icon = Icons.Default.Person,
icon = MeshtasticIcons.Person,
modifier = Modifier.weight(1f),
)
InfoItem(
label = stringResource(Res.string.role),
value = node.user.role.name,
icon = Icons.Default.Work,
icon = MeshtasticIcons.Role,
modifier = Modifier.weight(1f),
)
}
@ -191,13 +178,13 @@ private fun NodeIdentificationRow(node: Node) {
InfoItem(
label = stringResource(Res.string.node_id),
value = DataPacket.nodeNumToDefaultId(node.num),
icon = Icons.Default.Numbers,
icon = Icons.Rounded.Numbers,
modifier = Modifier.weight(1f),
)
InfoItem(
label = stringResource(Res.string.node_number),
value = node.num.toUInt().toString(),
icon = Icons.Default.Numbers,
icon = Icons.Rounded.Numbers,
modifier = Modifier.weight(1f),
)
}
@ -209,14 +196,14 @@ private fun HearsAndHopsRow(node: Node) {
InfoItem(
label = stringResource(Res.string.node_sort_last_heard),
value = formatAgo(node.lastHeard),
icon = Icons.Default.History,
icon = MeshtasticIcons.History,
modifier = Modifier.weight(1f),
)
if (node.hopsAway >= 0) {
InfoItem(
label = stringResource(Res.string.hops_away),
value = node.hopsAway.toString(),
icon = Icons.Default.SignalCellularAlt,
icon = MeshtasticIcons.Hops,
modifier = Modifier.weight(1f),
)
} else {
@ -231,14 +218,14 @@ private fun UserAndUptimeRow(node: Node) {
InfoItem(
label = stringResource(Res.string.user_id),
value = node.user.id,
icon = Icons.Default.Person,
icon = MeshtasticIcons.Person,
modifier = Modifier.weight(1f),
)
if (node.deviceMetrics.uptimeSeconds > 0) {
InfoItem(
label = stringResource(Res.string.uptime),
value = formatUptime(node.deviceMetrics.uptimeSeconds),
icon = Icons.Default.CheckCircle,
icon = MeshtasticIcons.ArrowCircleUp,
modifier = Modifier.weight(1f),
)
} else {
@ -254,7 +241,7 @@ private fun SignalRow(node: Node) {
InfoItem(
label = stringResource(Res.string.snr),
value = "%.1f dB".format(node.snr),
icon = Icons.Default.SignalCellularAlt,
icon = MeshtasticIcons.ChannelUtilization,
modifier = Modifier.weight(1f),
)
} else {
@ -264,7 +251,7 @@ private fun SignalRow(node: Node) {
InfoItem(
label = stringResource(Res.string.rssi),
value = "%d dBm".format(node.rssi),
icon = Icons.Default.SignalCellularAlt,
icon = MeshtasticIcons.ChannelUtilization,
modifier = Modifier.weight(1f),
)
} else {
@ -280,7 +267,7 @@ private fun MqttAndVerificationRow(node: Node) {
InfoItem(
label = stringResource(Res.string.via_mqtt),
value = "Yes",
icon = Icons.Default.Cloud,
icon = MeshtasticIcons.Cloud,
modifier = Modifier.weight(1f),
)
} else {
@ -290,7 +277,7 @@ private fun MqttAndVerificationRow(node: Node) {
InfoItem(
label = stringResource(Res.string.supported),
value = "Verified",
icon = Icons.Default.Verified,
icon = MeshtasticIcons.Verified,
modifier = Modifier.weight(1f),
)
} else {
@ -327,7 +314,7 @@ private fun PublicKeyItem(publicKeyBytes: ByteArray) {
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Lock,
imageVector = MeshtasticIcons.Lock,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),

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.foundation.background
@ -32,8 +31,8 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Sort
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@ -156,13 +155,13 @@ private fun NodeFilterTextField(filterText: String, onTextChange: (String) -> Un
)
},
leadingIcon = {
Icon(Icons.Default.Search, contentDescription = stringResource(Res.string.node_filter_placeholder))
Icon(Icons.Rounded.Search, contentDescription = stringResource(Res.string.node_filter_placeholder))
},
onValueChange = onTextChange,
trailingIcon = {
if (filterText.isNotEmpty() || isFocused) {
Icon(
Icons.Default.Clear,
Icons.Rounded.Clear,
contentDescription = stringResource(Res.string.desc_node_filter_clear),
modifier =
Modifier.clickable {

View file

@ -20,12 +20,11 @@ import android.content.res.Configuration
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
@ -47,15 +46,31 @@ import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.isUnmessageableRole
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.elevation_suffix
import org.meshtastic.core.strings.unknown_username
import org.meshtastic.core.ui.component.AirQualityInfo
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.HumidityInfo
import org.meshtastic.core.ui.component.LastHeardInfo
import org.meshtastic.core.ui.component.MaterialBatteryInfo
import org.meshtastic.core.ui.component.NodeChip
import org.meshtastic.core.ui.component.NodeIdInfo
import org.meshtastic.core.ui.component.NodeKeyStatusIcon
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.SatelliteCountInfo
import org.meshtastic.core.ui.component.SignalInfo
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.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig
@ -80,7 +95,17 @@ fun NodeItem(
val isFavorite = remember(thatNode) { thatNode.isFavorite }
val isMuted = remember(thatNode) { thatNode.isMuted }
val isIgnored = thatNode.isIgnored
val longName = thatNode.user.longName.ifEmpty { stringResource(Res.string.unknown_username) }
val originalLongName = thatNode.user.longName.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) { DisplayConfig.DisplayUnits.forNumber(distanceUnits) }
val distance =
@ -115,121 +140,183 @@ fun NodeItem(
}
}
Card(modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 80.dp), colors = cardColors) {
Card(modifier = modifier.fillMaxWidth(), colors = cardColors) {
Column(
modifier =
Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick).fillMaxWidth().padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
NodeChip(node = thatNode)
NodeItemHeader(
thatNode = thatNode,
isThisNode = isThisNode,
longName = longName,
style = style,
isIgnored = isIgnored,
isFavorite = isFavorite,
isMuted = isMuted,
isUnmessageable = unmessageable,
connectionState = connectionState,
contentColor = contentColor,
)
NodeKeyStatusIcon(
hasPKC = thatNode.hasPKC,
mismatchKey = thatNode.mismatchKey,
publicKey = thatNode.user.publicKey,
modifier = Modifier.size(32.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)
NodeStatusIcons(
isThisNode = isThisNode,
isFavorite = isFavorite,
isMuted = isMuted,
isUnmessageable = unmessageable,
connectionState = connectionState,
NodeItemMetrics(thatNode = thatNode, distance = distance, system = system, contentColor = contentColor)
SignalInfo(node = thatNode, isThisNode = isThisNode, contentColor = contentColor)
NodeItemEnvironment(thatNode = thatNode, tempInFahrenheit = tempInFahrenheit, contentColor = contentColor)
NodeItemFooter(thatNode = thatNode, contentColor = contentColor)
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun NodeItemHeader(
thatNode: Node,
isThisNode: Boolean,
longName: String,
style: FontStyle,
isIgnored: Boolean,
isFavorite: Boolean,
isMuted: Boolean,
isUnmessageable: Boolean,
connectionState: ConnectionState,
contentColor: Color,
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
NodeChip(node = thatNode)
NodeKeyStatusIcon(
hasPKC = thatNode.hasPKC,
mismatchKey = thatNode.mismatchKey,
publicKey = thatNode.user.publicKey,
modifier = Modifier.size(32.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)
NodeStatusIcons(
isThisNode = isThisNode,
isFavorite = isFavorite,
isMuted = isMuted,
isUnmessageable = isUnmessageable,
connectionState = connectionState,
contentColor = contentColor,
)
}
}
@Composable
private fun NodeItemMetrics(
thatNode: Node,
distance: String?,
system: DisplayConfig.DisplayUnits,
contentColor: Color,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
if (thatNode.batteryLevel > 0 || thatNode.voltage > 0f) {
MaterialBatteryInfo(level = thatNode.batteryLevel, voltage = thatNode.voltage, 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,
system = system,
suffix = stringResource(Res.string.elevation_suffix),
contentColor = contentColor,
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
if (thatNode.batteryLevel > 0 || thatNode.voltage > 0f) {
MaterialBatteryInfo(
level = thatNode.batteryLevel,
voltage = thatNode.voltage,
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,
system = system,
suffix = stringResource(Res.string.elevation_suffix),
contentColor = contentColor,
)
val satCount = position.satsInView
if (satCount > 0) {
SatelliteCountInfo(satCount = satCount, contentColor = contentColor)
}
}
}
}
Spacer(modifier = Modifier.height(4.dp))
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
itemVerticalAlignment = Alignment.CenterVertically,
) {
SignalInfo(node = thatNode, isThisNode = isThisNode, contentColor = contentColor)
}
val telemetryStrings = thatNode.getTelemetryStrings(tempInFahrenheit)
if (telemetryStrings.isNotEmpty()) {
Spacer(modifier = Modifier.height(2.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
telemetryStrings.forEach { telemetryString ->
Text(text = telemetryString, style = MaterialTheme.typography.bodySmall, color = contentColor)
}
}
}
if (!thatNode.nodeStatus.isNullOrEmpty()) {
Spacer(modifier = Modifier.height(2.dp))
Text(
text = thatNode.nodeStatus!!,
style = MaterialTheme.typography.bodySmall,
color = contentColor,
maxLines = 2,
)
}
Spacer(modifier = Modifier.height(2.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
val labelStyle =
if (thatNode.isUnknownUser) {
MaterialTheme.typography.labelSmall.copy(fontStyle = FontStyle.Italic)
} else {
MaterialTheme.typography.labelSmall
}
Text(text = thatNode.user.hwModel.name, style = labelStyle)
Text(text = thatNode.user.role.name, style = labelStyle)
Text(text = thatNode.user.id.ifEmpty { "???" }, style = labelStyle)
val satCount = thatNode.validPosition?.satsInView ?: 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 || pax.wifi != 0) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (pax.ble != 0 || pax.wifi != 0) {
PaxcountInfo(pax = "B:${pax.ble} W:${pax.wifi}", contentColor = contentColor)
}
if (env.temperature != 0f) {
val temp =
if (tempInFahrenheit) {
"%.1f°F".format(celsiusToFahrenheit(env.temperature))
} else {
"%.1f°C".format(env.temperature)
}
TemperatureInfo(temp = temp, contentColor = contentColor)
}
if (env.relativeHumidity != 0f) {
HumidityInfo(humidity = "%.0f%%".format(env.relativeHumidity), contentColor = contentColor)
}
if (env.barometricPressure != 0f) {
PressureInfo(pressure = "%.1fhPa".format(env.barometricPressure), contentColor = contentColor)
}
if (env.soilTemperature != 0f) {
val temp =
if (tempInFahrenheit) {
"%.1f°F".format(celsiusToFahrenheit(env.soilTemperature))
} else {
"%.1f°C".format(env.soilTemperature)
}
SoilTemperatureInfo(temp = temp, contentColor = contentColor)
}
if (env.soilMoisture != 0 && env.soilTemperature != 0f) {
SoilMoistureInfo(moisture = "${env.soilMoisture}%", contentColor = contentColor)
}
if (env.voltage != 0f) {
PowerInfo(value = "%.2fV".format(env.voltage), contentColor = contentColor)
}
if (env.current != 0f) {
PowerInfo(value = "%.1fmA".format(env.current), contentColor = contentColor)
}
if (env.iaq != 0) {
AirQualityInfo(iaq = "${env.iaq}", contentColor = contentColor)
}
}
}
}
@Composable
private fun NodeItemFooter(thatNode: Node, contentColor: Color) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
HardwareInfo(hwModel = thatNode.user.hwModel.name, contentColor = contentColor)
RoleInfo(role = thatNode.user.role.name, contentColor = contentColor)
NodeIdInfo(id = thatNode.user.id.ifEmpty { "???" }, contentColor = contentColor)
}
}
@Composable
@Preview(showBackground = false, uiMode = Configuration.UI_MODE_NIGHT_YES)
fun NodeInfoSimplePreview() {

View file

@ -19,17 +19,9 @@ package org.meshtastic.feature.node.component
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.VolumeOff
import androidx.compose.material.icons.rounded.NoCell
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.twotone.Cloud
import androidx.compose.material.icons.twotone.CloudDone
import androidx.compose.material.icons.twotone.CloudOff
import androidx.compose.material.icons.twotone.CloudSync
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
@ -55,6 +47,14 @@ import org.meshtastic.core.strings.favorite
import org.meshtastic.core.strings.mute_always
import org.meshtastic.core.strings.unmessageable
import org.meshtastic.core.strings.unmonitored_or_infrastructure
import org.meshtastic.core.ui.icon.CloudDone
import org.meshtastic.core.ui.icon.CloudOffTwoTone
import org.meshtastic.core.ui.icon.CloudSync
import org.meshtastic.core.ui.icon.CloudTwoTone
import org.meshtastic.core.ui.icon.Favorite
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Unmessageable
import org.meshtastic.core.ui.icon.VolumeOff
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@ -68,29 +68,33 @@ fun NodeStatusIcons(
isFavorite: Boolean,
isMuted: Boolean,
connectionState: ConnectionState,
modifier: Modifier = Modifier,
contentColor: Color = LocalContentColor.current,
) {
Row(modifier = Modifier.padding(4.dp)) {
Row(modifier = modifier.padding(4.dp)) {
if (isThisNode) {
ThisNodeStatusBadge(connectionState)
}
if (isUnmessageable) {
StatusBadge(
imageVector = Icons.Rounded.NoCell,
imageVector = MeshtasticIcons.Unmessageable,
contentDescription = Res.string.unmessageable,
tooltipText = Res.string.unmonitored_or_infrastructure,
tint = contentColor,
)
}
if (isMuted && !isThisNode) {
StatusBadge(
imageVector = Icons.AutoMirrored.Filled.VolumeOff,
imageVector = MeshtasticIcons.VolumeOff,
contentDescription = Res.string.mute_always,
tooltipText = Res.string.mute_always,
tint = contentColor,
)
}
if (isFavorite && !isThisNode) {
StatusBadge(
imageVector = Icons.Rounded.Star,
imageVector = MeshtasticIcons.Favorite,
contentDescription = Res.string.favorite,
tooltipText = Res.string.favorite,
tint = MaterialTheme.colorScheme.StatusYellow,
@ -132,7 +136,7 @@ private fun ThisNodeStatusBadge(connectionState: ConnectionState) {
@Composable
private fun ConnectedStatusIcon() {
Icon(
imageVector = Icons.TwoTone.CloudDone,
imageVector = MeshtasticIcons.CloudDone,
contentDescription = stringResource(Res.string.connected),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusGreen,
@ -142,7 +146,7 @@ private fun ConnectedStatusIcon() {
@Composable
private fun ConnectingStatusIcon() {
Icon(
imageVector = Icons.TwoTone.CloudSync,
imageVector = MeshtasticIcons.CloudSync,
contentDescription = stringResource(Res.string.connecting),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusOrange,
@ -152,7 +156,7 @@ private fun ConnectingStatusIcon() {
@Composable
private fun DisconnectedStatusIcon() {
Icon(
imageVector = Icons.TwoTone.CloudOff,
imageVector = MeshtasticIcons.CloudOffTwoTone,
contentDescription = stringResource(Res.string.disconnected),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusRed,
@ -162,7 +166,7 @@ private fun DisconnectedStatusIcon() {
@Composable
private fun DeviceSleepStatusIcon() {
Icon(
imageVector = Icons.TwoTone.Cloud,
imageVector = MeshtasticIcons.CloudTwoTone,
contentDescription = stringResource(Res.string.device_sleeping),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusYellow,
@ -175,21 +179,19 @@ private fun StatusBadge(
imageVector: ImageVector,
contentDescription: StringResource,
tooltipText: StringResource,
tint: Color = Color.Unspecified,
tint: Color = LocalContentColor.current,
) {
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = { PlainTooltip { Text(stringResource(tooltipText)) } },
state = rememberTooltipState(),
) {
IconButton(onClick = {}, modifier = Modifier.size(24.dp)) {
Icon(
imageVector = imageVector,
contentDescription = stringResource(contentDescription),
modifier = Modifier.size(24.dp),
tint = tint,
)
}
Icon(
imageVector = imageVector,
contentDescription = stringResource(contentDescription),
modifier = Modifier.size(24.dp),
tint = tint,
)
}
}

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.foundation.layout.Column
@ -25,7 +24,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
@ -87,7 +86,7 @@ fun NotesSection(node: Node, onSaveNotes: (Int, String) -> Unit, modifier: Modif
},
enabled = edited,
) {
Icon(imageVector = Icons.Default.Save, contentDescription = stringResource(Res.string.save))
Icon(imageVector = Icons.Rounded.Save, contentDescription = stringResource(Res.string.save))
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),

View file

@ -29,9 +29,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Explore
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.SocialDistance
import androidx.compose.material.icons.rounded.Explore
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material.icons.rounded.SocialDistance
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@ -132,7 +132,7 @@ private fun PositionMap(node: Node, distance: String?) {
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.Default.SocialDistance, null, Modifier.size(16.dp))
Icon(Icons.Rounded.SocialDistance, null, Modifier.size(16.dp))
Spacer(Modifier.width(6.dp))
Text(distance, style = MaterialTheme.typography.labelLarge)
}
@ -163,7 +163,7 @@ private fun PositionActionButtons(
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
) {
Icon(Icons.Default.LocationOn, null, Modifier.size(18.dp))
Icon(Icons.Rounded.LocationOn, null, Modifier.size(18.dp))
Spacer(Modifier.width(6.dp))
Text(
text = stringResource(Res.string.exchange_position),
@ -179,7 +179,7 @@ private fun PositionActionButtons(
modifier = Modifier.weight(COMPASS_BUTTON_WEIGHT),
shape = MaterialTheme.shapes.large,
) {
Icon(Icons.Default.Explore, null, Modifier.size(18.dp))
Icon(Icons.Rounded.Explore, null, Modifier.size(18.dp))
Spacer(Modifier.width(6.dp))
Text(
text = stringResource(Res.string.open_compass),

View file

@ -20,8 +20,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Power
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -47,16 +47,16 @@ internal fun PowerMetrics(node: Node) {
buildList {
with(node.powerMetrics) {
if (ch1Voltage != 0f) {
add(VectorMetricInfo(Res.string.channel_1, "%.2fV".format(ch1Voltage), Icons.Default.Bolt))
add(VectorMetricInfo(Res.string.channel_1, "%.1fmA".format(ch1Current), Icons.Default.Power))
add(VectorMetricInfo(Res.string.channel_1, "%.2fV".format(ch1Voltage), Icons.Rounded.Bolt))
add(VectorMetricInfo(Res.string.channel_1, "%.1fmA".format(ch1Current), Icons.Rounded.Power))
}
if (ch2Voltage != 0f) {
add(VectorMetricInfo(Res.string.channel_2, "%.2fV".format(ch2Voltage), Icons.Default.Bolt))
add(VectorMetricInfo(Res.string.channel_2, "%.1fmA".format(ch2Current), Icons.Default.Power))
add(VectorMetricInfo(Res.string.channel_2, "%.2fV".format(ch2Voltage), Icons.Rounded.Bolt))
add(VectorMetricInfo(Res.string.channel_2, "%.1fmA".format(ch2Current), Icons.Rounded.Power))
}
if (ch3Voltage != 0f) {
add(VectorMetricInfo(Res.string.channel_3, "%.2fV".format(ch3Voltage), Icons.Default.Bolt))
add(VectorMetricInfo(Res.string.channel_3, "%.1fmA".format(ch3Current), Icons.Default.Power))
add(VectorMetricInfo(Res.string.channel_3, "%.2fV".format(ch3Voltage), Icons.Rounded.Bolt))
add(VectorMetricInfo(Res.string.channel_3, "%.1fmA".format(ch3Current), Icons.Rounded.Power))
}
}
}

View file

@ -23,13 +23,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Air
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.StackedLineChart
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconButton
@ -64,6 +57,14 @@ import org.meshtastic.core.strings.request_local_stats
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.Groups
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Person
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.icon.Speed
import org.meshtastic.core.ui.icon.Temperature
import org.meshtastic.feature.node.model.LogsType
import org.meshtastic.feature.node.model.MetricsState
import org.meshtastic.feature.node.model.NodeDetailAction
@ -119,7 +120,7 @@ private fun rememberTelemetricFeatures(
listOf(
TelemetricFeature(
titleRes = Res.string.userinfo,
icon = Icons.Default.Person,
icon = MeshtasticIcons.Person,
requestAction = { NodeMenuAction.RequestUserInfo(it) },
),
TelemetricFeature(
@ -131,7 +132,7 @@ private fun rememberTelemetricFeatures(
),
TelemetricFeature(
titleRes = Res.string.neighbor_info,
icon = Icons.Default.Groups,
icon = MeshtasticIcons.Groups,
requestAction = { NodeMenuAction.RequestNeighborInfo(it) },
isVisible = { it.capabilities.canRequestNeighborInfo },
cooldownTimestamp = lastRequestNeighborsTime,
@ -145,7 +146,7 @@ private fun rememberTelemetricFeatures(
),
TelemetricFeature(
titleRes = LogsType.ENVIRONMENT.titleRes,
icon = Icons.Default.Air,
icon = MeshtasticIcons.Temperature,
requestAction = { NodeMenuAction.RequestTelemetry(it, TelemetryType.ENVIRONMENT) },
logsType = LogsType.ENVIRONMENT,
content = { EnvironmentMetrics(it, metricsState.displayUnits, metricsState.isFahrenheit) },
@ -153,7 +154,7 @@ private fun rememberTelemetricFeatures(
),
TelemetricFeature(
titleRes = Res.string.request_air_quality_metrics,
icon = Icons.Default.Air,
icon = MeshtasticIcons.AirQuality,
requestAction = { NodeMenuAction.RequestTelemetry(it, TelemetryType.AIR_QUALITY) },
),
TelemetricFeature(
@ -166,7 +167,7 @@ private fun rememberTelemetricFeatures(
),
TelemetricFeature(
titleRes = Res.string.request_local_stats,
icon = Icons.Default.Speed,
icon = MeshtasticIcons.Speed,
requestAction = { NodeMenuAction.RequestTelemetry(it, TelemetryType.LOCAL_STATS) },
),
TelemetricFeature(
@ -226,7 +227,7 @@ private fun FeatureRow(node: Node, feature: TelemetricFeature, hasLogs: Boolean,
},
) {
Icon(
Icons.Default.StackedLineChart,
MeshtasticIcons.Chart,
contentDescription = logsDescription,
modifier = Modifier.size(IconButtonDefaults.mediumIconSize),
tint = MaterialTheme.colorScheme.primary,
@ -252,7 +253,7 @@ private fun FeatureRow(node: Node, feature: TelemetricFeature, hasLogs: Boolean,
cooldownDuration = feature.cooldownDuration,
) {
Icon(
imageVector = Icons.Default.Refresh,
imageVector = MeshtasticIcons.Refresh,
contentDescription = requestDescription,
tint = MaterialTheme.colorScheme.primary,
)

View file

@ -0,0 +1,179 @@
/*
* 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
* 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 <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Air
import androidx.compose.material.icons.rounded.ElectricBolt
import androidx.compose.material.icons.rounded.Fingerprint
import androidx.compose.material.icons.rounded.Grass
import androidx.compose.material.icons.rounded.People
import androidx.compose.material.icons.rounded.Router
import androidx.compose.material.icons.rounded.Thermostat
import androidx.compose.material.icons.rounded.WaterDrop
import androidx.compose.material.icons.rounded.Work
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.node_id
import org.meshtastic.core.strings.pax_metrics_log
import org.meshtastic.core.strings.role
@Composable
fun TemperatureInfo(
temp: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Thermostat,
contentDescription = stringResource(Res.string.env_metrics_log),
text = temp,
contentColor = contentColor,
)
}
@Composable
fun HumidityInfo(
humidity: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.WaterDrop,
contentDescription = stringResource(Res.string.env_metrics_log),
text = humidity,
contentColor = contentColor,
)
}
@Composable
fun SoilTemperatureInfo(
temp: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Grass,
contentDescription = stringResource(Res.string.env_metrics_log),
text = temp,
contentColor = contentColor,
)
}
@Composable
fun SoilMoistureInfo(
moisture: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Grass,
contentDescription = stringResource(Res.string.env_metrics_log),
text = moisture,
contentColor = contentColor,
)
}
@Composable
fun PaxcountInfo(
pax: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.People,
contentDescription = stringResource(Res.string.pax_metrics_log),
text = pax,
contentColor = contentColor,
)
}
@Composable
fun AirQualityInfo(
iaq: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Air,
contentDescription = stringResource(Res.string.env_metrics_log),
text = iaq,
contentColor = contentColor,
)
}
@Composable
fun PowerInfo(value: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.ElectricBolt,
contentDescription = stringResource(Res.string.env_metrics_log),
text = value,
contentColor = contentColor,
)
}
@Composable
fun HardwareInfo(
hwModel: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Router,
contentDescription = "Hardware Model",
text = hwModel,
style = MaterialTheme.typography.labelSmall,
contentColor = contentColor,
)
}
@Composable
fun RoleInfo(role: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Work,
contentDescription = stringResource(Res.string.role),
text = role,
style = MaterialTheme.typography.labelSmall,
contentColor = contentColor,
)
}
@Composable
fun NodeIdInfo(id: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Fingerprint,
contentDescription = stringResource(Res.string.node_id),
text = id,
style = MaterialTheme.typography.labelSmall,
contentColor = contentColor,
)
}

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.metrics
import android.graphics.Paint
@ -32,7 +31,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -258,7 +257,7 @@ fun Legend(legendData: List<LegendData>, displayInfoIcon: Boolean = true, prompt
if (displayInfoIcon) {
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Default.Info,
imageVector = Icons.Rounded.Info,
modifier = Modifier.clickable { promptInfoDialog() },
contentDescription = stringResource(Res.string.info),
)

View file

@ -33,9 +33,8 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@ -78,6 +77,8 @@ import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MaterialBatteryInfo
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.GraphColors.Cyan
import org.meshtastic.core.ui.theme.GraphColors.Green
@ -158,10 +159,7 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
actions = {
if (!state.isLocal) {
IconButton(onClick = { viewModel.requestTelemetry(TelemetryType.DEVICE) }) {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
},

View file

@ -28,8 +28,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -75,6 +73,8 @@ import org.meshtastic.core.ui.component.IndoorAirQuality
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.metrics.CommonCharts.DATE_TIME_FORMAT
import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
@ -134,7 +134,7 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNa
if (!state.isLocal) {
IconButton(onClick = { viewModel.requestTelemetry(TelemetryType.ENVIRONMENT) }) {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Refresh,
imageVector = MeshtasticIcons.Refresh,
contentDescription = null,
)
}
@ -339,27 +339,6 @@ private fun GasCompositionDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics
}
}
}
// These are in a differnt proto ...
// envMetrics.co2?.let { co2 ->
// Spacer(modifier = Modifier.height(4.dp))
// Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
// Text(
// text = "%s %.0f ppm".format(stringResource(Res.string.co2), co2),
// color = MaterialTheme.colorScheme.onSurface,
// fontSize = MaterialTheme.typography.labelLarge.fontSize,
// )
// }
// }
// envMetrics.tvoc?.let { tvoc ->
// Spacer(modifier = Modifier.height(4.dp))
// Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
// Text(
// text = "%s %.0f ppb".format(stringResource(Res.string.tvoc), tvoc),
// color = MaterialTheme.colorScheme.onSurface,
// fontSize = MaterialTheme.typography.labelLarge.fontSize,
// )
// }
// }
}
@Composable

View file

@ -30,9 +30,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DataArray
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
@ -69,6 +66,9 @@ import org.meshtastic.core.strings.load_indexed
import org.meshtastic.core.strings.uptime
import org.meshtastic.core.strings.user_string
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.icon.DataArray
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.metrics.CommonCharts.DATE_TIME_FORMAT
@ -105,10 +105,7 @@ fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), o
actions = {
if (!state.isLocal) {
IconButton(onClick = { metricsViewModel.requestTelemetry(TelemetryType.HOST) }) {
Icon(
imageVector = androidx.compose.material.icons.Icons.Default.Refresh,
contentDescription = null,
)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
},
@ -136,7 +133,7 @@ fun HostMetricsItem(modifier: Modifier = Modifier, telemetry: TelemetryProtos.Te
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
) {
Row(modifier = Modifier.padding(16.dp)) {
Icon(imageVector = Icons.Default.DataArray, contentDescription = null, modifier = Modifier.width(24.dp))
Icon(imageVector = MeshtasticIcons.DataArray, contentDescription = null, modifier = Modifier.width(24.dp))
Spacer(modifier = Modifier.width(16.dp))
SelectionContainer {
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {

View file

@ -33,8 +33,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -69,11 +67,16 @@ import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.ble_devices
import org.meshtastic.core.strings.no_pax_metrics_logs
import org.meshtastic.core.strings.pax
import org.meshtastic.core.strings.pax_metrics_log
import org.meshtastic.core.strings.uptime
import org.meshtastic.core.strings.wifi_devices
import org.meshtastic.core.ui.component.IconInfo
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Paxcount
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.model.TimeFrame
import org.meshtastic.proto.PaxcountProtos
@ -229,7 +232,7 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNav
actions = {
if (!state.isLocal) {
IconButton(onClick = { metricsViewModel.requestTelemetry(TelemetryType.PAX) }) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
},
@ -331,6 +334,21 @@ fun unescapeProtoString(escaped: String): ByteArray {
return out.toByteArray()
}
@Composable
fun PaxcountInfo(
pax: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Paxcount,
contentDescription = stringResource(Res.string.pax_metrics_log),
text = pax,
contentColor = contentColor,
)
}
@Composable
fun PaxMetricsItem(log: MeshLog, pax: PaxcountProtos.Paxcount, dateFormat: DateFormat) {
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {

View file

@ -34,10 +34,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -80,6 +76,10 @@ import org.meshtastic.core.strings.save
import org.meshtastic.core.strings.speed
import org.meshtastic.core.strings.timestamp
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.icon.Delete
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.icon.Save
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.util.formatPositionTime
import org.meshtastic.feature.node.detail.NodeRequestEffect
@ -157,13 +157,13 @@ private fun ActionButtons(
enabled = clearButtonEnabled,
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error),
) {
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(Res.string.clear))
Icon(imageVector = MeshtasticIcons.Delete, contentDescription = stringResource(Res.string.clear))
Spacer(Modifier.width(8.dp))
Text(text = stringResource(Res.string.clear))
}
OutlinedButton(modifier = Modifier.weight(1f), onClick = onSave, enabled = saveButtonEnabled) {
Icon(imageVector = Icons.Default.Save, contentDescription = stringResource(Res.string.save))
Icon(imageVector = MeshtasticIcons.Save, contentDescription = stringResource(Res.string.save))
Spacer(Modifier.width(8.dp))
Text(text = stringResource(Res.string.save))
}
@ -207,7 +207,7 @@ fun PositionLogScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateU
actions = {
if (!state.isLocal) {
IconButton(onClick = { viewModel.requestPosition() }) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
},

View file

@ -34,7 +34,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -161,7 +161,7 @@ fun PowerMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigate
if (!state.isLocal) {
IconButton(onClick = { viewModel.requestTelemetry(TelemetryType.POWER) }) {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Refresh,
imageVector = Icons.Rounded.Refresh,
contentDescription = null,
)
}

View file

@ -34,8 +34,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -74,6 +72,8 @@ import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.component.SnrAndRssi
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.metrics.CommonCharts.DATE_TIME_FORMAT
import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
@ -133,7 +133,7 @@ fun SignalMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
if (!state.isLocal) {
IconButton(onClick = { viewModel.requestTelemetry(TelemetryType.LOCAL_STATS) }) {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Refresh,
imageVector = MeshtasticIcons.Refresh,
contentDescription = null,
)
}

View file

@ -32,12 +32,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Group
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.PersonOff
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@ -92,6 +86,12 @@ import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.SNR_FAIR_THRESHOLD
import org.meshtastic.core.ui.component.SNR_GOOD_THRESHOLD
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.icon.Delete
import org.meshtastic.core.ui.icon.Group
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.PersonOff
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.icon.Route
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
@ -163,7 +163,7 @@ fun TracerouteLogScreen(
onClick = { viewModel.requestTraceroute() },
cooldownTimestamp = lastTracerouteTime,
) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
},
@ -329,7 +329,7 @@ private fun DeleteItem(onClick: () -> Unit) {
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Delete,
imageVector = MeshtasticIcons.Delete,
contentDescription = stringResource(Res.string.delete),
tint = MaterialTheme.colorScheme.error,
)
@ -357,23 +357,23 @@ private fun TracerouteItem(icon: ImageVector, text: String, modifier: Modifier =
@Composable
private fun MeshProtos.RouteDiscovery?.getTextAndIcon(): Pair<String, ImageVector> = when {
this == null -> {
stringResource(Res.string.routing_error_no_response) to Icons.Default.PersonOff
stringResource(Res.string.routing_error_no_response) to MeshtasticIcons.PersonOff
}
// A direct route means the sender and receiver are the only two nodes in the route.
routeCount <= 2 && routeBackCount <= 2 -> { // also check routeBackCount for direct to be more robust
stringResource(Res.string.traceroute_direct) to Icons.Default.Group
stringResource(Res.string.traceroute_direct) to MeshtasticIcons.Group
}
routeCount == routeBackCount -> {
val hops = routeCount - 2
pluralStringResource(Res.plurals.traceroute_hops, hops, hops) to Icons.Default.Groups
pluralStringResource(Res.plurals.traceroute_hops, hops, hops) to MeshtasticIcons.Route
}
else -> {
// Asymmetric route
val towards = maxOf(0, routeCount - 2)
val back = maxOf(0, routeBackCount - 2)
stringResource(Res.string.traceroute_diff, towards, back) to Icons.Default.Groups
stringResource(Res.string.traceroute_diff, towards, back) to MeshtasticIcons.Route
}
}
@ -424,5 +424,5 @@ private fun TracerouteItemPreview() {
System.currentTimeMillis(),
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
)
AppTheme { TracerouteItem(icon = Icons.Default.Group, text = "$time - Direct") }
AppTheme { TracerouteItem(icon = MeshtasticIcons.Group, text = "$time - Direct") }
}

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.metrics
import androidx.compose.foundation.layout.Arrangement
@ -24,8 +23,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Route
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -52,6 +49,8 @@ import org.meshtastic.core.strings.traceroute_outgoing_route
import org.meshtastic.core.strings.traceroute_return_route
import org.meshtastic.core.strings.traceroute_showing_nodes
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Route
import org.meshtastic.core.ui.theme.TracerouteColors
import org.meshtastic.feature.map.MapView
import org.meshtastic.feature.map.model.TracerouteOverlay
@ -172,7 +171,7 @@ private fun TracerouteNodeCount(modifier: Modifier = Modifier, shown: Int, total
private fun LegendRow(color: Color, label: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Route,
imageVector = MeshtasticIcons.Route,
contentDescription = null,
tint = color,
modifier = Modifier.padding(end = 8.dp).size(18.dp),

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,19 +14,16 @@
* 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.model
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChargingStation
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Map
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Route
import androidx.compose.material.icons.filled.SignalCellularAlt
import androidx.compose.material.icons.filled.Thermostat
import androidx.compose.material.icons.rounded.ChargingStation
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material.icons.rounded.Map
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Power
import androidx.compose.material.icons.rounded.SignalCellularAlt
import androidx.compose.material.icons.rounded.Thermostat
import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.navigation.NodeDetailRoutes
@ -41,15 +38,18 @@ import org.meshtastic.core.strings.position_log
import org.meshtastic.core.strings.power_metrics_log
import org.meshtastic.core.strings.sig_metrics_log
import org.meshtastic.core.strings.traceroute_log
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Paxcount
import org.meshtastic.core.ui.icon.Route
enum class LogsType(val titleRes: StringResource, val icon: ImageVector, val routeFactory: (Int) -> Route) {
DEVICE(Res.string.device_metrics_log, Icons.Default.ChargingStation, { NodeDetailRoutes.DeviceMetrics(it) }),
NODE_MAP(Res.string.node_map, Icons.Default.Map, { NodeDetailRoutes.NodeMap(it) }),
POSITIONS(Res.string.position_log, Icons.Default.LocationOn, { NodeDetailRoutes.PositionLog(it) }),
ENVIRONMENT(Res.string.env_metrics_log, Icons.Default.Thermostat, { NodeDetailRoutes.EnvironmentMetrics(it) }),
SIGNAL(Res.string.sig_metrics_log, Icons.Default.SignalCellularAlt, { NodeDetailRoutes.SignalMetrics(it) }),
POWER(Res.string.power_metrics_log, Icons.Default.Power, { NodeDetailRoutes.PowerMetrics(it) }),
TRACEROUTE(Res.string.traceroute_log, Icons.Default.Route, { NodeDetailRoutes.TracerouteLog(it) }),
HOST(Res.string.host_metrics_log, Icons.Default.Memory, { NodeDetailRoutes.HostMetricsLog(it) }),
PAX(Res.string.pax_metrics_log, Icons.Default.People, { NodeDetailRoutes.PaxMetrics(it) }),
DEVICE(Res.string.device_metrics_log, Icons.Rounded.ChargingStation, { NodeDetailRoutes.DeviceMetrics(it) }),
NODE_MAP(Res.string.node_map, Icons.Rounded.Map, { NodeDetailRoutes.NodeMap(it) }),
POSITIONS(Res.string.position_log, Icons.Rounded.LocationOn, { NodeDetailRoutes.PositionLog(it) }),
ENVIRONMENT(Res.string.env_metrics_log, Icons.Rounded.Thermostat, { NodeDetailRoutes.EnvironmentMetrics(it) }),
SIGNAL(Res.string.sig_metrics_log, Icons.Rounded.SignalCellularAlt, { NodeDetailRoutes.SignalMetrics(it) }),
POWER(Res.string.power_metrics_log, Icons.Rounded.Power, { NodeDetailRoutes.PowerMetrics(it) }),
TRACEROUTE(Res.string.traceroute_log, MeshtasticIcons.Route, { NodeDetailRoutes.TracerouteLog(it) }),
HOST(Res.string.host_metrics_log, Icons.Rounded.Memory, { NodeDetailRoutes.HostMetricsLog(it) }),
PAX(Res.string.pax_metrics_log, MeshtasticIcons.Paxcount, { NodeDetailRoutes.PaxMetrics(it) }),
}