refactor(ui)!: update NodeItem display with new components (#3273)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
pull/3279/head
James Rich 2025-10-01 14:31:08 -05:00 zatwierdzone przez GitHub
rodzic a3009c9c84
commit 0847598d38
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
19 zmienionych plików z 536 dodań i 306 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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,
)
}
}
}
}

Wyświetl plik

@ -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(

Wyświetl plik

@ -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))

Wyświetl plik

@ -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

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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.") }
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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") })
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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")

Wyświetl plik

@ -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(),
)
}
}

Wyświetl plik

@ -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(

Wyświetl plik

@ -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

Wyświetl plik

@ -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<String> {
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<String> =
environmentMetrics.getDisplayStrings(isFahrenheit)
}
fun ConfigProtos.Config.DeviceConfig.Role?.isUnmessageableRole(): Boolean = this in

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Int?, Float?>) {
AppTheme { BatteryInfo(batteryLevel = batteryInfo.first, voltage = batteryInfo.second) }
}
@Composable
@Preview
fun BatteryInfoPreviewSimple() {
AppTheme { BatteryInfo(batteryLevel = 85, voltage = 3.7F) }
}
class BatteryInfoPreviewParameterProvider : PreviewParameterProvider<Pair<Int?, Float?>> {
override val values: Sequence<Pair<Int?, Float?>>
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,
)
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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<Int> {
override val values: Sequence<Int> = sequenceOf(-1, 19, 39, 90, 101)
class BatteryInfoPreviewParameterProvider : PreviewParameterProvider<Pair<Int?, Float?>> {
override val values: Sequence<Pair<Int?, Float?>>
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<Int?, Float?>) {
AppTheme { MaterialBatteryInfo(level = info.first, voltage = info.second) }
}

Wyświetl plik

@ -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 <https://www.gnu.org/licenses/>.
*/
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

Wyświetl plik

@ -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]