mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(ui)!: update NodeItem display with new components (#3273)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
a3009c9c84
commit
0847598d38
19 changed files with 536 additions and 306 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
100
core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Elevation.kt
Normal file
100
core/ui/src/main/kotlin/org/meshtastic/core/ui/icon/Elevation.kt
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue