feat: migrate to Material 3 Expressive APIs (#4934)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-26 11:42:46 -05:00 committed by GitHub
parent c259c76550
commit 141b54ff9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 112 additions and 103 deletions

View file

@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Notes
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -95,6 +96,7 @@ private const val ACTIVE_ALPHA = 0.5f
private const val INACTIVE_ALPHA = 0.2f
private const val GRID_COLUMNS = 3
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Suppress("LongMethod")
fun NodeItem(
@ -389,6 +391,7 @@ private fun MetricsGrid(items: List<@Composable () -> Unit>) {
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun NodeItemHeader(
thatNode: Node,
@ -424,7 +427,7 @@ private fun NodeItemHeader(
) {
Text(
text = longName,
style = MaterialTheme.typography.titleMedium.copy(fontStyle = style),
style = MaterialTheme.typography.titleMediumEmphasized.copy(fontStyle = style),
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View file

@ -30,8 +30,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@ -62,6 +64,7 @@ import org.meshtastic.feature.node.component.NodeFilterTextField
import org.meshtastic.feature.node.component.NodeItem
@Suppress("LongMethod", "CyclomaticComplexMethod")
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun NodeListScreen(
navigateToNodeDetails: (Int) -> Unit,
@ -114,16 +117,19 @@ fun NodeListScreen(
},
floatingActionButton = {
val shareCapable = ourNode?.capabilities?.supportsQrCodeSharing ?: false
if (!isScrollInProgress && connectionState == ConnectionState.Connected && shareCapable) {
MeshtasticImportFAB(
onImport = { uriString ->
onHandleDeepLink(org.meshtastic.core.common.util.MeshtasticUri(uriString)) {
scope.launch { showToast(Res.string.channel_invalid) }
}
},
isContactContext = true,
)
}
MeshtasticImportFAB(
modifier =
Modifier.animateFloatingActionButton(
visible = !isScrollInProgress && connectionState == ConnectionState.Connected && shareCapable,
alignment = androidx.compose.ui.Alignment.BottomEnd,
),
onImport = { uriString ->
onHandleDeepLink(org.meshtastic.core.common.util.MeshtasticUri(uriString)) {
scope.launch { showToast(Res.string.channel_invalid) }
}
},
isContactContext = true,
)
},
) { contentPadding ->
Box(modifier = Modifier.fillMaxSize().padding(contentPadding).focusable()) {

View file

@ -35,6 +35,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -214,6 +215,7 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun DeviceMetricsChart(
modifier: Modifier = Modifier,
telemetries: List<Telemetry>,
@ -386,6 +388,7 @@ private fun DeviceMetricsChart(
@Suppress("detekt:MagicNumber", "UnusedPrivateMember") // Compose preview with fake data
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun DeviceMetricsChartPreview() {
val now = nowSeconds.toInt()
val telemetries =
@ -416,6 +419,7 @@ private fun DeviceMetricsChartPreview() {
@Composable
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick: () -> Unit) {
val deviceMetrics = telemetry.device_metrics
val time = telemetry.time.toLong() * MS_PER_SEC
@ -444,7 +448,7 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = CommonCharts.formatDateTime(time),
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMediumEmphasized,
fontWeight = FontWeight.Bold,
)
@ -518,6 +522,7 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
@Suppress("detekt:MagicNumber", "UnusedPrivateMember") // Compose preview with fake data
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun DeviceMetricsCardPreview() {
val now = nowSeconds.toInt()
val telemetry =
@ -537,6 +542,7 @@ private fun DeviceMetricsCardPreview() {
@Suppress("detekt:MagicNumber", "UnusedPrivateMember") // Compose preview with fake data
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun DeviceMetricsScreenPreview() {
val now = nowSeconds.toInt()
val telemetries =

View file

@ -34,6 +34,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -119,6 +120,7 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Un
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun TemperatureDisplay(
envMetrics: org.meshtastic.proto.EnvironmentMetrics,
environmentDisplayFahrenheit: Boolean,
@ -140,6 +142,7 @@ private fun TemperatureDisplay(
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun HumidityAndBarometricPressureDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) {
val hasHumidity = envMetrics.relative_humidity?.let { !it.isNaN() } == true
val hasPressure = envMetrics.barometric_pressure?.let { !it.isNaN() && it > 0 } == true
@ -180,6 +183,7 @@ private fun HumidityAndBarometricPressureDisplay(envMetrics: org.meshtastic.prot
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun SoilMetricsDisplay(
envMetrics: org.meshtastic.proto.EnvironmentMetrics,
environmentDisplayFahrenheit: Boolean,
@ -232,6 +236,7 @@ private fun SoilMetricsDisplay(
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun LuxUVLuxDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) {
val hasLux = envMetrics.lux != null && !envMetrics.lux!!.isNaN()
val hasUvLux = envMetrics.uv_lux != null && !envMetrics.uv_lux!!.isNaN()
@ -267,6 +272,7 @@ private fun LuxUVLuxDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics)
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun VoltageCurrentDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) {
val hasVoltage = envMetrics.voltage != null && !envMetrics.voltage!!.isNaN()
val hasCurrent = envMetrics.current != null && !envMetrics.current!!.isNaN()
@ -294,6 +300,7 @@ private fun VoltageCurrentDisplay(envMetrics: org.meshtastic.proto.EnvironmentMe
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun GasCompositionDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) {
val iaqValue = envMetrics.iaq
val gasResistance = envMetrics.gas_resistance
@ -329,6 +336,7 @@ private fun GasCompositionDisplay(envMetrics: org.meshtastic.proto.EnvironmentMe
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun RadiationDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) {
envMetrics.radiation?.let { radiation ->
if (!radiation.isNaN() && radiation > 0f) {
@ -344,6 +352,7 @@ private fun RadiationDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun EnvironmentMetricsCard(
telemetry: Telemetry,
environmentDisplayFahrenheit: Boolean,
@ -370,6 +379,7 @@ private fun EnvironmentMetricsCard(
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFahrenheit: Boolean) {
val envMetrics = telemetry.environment_metrics ?: org.meshtastic.proto.EnvironmentMetrics()
val time = telemetry.time.toLong() * MS_PER_SEC
@ -378,7 +388,7 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = CommonCharts.formatDateTime(time),
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMediumEmphasized,
fontWeight = FontWeight.Bold,
)
TemperatureDisplay(envMetrics, environmentDisplayFahrenheit)
@ -401,6 +411,7 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa
@Suppress("MagicNumber", "UnusedPrivateMember") // Compose preview with fake data
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun PreviewEnvironmentMetricsContent() {
val fakeEnvMetrics =
org.meshtastic.proto.EnvironmentMetrics(

View file

@ -32,6 +32,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
@ -103,6 +104,7 @@ fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel, onNavigateUp: () ->
@Suppress("LongMethod", "MagicNumber")
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun HostMetricsItem(modifier: Modifier = Modifier, telemetry: Telemetry) {
val hostMetrics = telemetry.host_metrics
val time = telemetry.time.toLong() * TimeConstants.MS_PER_SEC
@ -119,7 +121,7 @@ fun HostMetricsItem(modifier: Modifier = Modifier, telemetry: Telemetry) {
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.End,
text = DateFormatter.formatDateTime(time),
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMediumEmphasized,
fontWeight = FontWeight.Bold,
)
hostMetrics?.uptime_seconds?.let {

View file

@ -30,6 +30,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -48,6 +49,7 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons
/** Shared metric log/list UI components used by TracerouteLog, NeighborInfoLog, HostMetricsLog, and PositionLog. */
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun MetricLogItem(icon: ImageVector, text: String, contentDescription: String, modifier: Modifier = Modifier) {
Card(
modifier = modifier.fillMaxWidth().heightIn(min = 64.dp).padding(vertical = 4.dp, horizontal = 8.dp),
@ -72,7 +74,7 @@ fun MetricLogItem(icon: ImageVector, text: String, contentDescription: String, m
}
Text(
text = text,
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMediumEmphasized,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)

View file

@ -30,6 +30,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -86,6 +87,7 @@ private val LEGEND_DATA =
@Suppress("LongMethod")
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun PaxMetricsChart(
modifier: Modifier = Modifier,
totalSeries: List<Pair<Int, Int>>,
@ -262,6 +264,7 @@ fun PaxcountInfo(
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun PaxMetricsItem(log: MeshLog, pax: ProtoPaxcount, isSelected: Boolean, onClick: () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clickable { onClick() },
@ -279,7 +282,7 @@ fun PaxMetricsItem(log: MeshLog, pax: ProtoPaxcount, isSelected: Boolean, onClic
Column(modifier = Modifier.fillMaxWidth().padding(12.dp)) {
Text(
text = DateFormatter.formatDateTime(log.received_date),
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMediumEmphasized,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.End,
modifier = Modifier.fillMaxWidth(),

View file

@ -34,6 +34,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -168,6 +169,7 @@ fun PowerMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
@Suppress("LongMethod")
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun PowerMetricsChart(
modifier: Modifier = Modifier,
telemetries: List<Telemetry>,
@ -295,6 +297,7 @@ private fun PowerMetricsChart(
@Composable
@Suppress("CyclomaticComplexMethod")
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun PowerMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick: () -> Unit) {
val time = telemetry.time.toLong() * MS_PER_SEC
Card(
@ -318,7 +321,7 @@ private fun PowerMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick:
Row {
Text(
text = CommonCharts.formatDateTime(time),
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMediumEmphasized,
fontWeight = FontWeight.Bold,
)
}
@ -347,6 +350,7 @@ private fun PowerMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick:
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun PowerChannelColumn(titleRes: StringResource, voltage: Float, current: Float) {
Column {
Text(
@ -376,6 +380,7 @@ private fun PowerChannelColumn(titleRes: StringResource, voltage: Float, current
}
/** Retrieves the appropriate voltage depending on `channelSelected`. */
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun retrieveVoltage(channelSelected: PowerChannel, telemetry: Telemetry): Float = when (channelSelected) {
PowerChannel.ONE -> telemetry.power_metrics?.ch1_voltage ?: Float.NaN
PowerChannel.TWO -> telemetry.power_metrics?.ch2_voltage ?: Float.NaN
@ -383,6 +388,7 @@ private fun retrieveVoltage(channelSelected: PowerChannel, telemetry: Telemetry)
}
/** Retrieves the appropriate current depending on `channelSelected`. */
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun retrieveCurrent(channelSelected: PowerChannel, telemetry: Telemetry): Float = when (channelSelected) {
PowerChannel.ONE -> telemetry.power_metrics?.ch1_current ?: Float.NaN
PowerChannel.TWO -> telemetry.power_metrics?.ch2_current ?: Float.NaN

View file

@ -34,6 +34,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -134,6 +135,7 @@ fun SignalMetricsScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun SignalMetricsChart(
modifier: Modifier = Modifier,
meshPackets: List<MeshPacket>,
@ -245,6 +247,7 @@ private fun SignalMetricsChart(
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun SignalMetricsCard(meshPacket: MeshPacket, isSelected: Boolean, onClick: () -> Unit) {
val time = meshPacket.rx_time.toLong() * MS_PER_SEC
Card(
@ -270,7 +273,7 @@ private fun SignalMetricsCard(meshPacket: MeshPacket, isSelected: Boolean, onCli
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = CommonCharts.formatDateTime(time),
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMediumEmphasized,
fontWeight = FontWeight.Bold,
)
}