diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dcdcd50d2..1bcfa30ed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -225,14 +225,10 @@ dependencies { } val googleServiceKeywords = listOf("crashlytics", "google", "datadog") + tasks.configureEach { if ( - googleServiceKeywords.any { - name.contains( - it, - ignoreCase = true - ) - } && name.contains("fdroid", ignoreCase = true) + googleServiceKeywords.any { name.contains(it, ignoreCase = true) } && name.contains("fdroid", ignoreCase = true) ) { project.logger.lifecycle("Disabling task for F-Droid: $name") enabled = false diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/SignalInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/SignalInfo.kt index c41958236..7f4223a58 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/SignalInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/SignalInfo.kt @@ -17,24 +17,33 @@ package com.geeksville.mesh.ui.common.components -import androidx.compose.foundation.layout.Column +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.res.stringResource 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 com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider import org.meshtastic.core.database.model.Node import org.meshtastic.core.strings.R -import org.meshtastic.core.ui.component.NodeSignalQuality +import org.meshtastic.core.ui.component.Rssi +import org.meshtastic.core.ui.component.Snr +import org.meshtastic.core.ui.component.determineSignalQuality 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) { val text = @@ -60,19 +69,42 @@ fun SignalInfo(modifier: Modifier = Modifier, node: Node, isThisNode: Boolean) { } .joinToString(" ") } - Column { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { if (text.isNotEmpty()) { - Text( - modifier = modifier, - text = text, - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.bodySmall.fontSize, - ) + Text(text = text, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelSmall) } /* We only know the Signal Quality from direct nodes aka 0 hop. */ if (node.hopsAway <= 0) { if (node.snr < MAX_VALID_SNR && node.rssi < MAX_VALID_RSSI) { - NodeSignalQuality(node.snr, node.rssi) + val quality = determineSignalQuality(node.snr, node.rssi) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Snr(node.snr) + Rssi(node.rssi) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = quality.imageVector, + contentDescription = stringResource(R.string.signal_quality), + tint = quality.color.invoke(), + ) + Text( + text = "${stringResource(R.string.signal)} ${stringResource(quality.nameRes)}", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + ) + } } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt index 9701392c4..adcd14593 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt @@ -174,8 +174,8 @@ internal fun MessageItem( if (!message.fromLocal) { if (message.hopsAway == 0) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Snr(message.snr, fontSize = MaterialTheme.typography.labelSmall.fontSize) - Rssi(message.rssi, fontSize = MaterialTheme.typography.labelSmall.fontSize) + Snr(message.snr) + Rssi(message.rssi) } } else { Text( diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt index 6e3925af2..f449db185 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt @@ -70,7 +70,7 @@ import com.geeksville.mesh.util.GraphUtil import com.geeksville.mesh.util.GraphUtil.createPath import com.geeksville.mesh.util.GraphUtil.plotPoint import org.meshtastic.core.strings.R -import org.meshtastic.core.ui.component.BatteryInfo +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.theme.AppTheme @@ -319,7 +319,7 @@ private fun DeviceMetricsCard(telemetry: Telemetry) { fontSize = MaterialTheme.typography.labelLarge.fontSize, ) - BatteryInfo(batteryLevel = deviceMetrics.batteryLevel, voltage = deviceMetrics.voltage) + MaterialBatteryInfo(level = deviceMetrics.batteryLevel, voltage = deviceMetrics.voltage) } Spacer(modifier = Modifier.height(4.dp)) diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt index a31b73de3..7d194b141 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt @@ -18,7 +18,6 @@ package com.geeksville.mesh.ui.node import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer @@ -55,7 +54,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -73,7 +71,7 @@ import org.meshtastic.core.strings.R import org.meshtastic.core.ui.component.rememberTimeTickWithLifecycle import org.meshtastic.core.ui.theme.StatusColors.StatusRed -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeDetails: (Int) -> Unit) { @@ -172,11 +170,12 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD onConfirmRemove = { nodesViewModel.removeNode(it.num) }, ) + var expanded by remember { mutableStateOf(false) } + Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { - var showContextMenu by remember { mutableStateOf(false) } val longClick = if (node.num != ourNode?.num) { - { showContextMenu = true } + { expanded = true } } else { null } @@ -193,14 +192,16 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD isConnected = connectionState.isConnected(), ) val isThisNode = remember(node) { ourNode?.num == node.num } - ContextMenu( - expanded = !isThisNode && showContextMenu, - node = node, - onClickFavorite = { displayFavoriteDialog = true }, - onClickIgnore = { displayIgnoreDialog = true }, - onClickRemove = { displayRemoveDialog = true }, - onDismiss = { showContextMenu = false }, - ) + if (!isThisNode) { + ContextMenu( + expanded = expanded, + node = node, + onClickFavorite = { displayFavoriteDialog = true }, + onClickIgnore = { displayIgnoreDialog = true }, + onClickRemove = { displayRemoveDialog = true }, + onDismiss = { expanded = false }, + ) + } } } item { Spacer(modifier = Modifier.height(88.dp)) } @@ -218,7 +219,7 @@ private fun ContextMenu( onClickRemove: (Node) -> Unit, onDismiss: () -> Unit, ) { - DropdownMenu(expanded = expanded, onDismissRequest = onDismiss, offset = DpOffset(x = 0.dp, y = 8.dp)) { + DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { val isFavorite = node.isFavorite val isIgnored = node.isIgnored diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/DistanceInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/DistanceInfo.kt new file mode 100644 index 000000000..8ef68fd94 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/DistanceInfo.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.ui.node.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.SocialDistance +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import org.meshtastic.core.strings.R +import org.meshtastic.core.ui.theme.AppTheme + +@Composable +fun DistanceInfo(distance: String, modifier: Modifier = Modifier) { + IconInfo( + modifier = modifier, + icon = Icons.Rounded.SocialDistance, + contentDescription = stringResource(R.string.distance), + text = distance, + ) +} + +@PreviewLightDark +@Composable +private fun DistanceInfoPreview() { + AppTheme { DistanceInfo(distance = "423 mi.") } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/ElevationInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/ElevationInfo.kt index bfbdb1015..acbb58aff 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/ElevationInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/ElevationInfo.kt @@ -18,24 +18,30 @@ package com.geeksville.mesh.ui.node.components import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits import org.meshtastic.core.model.util.metersIn import org.meshtastic.core.model.util.toString +import org.meshtastic.core.strings.R +import org.meshtastic.core.ui.icon.Elevation +import org.meshtastic.core.ui.icon.MeshtasticIcons @Composable -fun ElevationInfo(modifier: Modifier = Modifier, altitude: Int, system: DisplayUnits, suffix: String) { - val annotatedString = buildAnnotatedString { - append(altitude.metersIn(system).toString(system)) - MaterialTheme.typography.labelSmall.toSpanStyle().let { style -> withStyle(style) { append(" $suffix") } } - } - - Text(modifier = modifier, fontSize = MaterialTheme.typography.labelLarge.fontSize, text = annotatedString) +fun ElevationInfo( + modifier: Modifier = Modifier, + altitude: Int, + system: DisplayUnits, + suffix: String = stringResource(R.string.elevation_suffix), +) { + IconInfo( + modifier = modifier, + icon = MeshtasticIcons.Elevation, + contentDescription = stringResource(R.string.altitude), + text = altitude.metersIn(system).toString(system) + " " + suffix, + ) } @Composable diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/IconInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/IconInfo.kt new file mode 100644 index 000000000..5d97ba9a9 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/IconInfo.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.ui.node.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +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.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.meshtastic.core.ui.icon.Elevation +import org.meshtastic.core.ui.icon.MeshtasticIcons + +private const val SIZE_ICON = 20 + +@Composable +fun IconInfo( + icon: ImageVector, + contentDescription: String, + modifier: Modifier = Modifier, + text: String? = null, + content: @Composable () -> Unit = {}, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + Icon( + modifier = Modifier.size(SIZE_ICON.dp), + imageVector = icon, + contentDescription = contentDescription, + tint = MaterialTheme.colorScheme.onSurface, + ) + text?.let { + Text(text = it, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurface) + } + content() + } +} + +@Composable +@Preview +private fun IconInfoPreview() { + MaterialTheme { + IconInfo(icon = MeshtasticIcons.Elevation, contentDescription = "Elevation", content = { Text(text = "100") }) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/LastHeardInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/LastHeardInfo.kt index 27aa8adc4..d055ca2d5 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/LastHeardInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/LastHeardInfo.kt @@ -17,37 +17,24 @@ package com.geeksville.mesh.ui.node.components -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height -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.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.dp import com.geeksville.mesh.R import org.meshtastic.core.model.util.formatAgo import org.meshtastic.core.ui.theme.AppTheme @Composable fun LastHeardInfo(modifier: Modifier = Modifier, lastHeard: Int, currentTimeMillis: Long) { - Row( + IconInfo( modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - Icon( - modifier = Modifier.height(18.dp), - imageVector = ImageVector.vectorResource(id = R.drawable.ic_antenna_24), - contentDescription = null, - ) - Text(text = formatAgo(lastHeard, currentTimeMillis), fontSize = MaterialTheme.typography.labelLarge.fontSize) - } + icon = ImageVector.vectorResource(id = R.drawable.ic_antenna_24), + contentDescription = stringResource(org.meshtastic.core.strings.R.string.node_sort_last_heard), + text = formatAgo(lastHeard, currentTimeMillis), + ) } @PreviewLightDark diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeChip.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeChip.kt index 3e17388e5..bd1241c2e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeChip.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeChip.kt @@ -17,7 +17,9 @@ package com.geeksville.mesh.ui.node.components +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth @@ -42,34 +44,52 @@ import com.geeksville.mesh.TelemetryProtos import org.meshtastic.core.database.model.Node @Composable -fun NodeChip(modifier: Modifier = Modifier, node: Node, onClick: (Node) -> Unit = {}) { +fun NodeChip( + modifier: Modifier = Modifier, + node: Node, + onClick: (Node) -> Unit = {}, + onLongClick: (() -> Unit)? = {}, + interactionSource: MutableInteractionSource? = null, +) { val isIgnored = node.isIgnored val (textColor, nodeColor) = node.colors - val inputChipInteractionSource = remember { MutableInteractionSource() } - ElevatedAssistChip( - modifier = - modifier.width(IntrinsicSize.Min).defaultMinSize(minWidth = 72.dp).semantics { - contentDescription = node.user.shortName.ifEmpty { "Node" } - }, - elevation = AssistChipDefaults.elevatedAssistChipElevation(), - colors = - AssistChipDefaults.elevatedAssistChipColors( - containerColor = Color(nodeColor), - labelColor = Color(textColor), - ), - label = { - Text( - modifier = Modifier.fillMaxWidth(), - text = node.user.shortName.ifEmpty { "???" }, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - textDecoration = TextDecoration.LineThrough.takeIf { isIgnored }, - textAlign = TextAlign.Center, - maxLines = 1, - ) - }, - onClick = { onClick(node) }, - interactionSource = inputChipInteractionSource, - ) + val inputChipInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + Box(modifier = modifier) { + ElevatedAssistChip( + modifier = + Modifier.width(IntrinsicSize.Min).defaultMinSize(minWidth = 72.dp).semantics { + contentDescription = node.user.shortName.ifEmpty { "Node" } + }, + elevation = AssistChipDefaults.elevatedAssistChipElevation(), + colors = + AssistChipDefaults.elevatedAssistChipColors( + containerColor = Color(nodeColor), + labelColor = Color(textColor), + ), + label = { + Text( + modifier = Modifier.fillMaxWidth(), + text = node.user.shortName.ifEmpty { "???" }, + fontSize = MaterialTheme.typography.labelLarge.fontSize, + textDecoration = TextDecoration.LineThrough.takeIf { isIgnored }, + textAlign = TextAlign.Center, + maxLines = 1, + ) + }, + onClick = {}, + interactionSource = inputChipInteractionSource, + ) + Box( + modifier = + Modifier.matchParentSize() + .combinedClickable( + onLongClick = onLongClick, + onClick = { onClick(node) }, + interactionSource = inputChipInteractionSource, + indication = null, + ), + ) + } } @Suppress("MagicNumber") diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeItem.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeItem.kt index b9444d9fc..25cf1dd98 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeItem.kt @@ -19,8 +19,10 @@ package com.geeksville.mesh.ui.node.components import android.content.res.Configuration import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize @@ -28,14 +30,15 @@ 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.foundation.layout.width import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -53,9 +56,10 @@ import org.meshtastic.core.database.model.Node import org.meshtastic.core.database.model.isUnmessageableRole import org.meshtastic.core.model.util.toDistanceString import org.meshtastic.core.strings.R -import org.meshtastic.core.ui.component.BatteryInfo +import org.meshtastic.core.ui.component.MaterialBatteryInfo import org.meshtastic.core.ui.theme.AppTheme +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun NodeItem( @@ -77,13 +81,7 @@ fun NodeItem( val distance = remember(thisNode, thatNode) { thisNode?.distance(thatNode)?.takeIf { it > 0 }?.toDistanceString(system) } - val style = - if (thatNode.isUnknownUser) { - LocalTextStyle.current.copy(fontStyle = FontStyle.Italic) - } else { - LocalTextStyle.current - } - + var contentColor = MaterialTheme.colorScheme.onSurface val cardColors = if (isThisNode) { thisNode?.colors?.second @@ -92,10 +90,17 @@ fun NodeItem( } ?.let { val containerColor = Color(it).copy(alpha = 0.2f) - CardDefaults.cardColors() - .copy(containerColor = containerColor, contentColor = contentColorFor(containerColor)) + contentColor = contentColorFor(containerColor) + CardDefaults.cardColors().copy(containerColor = containerColor, contentColor = contentColor) } ?: (CardDefaults.cardColors()) + val style = + if (thatNode.isUnknownUser) { + LocalTextStyle.current.copy(fontStyle = FontStyle.Italic) + } else { + LocalTextStyle.current + } + val unmessageable = remember(thatNode) { when { @@ -104,13 +109,23 @@ fun NodeItem( } } - Card(modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 80.dp), colors = cardColors) { - Column( - modifier = - Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick).fillMaxWidth().padding(8.dp), - ) { + val interactionSource = remember { MutableInteractionSource() } + Card( + modifier = + modifier + .combinedClickable(onClick = onClick, onLongClick = onLongClick, interactionSource = interactionSource) + .fillMaxWidth() + .defaultMinSize(minHeight = 80.dp), + colors = cardColors, + ) { + Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - NodeChip(node = thatNode) + NodeChip( + node = thatNode, + onClick = { onClick() }, + onLongClick = onLongClick, + interactionSource = interactionSource, + ) NodeKeyStatusIcon( hasPKC = thatNode.hasPKC, @@ -121,7 +136,10 @@ fun NodeItem( Text( modifier = Modifier.weight(1f), text = longName, - style = style, + style = + MaterialTheme.typography.titleMediumEmphasized.copy( + color = MaterialTheme.colorScheme.onSurface, + ), textDecoration = TextDecoration.LineThrough.takeIf { isIgnored }, softWrap = true, ) @@ -133,45 +151,79 @@ fun NodeItem( isConnected = isConnected, ) } - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - if (distance != null) { - Text(text = distance, fontSize = MaterialTheme.typography.labelLarge.fontSize) - } else { - Spacer(modifier = Modifier.width(16.dp)) - } - BatteryInfo(batteryLevel = thatNode.batteryLevel, voltage = thatNode.voltage) - } - Spacer(modifier = Modifier.height(4.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - SignalInfo(node = thatNode, isThisNode = isThisNode) - thatNode.validPosition?.let { position -> - val satCount = position.satsInView - if (satCount > 0) { - SatelliteCountInfo(satCount = satCount) + MaterialBatteryInfo(level = thatNode.batteryLevel, voltage = thatNode.voltage) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (distance != null) { + DistanceInfo(distance = distance) + } + thatNode.validPosition?.let { position -> + ElevationInfo( + altitude = position.altitude, + system = system, + suffix = stringResource(id = R.string.elevation_suffix), + ) + val satCount = position.satsInView + if (satCount > 0) { + SatelliteCountInfo(satCount = satCount) + } } } } Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - val telemetryString = thatNode.getTelemetryString(tempInFahrenheit) - if (telemetryString.isNotEmpty()) { - Text( - text = telemetryString, - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + itemVerticalAlignment = Alignment.CenterVertically, + ) { + SignalInfo(node = thatNode, isThisNode = isThisNode) + } + 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, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + ) + } } } + 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, + color = MaterialTheme.colorScheme.onSurface, + ) + } else { + MaterialTheme.typography.labelSmall.copy(color = MaterialTheme.colorScheme.onSurface) + } + Text(text = thatNode.user.hwModel.name, style = labelStyle) + Text(text = thatNode.user.role.name, style = labelStyle) + Text(text = thatNode.user.id.ifEmpty { "???" }, style = labelStyle) + } } } } @Composable -@Preview(showBackground = false) +@Preview(showBackground = false, uiMode = Configuration.UI_MODE_NIGHT_YES) fun NodeInfoSimplePreview() { AppTheme { val thisNode = NodePreviewParameterProvider().values.first() @@ -185,23 +237,12 @@ fun NodeInfoSimplePreview() { fun NodeInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) thatNode: Node) { AppTheme { val thisNode = NodePreviewParameterProvider().values.first() - Column { - Text(text = "Details Collapsed", color = MaterialTheme.colorScheme.onBackground) - NodeItem( - thisNode = thisNode, - thatNode = thatNode, - distanceUnits = 1, - tempInFahrenheit = true, - currentTimeMillis = System.currentTimeMillis(), - ) - Text(text = "Details Shown", color = MaterialTheme.colorScheme.onBackground) - NodeItem( - thisNode = thisNode, - thatNode = thatNode, - distanceUnits = 1, - tempInFahrenheit = true, - currentTimeMillis = System.currentTimeMillis(), - ) - } + NodeItem( + thisNode = thisNode, + thatNode = thatNode, + distanceUnits = 1, + tempInFahrenheit = true, + currentTimeMillis = System.currentTimeMillis(), + ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeStatusIcons.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeStatusIcons.kt index 62c85ab3d..82273a9be 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeStatusIcons.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeStatusIcons.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Text +import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.rememberTooltipState @@ -51,7 +52,7 @@ fun NodeStatusIcons(isThisNode: Boolean, isUnmessageable: Boolean, isFavorite: B Row(modifier = Modifier.padding(4.dp)) { if (isThisNode) { TooltipBox( - positionProvider = TooltipDefaults.rememberTooltipPositionProvider(), + positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { PlainTooltip { Text( diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/SatelliteCountInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/SatelliteCountInfo.kt index 3f1a87b61..581766036 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/SatelliteCountInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/SatelliteCountInfo.kt @@ -17,40 +17,22 @@ package com.geeksville.mesh.ui.node.components -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.SatelliteAlt -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.res.stringResource import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.dp import org.meshtastic.core.ui.theme.AppTheme @Composable fun SatelliteCountInfo(modifier: Modifier = Modifier, satCount: Int) { - Row( + IconInfo( modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Icon( - modifier = Modifier.size(18.dp), - imageVector = Icons.TwoTone.SatelliteAlt, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = "$satCount", - fontSize = MaterialTheme.typography.labelLarge.fontSize, - color = MaterialTheme.colorScheme.onSurface, - ) - } + icon = Icons.TwoTone.SatelliteAlt, + contentDescription = stringResource(org.meshtastic.core.strings.R.string.sats), + text = "$satCount", + ) } @PreviewLightDark diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt index e5d4dc6a3..ca003ea40 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/model/Node.kt @@ -119,7 +119,7 @@ data class Node( fun gpsString(): String = GPSFormat.toDec(latitude, longitude) - private fun EnvironmentMetrics.getDisplayString(isFahrenheit: Boolean): String { + private fun EnvironmentMetrics.getDisplayStrings(isFahrenheit: Boolean): List { val temp = if (temperature != 0f) { if (isFahrenheit) { @@ -152,15 +152,23 @@ data class Node( val current = if (current != 0f) "%.1fmA".format(current) else null val iaq = if (iaq != 0) "IAQ: $iaq" else null - return listOfNotNull(temp, humidity, soilTemperatureStr, soilMoisture, voltage, current, iaq).joinToString(" ") + return listOfNotNull( + paxcounter.getDisplayString(), + temp, + humidity, + soilTemperatureStr, + soilMoisture, + voltage, + current, + iaq, + ) } private fun PaxcountProtos.Paxcount.getDisplayString() = "PAX: ${ble + wifi} (B:$ble/W:$wifi)".takeIf { ble != 0 || wifi != 0 } - fun getTelemetryString(isFahrenheit: Boolean = false): String = - listOfNotNull(paxcounter.getDisplayString(), environmentMetrics.getDisplayString(isFahrenheit)) - .joinToString(" ") + fun getTelemetryStrings(isFahrenheit: Boolean = false): List = + environmentMetrics.getDisplayStrings(isFahrenheit) } fun ConfigProtos.Config.DeviceConfig.Role?.isUnmessageableRole(): Boolean = this in diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/BatteryInfo.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/BatteryInfo.kt deleted file mode 100644 index 845d1560c..000000000 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/BatteryInfo.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.ui.component - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height -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.vector.ImageVector -import androidx.compose.ui.res.vectorResource -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.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import org.meshtastic.core.ui.R -import org.meshtastic.core.ui.theme.AppTheme - -@Composable -fun BatteryInfo(modifier: Modifier = Modifier, batteryLevel: Int?, voltage: Float?) { - val infoString = "%d%% %.2fV".format(batteryLevel, voltage) - val (image, level) = - when (batteryLevel) { - in 0..4 -> R.drawable.ic_battery_alert to " $infoString" - in 5..14 -> R.drawable.ic_battery_outline to infoString - in 15..34 -> R.drawable.ic_battery_low to infoString - in 35..79 -> R.drawable.ic_battery_medium to infoString - in 80..100 -> R.drawable.ic_battery_high to infoString - 101 -> R.drawable.ic_power_plug_24 to "%.2fV".format(voltage) - else -> R.drawable.ic_battery_unknown to (voltage?.let { "%.2fV".format(it) } ?: "") - } - - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - Icon( - modifier = Modifier.height(18.dp), - imageVector = ImageVector.vectorResource(id = image), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = level, - color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize, - ) - } -} - -@PreviewLightDark -@Composable -fun BatteryInfoPreview(@PreviewParameter(BatteryInfoPreviewParameterProvider::class) batteryInfo: Pair) { - AppTheme { BatteryInfo(batteryLevel = batteryInfo.first, voltage = batteryInfo.second) } -} - -@Composable -@Preview -fun BatteryInfoPreviewSimple() { - AppTheme { BatteryInfo(batteryLevel = 85, voltage = 3.7F) } -} - -class BatteryInfoPreviewParameterProvider : PreviewParameterProvider> { - override val values: Sequence> - get() = - sequenceOf( - 85 to 3.7F, - 2 to 3.7F, - 12 to 3.7F, - 28 to 3.7F, - 50 to 3.7F, - 101 to 4.9F, - null to 4.5F, - null to null, - ) -} diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt index 6975d5bcb..ab1197df5 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt @@ -24,11 +24,10 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.SignalCellular4Bar import androidx.compose.material.icons.filled.SignalCellularAlt @@ -45,7 +44,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import org.meshtastic.core.strings.R import org.meshtastic.core.ui.theme.StatusColors.StatusGreen @@ -60,7 +58,7 @@ const val RSSI_GOOD_THRESHOLD = -115 const val RSSI_FAIR_THRESHOLD = -126 @Stable -private enum class Quality( +enum class Quality( @Stable val nameRes: Int, @Stable val imageVector: ImageVector, @Stable val color: @Composable () -> Color, @@ -79,18 +77,20 @@ private enum class Quality( @Composable fun NodeSignalQuality(snr: Float, rssi: Int, modifier: Modifier = Modifier) { val quality = determineSignalQuality(snr, rssi) - FlowRow(modifier = modifier, maxLines = 1) { + FlowRow( + modifier = modifier, + itemVerticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { Snr(snr) - Spacer(Modifier.width(8.dp)) Rssi(rssi) - Spacer(Modifier.width(8.dp)) Text( text = "${stringResource(R.string.signal)} ${stringResource(quality.nameRes)}", - fontSize = MaterialTheme.typography.labelLarge.fontSize, + style = MaterialTheme.typography.labelSmall, maxLines = 1, ) - Spacer(Modifier.width(8.dp)) Icon( + modifier = Modifier.size(20.dp), imageVector = quality.imageVector, contentDescription = stringResource(R.string.signal_quality), tint = quality.color.invoke(), @@ -111,23 +111,26 @@ fun SnrAndRssi(snr: Float, rssi: Int) { @Composable fun LoraSignalIndicator(snr: Float, rssi: Int) { val quality = determineSignalQuality(snr, rssi) - Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize().padding(8.dp), ) { Icon( + modifier = Modifier.size(20.dp), imageVector = quality.imageVector, contentDescription = stringResource(R.string.signal_quality), tint = quality.color.invoke(), ) - Text(text = "${stringResource(R.string.signal)} ${stringResource(quality.nameRes)}") + Text( + text = "${stringResource(R.string.signal)} ${stringResource(quality.nameRes)}", + style = MaterialTheme.typography.labelSmall, + ) } } @Composable -fun Snr(snr: Float, fontSize: TextUnit = MaterialTheme.typography.labelLarge.fontSize) { +fun Snr(snr: Float) { val color: Color = if (snr > SNR_GOOD_THRESHOLD) { Quality.GOOD.color.invoke() @@ -137,11 +140,15 @@ fun Snr(snr: Float, fontSize: TextUnit = MaterialTheme.typography.labelLarge.fon Quality.BAD.color.invoke() } - Text(text = "%s %.2fdB".format(stringResource(id = R.string.snr), snr), color = color, fontSize = fontSize) + Text( + text = "%s %.2fdB".format(stringResource(id = R.string.snr), snr), + color = color, + style = MaterialTheme.typography.labelSmall, + ) } @Composable -fun Rssi(rssi: Int, fontSize: TextUnit = MaterialTheme.typography.labelLarge.fontSize) { +fun Rssi(rssi: Int) { val color: Color = if (rssi > RSSI_GOOD_THRESHOLD) { Quality.GOOD.color.invoke() @@ -150,10 +157,14 @@ fun Rssi(rssi: Int, fontSize: TextUnit = MaterialTheme.typography.labelLarge.fon } else { Quality.BAD.color.invoke() } - Text(text = "%s %ddBm".format(stringResource(id = R.string.rssi), rssi), color = color, fontSize = fontSize) + Text( + text = "%s %ddBm".format(stringResource(id = R.string.rssi), rssi), + color = color, + style = MaterialTheme.typography.labelSmall, + ) } -private fun determineSignalQuality(snr: Float, rssi: Int): Quality = when { +fun determineSignalQuality(snr: Float, rssi: Int): Quality = when { snr > SNR_GOOD_THRESHOLD && rssi > RSSI_GOOD_THRESHOLD -> Quality.GOOD snr > SNR_GOOD_THRESHOLD && rssi > RSSI_FAIR_THRESHOLD -> Quality.FAIR snr > SNR_FAIR_THRESHOLD && rssi > RSSI_GOOD_THRESHOLD -> Quality.FAIR diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt index e07084358..b43ece092 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt @@ -32,10 +32,12 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.res.stringResource 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 org.meshtastic.core.strings.R import org.meshtastic.core.ui.icon.BatteryEmpty import org.meshtastic.core.ui.icon.BatteryUnknown import org.meshtastic.core.ui.icon.MeshtasticIcons @@ -47,9 +49,9 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusRed private const val FORMAT = "%d%%" private const val SIZE_ICON = 20 -@Suppress("MagicNumber") +@Suppress("MagicNumber", "LongMethod") @Composable -fun MaterialBatteryInfo(modifier: Modifier = Modifier, level: Int) { +fun MaterialBatteryInfo(modifier: Modifier = Modifier, level: Int?, voltage: Float? = null) { val levelString = FORMAT.format(level) Row( @@ -57,21 +59,25 @@ fun MaterialBatteryInfo(modifier: Modifier = Modifier, level: Int) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp), ) { - if (level > 100) { - Icon( - modifier = Modifier.size(SIZE_ICON.dp).rotate(90f), - imageVector = Icons.Rounded.Power, - tint = MaterialTheme.colorScheme.onSurface, - contentDescription = null, - ) - - Text(text = "PWD", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge) - } else if (level < 0) { + if (level == null || level < 0) { Icon( modifier = Modifier.size(SIZE_ICON.dp), imageVector = MeshtasticIcons.BatteryUnknown, tint = MaterialTheme.colorScheme.onSurface, - contentDescription = null, + contentDescription = stringResource(R.string.unknown), + ) + } else if (level > 100) { + Icon( + modifier = Modifier.size(SIZE_ICON.dp).rotate(90f), + imageVector = Icons.Rounded.Power, + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = levelString, + ) + + Text( + text = "PWD", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium, ) } else { // Map battery percentage to color @@ -103,24 +109,42 @@ fun MaterialBatteryInfo(modifier: Modifier = Modifier, level: Int) { }, imageVector = MeshtasticIcons.BatteryEmpty, tint = MaterialTheme.colorScheme.onSurface, - contentDescription = null, + contentDescription = levelString, ) Text( text = levelString, color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.labelLarge, + style = MaterialTheme.typography.labelMedium, ) + voltage?.let { + Text( + text = "%.2fV".format(it), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelMedium, + ) + } } } } -class BatteryLevelProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf(-1, 19, 39, 90, 101) +class BatteryInfoPreviewParameterProvider : PreviewParameterProvider> { + override val values: Sequence> + get() = + sequenceOf( + 85 to 3.7F, + 2 to 3.7F, + 12 to 3.7F, + 28 to 3.7F, + 50 to 3.7F, + 101 to 4.9F, + null to 4.5F, + null to null, + ) } @PreviewLightDark @Composable -fun MaterialBatteryInfoPreview(@PreviewParameter(BatteryLevelProvider::class) batteryLevel: Int) { - AppTheme { MaterialBatteryInfo(level = batteryLevel) } +fun MaterialBatteryInfoPreview(@PreviewParameter(BatteryInfoPreviewParameterProvider::class) info: Pair) { + AppTheme { MaterialBatteryInfo(level = info.first, voltage = info.second) } } diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Elevation.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Elevation.kt new file mode 100644 index 000000000..d77914cd9 --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Elevation.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.meshtastic.core.ui.icon + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +/** + * This is from Material Symbols. + * + * @see + * [elevation](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:elevation:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=elevation&icon.size=24&icon.color=%23e3e3e3&icon.platform=android&icon.style=Rounded) + */ +val MeshtasticIcons.Elevation: ImageVector + get() { + if (elevation != null) { + return elevation!! + } + elevation = + ImageVector.Builder( + name = "Rounded.Elevation", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ) + .apply { + path(fill = SolidColor(Color(0xFFE3E3E3))) { + moveTo(760f, 840f) + lineTo(160f, 840f) + quadToRelative(-25f, 0f, -35.5f, -21.5f) + reflectiveQuadTo(128f, 777f) + lineToRelative(188f, -264f) + quadToRelative(11f, -16f, 28f, -24.5f) + reflectiveQuadToRelative(37f, -8.5f) + horizontalLineToRelative(161f) + lineToRelative(228f, -266f) + quadToRelative(18f, -21f, 44f, -11.5f) + reflectiveQuadToRelative(26f, 37.5f) + verticalLineToRelative(520f) + quadToRelative(0f, 33f, -23.5f, 56.5f) + reflectiveQuadTo(760f, 840f) + close() + moveTo(300f, 400f) + lineTo(176f, 575f) + quadToRelative(-10f, 14f, -26f, 16.5f) + reflectiveQuadToRelative(-30f, -7.5f) + quadToRelative(-14f, -10f, -16.5f, -26f) + reflectiveQuadToRelative(7.5f, -30f) + lineToRelative(125f, -174f) + quadToRelative(11f, -16f, 28f, -25f) + reflectiveQuadToRelative(37f, -9f) + horizontalLineToRelative(161f) + lineToRelative(162f, -189f) + quadToRelative(11f, -13f, 27f, -14f) + reflectiveQuadToRelative(29f, 10f) + quadToRelative(13f, 11f, 14f, 27f) + reflectiveQuadToRelative(-10f, 29f) + lineTo(522f, 372f) + quadToRelative(-11f, 14f, -27f, 21f) + reflectiveQuadToRelative(-33f, 7f) + lineTo(300f, 400f) + close() + moveTo(238f, 760f) + horizontalLineToRelative(522f) + verticalLineToRelative(-412f) + lineTo(602f, 532f) + quadToRelative(-11f, 14f, -27f, 21f) + reflectiveQuadToRelative(-33f, 7f) + lineTo(380f, 560f) + lineTo(238f, 760f) + close() + moveTo(760f, 760f) + close() + } + } + .build() + + return elevation!! + } + +private var elevation: ImageVector? = null diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef8736cb7..4619399b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -86,6 +86,7 @@ androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-runtime-tracing = { module = "androidx.compose.runtime:runtime-tracing", version.ref = "androidxTracing" } androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test-junit4" } androidx-compose-ui-testManifest = { module = "androidx.compose.ui:ui-test-manifest" } +androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" } @@ -216,7 +217,7 @@ testing-room = ["room-testing"] # UI adaptive = ["androidx-compose-material3-adaptive", "androidx-compose-material3-adaptive-layout", "androidx-compose-material3-adaptive-navigation", "androidx-compose-material3-navigationSuite"] -ui = ["material", "constraintlayout", "androidx-compose-material3", "androidx-compose-material-iconsExtended", "androidx-compose-ui-tooling-preview", "compose-runtime-livedata"] +ui = ["material", "constraintlayout", "androidx-compose-material3", "androidx-compose-material-iconsExtended", "androidx-compose-ui-tooling-preview", "compose-runtime-livedata", "androidx-compose-ui-text"] ui-tooling = ["androidx-compose-ui-tooling"] #Separate for debugImplementation [plugins]