From 6fd444c0778637506e01d741ce34e972705a0048 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:27:33 -0500 Subject: [PATCH] refactor(colors): consolidate colors in UI components (#2520) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../mesh/model/EnvironmentMetricsState.kt | 121 ++--- .../main/java/com/geeksville/mesh/ui/Main.kt | 203 +++----- .../ui/common/components/IndoorAirQuality.kt | 133 ++--- .../common/components/LoraSignalIndicator.kt | 96 ++-- .../mesh/ui/common/components/SecurityIcon.kt | 193 +++---- .../geeksville/mesh/ui/common/theme/Color.kt | 12 - .../mesh/ui/common/theme/CustomColors.kt | 90 ++++ .../connections/components/DeviceListItem.kt | 127 +++-- .../mesh/ui/metrics/DeviceMetrics.kt | 268 ++++------ .../mesh/ui/metrics/EnvironmentMetrics.kt | 259 ++++------ .../mesh/ui/metrics/PowerMetrics.kt | 213 +++----- .../com/geeksville/mesh/ui/node/NodeDetail.kt | 488 ++++++++---------- .../ui/node/components/NodeKeyStatusIcon.kt | 143 ++--- .../ui/node/components/NodeStatusIcons.kt | 68 +-- 14 files changed, 1028 insertions(+), 1386 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/common/theme/CustomColors.kt diff --git a/app/src/main/java/com/geeksville/mesh/model/EnvironmentMetricsState.kt b/app/src/main/java/com/geeksville/mesh/model/EnvironmentMetricsState.kt index 632592071..d0e6e1a55 100644 --- a/app/src/main/java/com/geeksville/mesh/model/EnvironmentMetricsState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/EnvironmentMetricsState.kt @@ -19,51 +19,39 @@ package com.geeksville.mesh.model import androidx.compose.ui.graphics.Color import com.geeksville.mesh.TelemetryProtos.Telemetry -import com.geeksville.mesh.ui.common.theme.InfantryBlue -import com.geeksville.mesh.ui.common.theme.Orange -import com.geeksville.mesh.ui.common.theme.Pink -import com.geeksville.mesh.ui.common.theme.Purple +import com.geeksville.mesh.ui.common.theme.GraphColors.InfantryBlue +import com.geeksville.mesh.ui.common.theme.GraphColors.Orange +import com.geeksville.mesh.ui.common.theme.GraphColors.Pink +import com.geeksville.mesh.ui.common.theme.GraphColors.Purple +import com.geeksville.mesh.ui.common.theme.GraphColors.Red import com.geeksville.mesh.util.UnitConversions enum class Environment(val color: Color) { - TEMPERATURE(Color.Red) { - override fun getValue(telemetry: Telemetry): Float { - return telemetry.environmentMetrics.temperature - } + TEMPERATURE(Red) { + override fun getValue(telemetry: Telemetry): Float = telemetry.environmentMetrics.temperature }, HUMIDITY(InfantryBlue) { - override fun getValue(telemetry: Telemetry): Float { - return telemetry.environmentMetrics.relativeHumidity - } + override fun getValue(telemetry: Telemetry): Float = telemetry.environmentMetrics.relativeHumidity }, SOIL_TEMPERATURE(Pink) { - override fun getValue(telemetry: Telemetry): Float { - return telemetry.environmentMetrics.soilTemperature - } + override fun getValue(telemetry: Telemetry): Float = telemetry.environmentMetrics.soilTemperature }, SOIL_MOISTURE(Purple) { - override fun getValue(telemetry: Telemetry): Float { - return telemetry.environmentMetrics.soilMoisture.toFloat() - } + override fun getValue(telemetry: Telemetry): Float = telemetry.environmentMetrics.soilMoisture.toFloat() }, IAQ(Color.Green) { - override fun getValue(telemetry: Telemetry): Float { - return telemetry.environmentMetrics.iaq.toFloat() - } + override fun getValue(telemetry: Telemetry): Float = telemetry.environmentMetrics.iaq.toFloat() }, BAROMETRIC_PRESSURE(Orange) { - override fun getValue(telemetry: Telemetry): Float { - return telemetry.environmentMetrics.barometricPressure - } - }; + override fun getValue(telemetry: Telemetry): Float = telemetry.environmentMetrics.barometricPressure + }, ; abstract fun getValue(telemetry: Telemetry): Float } /** * @param metrics the filtered [List] - * @param shouldPlot a [List] the size of [Environment] used to determine if a metric - * should be plotted + * @param shouldPlot a [List] the size of [Environment] used to determine if a metric should be plotted * @param leftMinMax [Pair] with the min and max of the barometric pressure * @param rightMinMax [Pair] with the combined min and max of: the temperature, humidity, and IAQ * @param times [Pair] with the oldest and newest times in that order @@ -73,12 +61,10 @@ data class EnvironmentGraphingData( val shouldPlot: List, val leftMinMax: Pair = Pair(0f, 0f), val rightMinMax: Pair = Pair(0f, 0f), - val times: Pair = Pair(0, 0) + val times: Pair = Pair(0, 0), ) -data class EnvironmentMetricsState( - val environmentMetrics: List = emptyList(), -) { +data class EnvironmentMetricsState(val environmentMetrics: List = emptyList()) { fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty() /** @@ -99,10 +85,11 @@ data class EnvironmentMetricsState( /* Grab the combined min and max for temp, humidity, soil_Temperature, soilMoisture and iaq. */ val minValues = mutableListOf() val maxValues = mutableListOf() - val (minTemp, maxTemp) = Pair( - telemetries.minBy { it.environmentMetrics.temperature }, - telemetries.maxBy { it.environmentMetrics.temperature } - ) + val (minTemp, maxTemp) = + Pair( + telemetries.minBy { it.environmentMetrics.temperature }, + telemetries.maxBy { it.environmentMetrics.temperature }, + ) var minTempValue = minTemp.environmentMetrics.temperature var maxTempValue = maxTemp.environmentMetrics.temperature if (useFahrenheit) { @@ -115,12 +102,15 @@ data class EnvironmentMetricsState( shouldPlot[Environment.TEMPERATURE.ordinal] = true } - val (minHumidity, maxHumidity) = Pair( - telemetries.minBy { it.environmentMetrics.relativeHumidity }, - telemetries.maxBy { it.environmentMetrics.relativeHumidity } - ) - if (minHumidity.environmentMetrics.relativeHumidity != 0f || - maxHumidity.environmentMetrics.relativeHumidity != 0f) { + val (minHumidity, maxHumidity) = + Pair( + telemetries.minBy { it.environmentMetrics.relativeHumidity }, + telemetries.maxBy { it.environmentMetrics.relativeHumidity }, + ) + if ( + minHumidity.environmentMetrics.relativeHumidity != 0f || + maxHumidity.environmentMetrics.relativeHumidity != 0f + ) { minValues.add(minHumidity.environmentMetrics.relativeHumidity) maxValues.add(maxHumidity.environmentMetrics.relativeHumidity) shouldPlot[Environment.HUMIDITY.ordinal] = true @@ -132,29 +122,29 @@ data class EnvironmentMetricsState( minSoilTemperatureValue = UnitConversions.celsiusToFahrenheit(minSoilTemperatureValue) maxSoilTemperatureValue = UnitConversions.celsiusToFahrenheit(maxSoilTemperatureValue) } - if (minTemp.environmentMetrics.soilTemperature != 0f || - maxTemp.environmentMetrics.soilTemperature != 0f) { + if (minTemp.environmentMetrics.soilTemperature != 0f || maxTemp.environmentMetrics.soilTemperature != 0f) { minValues.add(minSoilTemperatureValue) maxValues.add(maxSoilTemperatureValue) shouldPlot[Environment.SOIL_TEMPERATURE.ordinal] = true } - val (minSoilMoisture, maxSoilMoisture) = Pair( - telemetries.minBy { it.environmentMetrics.soilMoisture }, - telemetries.maxBy { it.environmentMetrics.soilMoisture } - ) + val (minSoilMoisture, maxSoilMoisture) = + Pair( + telemetries.minBy { it.environmentMetrics.soilMoisture }, + telemetries.maxBy { it.environmentMetrics.soilMoisture }, + ) val soilMoistureRange = 0..100 - if (minSoilMoisture.environmentMetrics.soilMoisture in soilMoistureRange || - maxSoilMoisture.environmentMetrics.soilMoisture in soilMoistureRange) { + if ( + minSoilMoisture.environmentMetrics.soilMoisture in soilMoistureRange || + maxSoilMoisture.environmentMetrics.soilMoisture in soilMoistureRange + ) { minValues.add(minSoilMoisture.environmentMetrics.soilMoisture.toFloat()) maxValues.add(maxSoilMoisture.environmentMetrics.soilMoisture.toFloat()) shouldPlot[Environment.SOIL_MOISTURE.ordinal] = true } - val (minIAQ, maxIAQ) = Pair( - telemetries.minBy { it.environmentMetrics.iaq }, - telemetries.maxBy { it.environmentMetrics.iaq } - ) + val (minIAQ, maxIAQ) = + Pair(telemetries.minBy { it.environmentMetrics.iaq }, telemetries.maxBy { it.environmentMetrics.iaq }) if (minIAQ.environmentMetrics.iaq != 0 || maxIAQ.environmentMetrics.iaq != 0) { minValues.add(minIAQ.environmentMetrics.iaq.toFloat()) maxValues.add(maxIAQ.environmentMetrics.iaq.toFloat()) @@ -164,28 +154,29 @@ data class EnvironmentMetricsState( val min = if (minValues.isEmpty()) 0f else minValues.minOf { it } val max = if (maxValues.isEmpty()) 0f else maxValues.maxOf { it } - val (minPressure, maxPressure) = Pair( - telemetries.minBy { it.environmentMetrics.barometricPressure }, - telemetries.maxBy { it.environmentMetrics.barometricPressure } - ) - if (minPressure.environmentMetrics.barometricPressure != 0.0F && - maxPressure.environmentMetrics.barometricPressure != 0.0F) { + val (minPressure, maxPressure) = + Pair( + telemetries.minBy { it.environmentMetrics.barometricPressure }, + telemetries.maxBy { it.environmentMetrics.barometricPressure }, + ) + if ( + minPressure.environmentMetrics.barometricPressure != 0.0F && + maxPressure.environmentMetrics.barometricPressure != 0.0F + ) { shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal] = true } - val (oldest, newest) = Pair( - telemetries.minBy { it.time }, - telemetries.maxBy { it.time } - ) + val (oldest, newest) = Pair(telemetries.minBy { it.time }, telemetries.maxBy { it.time }) return EnvironmentGraphingData( metrics = telemetries, shouldPlot = shouldPlot.toList(), - leftMinMax = Pair( + leftMinMax = + Pair( minPressure.environmentMetrics.barometricPressure, - maxPressure.environmentMetrics.barometricPressure + maxPressure.environmentMetrics.barometricPressure, ), rightMinMax = Pair(min, max), - times = Pair(oldest.time, newest.time) + times = Pair(oldest.time, newest.time), ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index 95626e009..079ba5089 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -42,6 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Text import androidx.compose.material3.TooltipBox @@ -96,6 +97,9 @@ import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel import com.geeksville.mesh.ui.common.components.MultipleChoiceAlertDialog import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog import com.geeksville.mesh.ui.common.components.SimpleAlertDialog +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow import com.geeksville.mesh.ui.debug.DebugMenuActions import com.geeksville.mesh.ui.node.components.NodeChip import com.geeksville.mesh.ui.node.components.NodeMenuAction @@ -117,10 +121,11 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, MapRoutes.Map, ChannelsRoutes.Channels, ConnectionsRoutes.Connections, - ).any { this.hasRoute(it::class) } + ) + .any { this.hasRoute(it::class) } - fun fromNavDestination(destination: NavDestination?): TopLevelDestination? = entries - .find { dest -> destination?.hierarchy?.any { it.hasRoute(dest.route::class) } == true } + fun fromNavDestination(destination: NavDestination?): TopLevelDestination? = + entries.find { dest -> destination?.hierarchy?.any { it.hasRoute(dest.route::class) } == true } } } @@ -137,9 +142,7 @@ fun MainScreen( val localConfig by uIViewModel.localConfig.collectAsStateWithLifecycle() val requestChannelSet by uIViewModel.requestChannelSet.collectAsStateWithLifecycle() if (connectionState.isConnected()) { - requestChannelSet?.let { newChannelSet -> - ScannedQrCodeDialog(uIViewModel, newChannelSet) - } + requestChannelSet?.let { newChannelSet -> ScannedQrCodeDialog(uIViewModel, newChannelSet) } } VersionChecks(uIViewModel) @@ -176,9 +179,7 @@ fun MainScreen( } SimpleAlertDialog( title = R.string.client_notification, - text = { - Text(text = message) - }, + text = { Text(text = message) }, onConfirm = { if (compromisedKeys) { navController.navigate(RadioConfigRoutes.Security) @@ -192,15 +193,12 @@ fun MainScreen( traceRouteResponse?.let { response -> SimpleAlertDialog( title = R.string.traceroute, - text = { - Text(text = response) - }, + text = { Text(text = response) }, dismissText = stringResource(id = R.string.okay), - onDismiss = { uIViewModel.clearTracerouteResponse() } + onDismiss = { uIViewModel.clearTracerouteResponse() }, ) } - val navSuiteType = - NavigationSuiteScaffoldDefaults.navigationSuiteType(currentWindowAdaptiveInfo()) + val navSuiteType = NavigationSuiteScaffoldDefaults.navigationSuiteType(currentWindowAdaptiveInfo()) val currentDestination = navController.currentBackStackEntryAsState().value?.destination val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination) NavigationSuiteScaffold( @@ -224,7 +222,7 @@ fun MainScreen( ) } }, - state = rememberTooltipState() + state = rememberTooltipState(), ) { TopLevelNavIcon(destination, connectionState) } @@ -237,26 +235,18 @@ fun MainScreen( }, onClick = { navController.navigate(destination.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } + popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true } - } + }, ) } - } + }, ) { - Column( - modifier = Modifier - .fillMaxSize() - ) { + Column(modifier = Modifier.fillMaxSize()) { var sharedContact: Node? by remember { mutableStateOf(null) } if (sharedContact != null) { - SharedContactDialog( - contact = sharedContact, - onDismiss = { sharedContact = null } - ) + SharedContactDialog(contact = sharedContact, onDismiss = { sharedContact = null }) } MainAppBar( viewModel = uIViewModel, @@ -274,13 +264,11 @@ fun MainScreen( when (action) { is NodeMenuAction.MoreDetails -> { navController.navigate( - NodesRoutes.NodeDetailGraph( - action.node.num - ), + NodesRoutes.NodeDetailGraph(action.node.num), { launchSingleTop = true restoreState = true - } + }, ) } @@ -291,11 +279,7 @@ fun MainScreen( }, ) NavGraph( - modifier = Modifier - .fillMaxSize() - .recalculateWindowInsets() - .safeDrawingPadding() - .imePadding(), + modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding().imePadding(), uIViewModel = uIViewModel, bluetoothViewModel = bluetoothViewModel, navController = navController, @@ -305,14 +289,11 @@ fun MainScreen( } @Composable -private fun VersionChecks( - viewModel: UIViewModel, -) { +private fun VersionChecks(viewModel: UIViewModel) { val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle() val context = LocalContext.current - val latestStableFirmwareRelease by - viewModel.latestStableFirmwareRelease.collectAsState(DeviceVersion("2.6.4")) + val latestStableFirmwareRelease by viewModel.latestStableFirmwareRelease.collectAsState(DeviceVersion("2.6.4")) // Check if the device is running an old app version or firmware version LaunchedEffect(connectionState, myNodeInfo) { if (connectionState == MeshService.ConnectionState.CONNECTED) { @@ -327,7 +308,7 @@ private fun VersionChecks( onConfirm = { val service = viewModel.meshService ?: return@showAlert MeshService.changeDeviceAddress(context, service, "n") - } + }, ) } else if (curVer < MeshService.absoluteMinDeviceVersion) { val title = context.getString(R.string.firmware_too_old) @@ -339,21 +320,12 @@ private fun VersionChecks( onConfirm = { val service = viewModel.meshService ?: return@showAlert MeshService.changeDeviceAddress(context, service, "n") - } + }, ) } else if (curVer < MeshService.minDeviceVersion) { val title = context.getString(R.string.should_update_firmware) - val message = - context.getString( - R.string.should_update, - latestStableFirmwareRelease.asString - ) - viewModel.showAlert( - title = title, - message = message, - dismissable = false, - onConfirm = {} - ) + val message = context.getString(R.string.should_update, latestStableFirmwareRelease.asString) + viewModel.showAlert(title = title, message = message, dismissable = false, onConfirm = {}) } } } @@ -379,7 +351,7 @@ private fun MainAppBar( isManaged: Boolean, navController: NavHostController, modifier: Modifier = Modifier, - onAction: (Any?) -> Unit + onAction: (Any?) -> Unit, ) { val backStackEntry by navController.currentBackStackEntryAsState() val currentDestination = backStackEntry?.destination @@ -393,19 +365,21 @@ private fun MainAppBar( val totalNodeCount by viewModel.totalNodeCount.collectAsStateWithLifecycle(0) TopAppBar( title = { - val title = when { - currentDestination == null || currentDestination.isTopLevel() -> stringResource(id = R.string.app_name) + val title = + when { + currentDestination == null || currentDestination.isTopLevel() -> + stringResource(id = R.string.app_name) - currentDestination.hasRoute() -> stringResource(id = R.string.debug_panel) + currentDestination.hasRoute() -> stringResource(id = R.string.debug_panel) - currentDestination.hasRoute() -> stringResource(id = R.string.quick_chat) + currentDestination.hasRoute() -> stringResource(id = R.string.quick_chat) - currentDestination.hasRoute() -> stringResource(id = R.string.share_to) + currentDestination.hasRoute() -> stringResource(id = R.string.share_to) - currentDestination.showLongNameTitle() -> title + currentDestination.showLongNameTitle() -> title - else -> stringResource(id = R.string.app_name) - } + else -> stringResource(id = R.string.app_name) + } Text( text = title, maxLines = 1, @@ -415,17 +389,12 @@ private fun MainAppBar( }, subtitle = { if (currentDestination?.hasRoute() == true) { - Text( - text = stringResource( - R.string.node_count_template, - onlineNodeCount, - totalNodeCount - ), - ) + Text(text = stringResource(R.string.node_count_template, onlineNodeCount, totalNodeCount)) } }, modifier = modifier, - navigationIcon = if (canNavigateBack && currentDestination?.isTopLevel() == false) { + navigationIcon = + if (canNavigateBack && currentDestination?.isTopLevel() == false) { { IconButton(onClick = navigateUp) { Icon( @@ -436,10 +405,7 @@ private fun MainAppBar( } } else { { - IconButton( - enabled = false, - onClick = { }, - ) { + IconButton(enabled = false, onClick = {}) { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.app_icon), contentDescription = stringResource(id = R.string.application_icon), @@ -452,7 +418,7 @@ private fun MainAppBar( viewModel = viewModel, currentDestination = currentDestination, isManaged = isManaged, - onAction = onAction + onAction = onAction, ) }, ) @@ -463,27 +429,18 @@ private fun TopBarActions( viewModel: UIViewModel = hiltViewModel(), currentDestination: NavDestination?, isManaged: Boolean, - onAction: (Any?) -> Unit + onAction: (Any?) -> Unit, ) { val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle() val isConnected by viewModel.isConnected.collectAsStateWithLifecycle(false) AnimatedVisibility(ourNode != null && currentDestination?.isTopLevel() == true && isConnected) { - ourNode?.let { - NodeChip( - node = it, - isThisNode = true, - isConnected = isConnected, - onAction = onAction - ) - } + ourNode?.let { NodeChip(node = it, isThisNode = true, isConnected = isConnected, onAction = onAction) } } currentDestination?.let { when { - it.isTopLevel() -> - MainMenuActions(isManaged, onAction) + it.isTopLevel() -> MainMenuActions(isManaged, onAction) - currentDestination.hasRoute() -> - DebugMenuActions() + currentDestination.hasRoute() -> DebugMenuActions() currentDestination.hasRoute() -> RadioConfigMenuActions(viewModel = viewModel) @@ -494,16 +451,10 @@ private fun TopBarActions( } @Composable -private fun MainMenuActions( - isManaged: Boolean, - onAction: (MainMenuAction) -> Unit -) { +private fun MainMenuActions(isManaged: Boolean, onAction: (MainMenuAction) -> Unit) { var showMenu by remember { mutableStateOf(false) } IconButton(onClick = { showMenu = true }) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.overflow_menu), - ) + Icon(imageVector = Icons.Default.MoreVert, contentDescription = stringResource(R.string.overflow_menu)) } DropdownMenu( @@ -518,7 +469,8 @@ private fun MainMenuActions( onAction(action) showMenu = false }, - enabled = when (action) { + enabled = + when (action) { MainMenuAction.RADIO_CONFIG -> !isManaged else -> true }, @@ -528,46 +480,35 @@ private fun MainMenuActions( } @Composable -private fun MeshService.ConnectionState.getConnectionColor(): Color { - return when (this) { - MeshService.ConnectionState.CONNECTED -> Color(color = 0xFF30C047) - MeshService.ConnectionState.DEVICE_SLEEP -> MaterialTheme.colorScheme.tertiary - MeshService.ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.error - } +private fun MeshService.ConnectionState.getConnectionColor(): Color = when (this) { + MeshService.ConnectionState.CONNECTED -> colorScheme.StatusGreen + MeshService.ConnectionState.DEVICE_SLEEP -> colorScheme.StatusYellow + MeshService.ConnectionState.DISCONNECTED -> colorScheme.StatusRed } -private fun MeshService.ConnectionState.getConnectionIcon(): ImageVector { - return when (this) { - MeshService.ConnectionState.CONNECTED -> Icons.TwoTone.CloudDone - MeshService.ConnectionState.DEVICE_SLEEP -> Icons.TwoTone.CloudUpload - MeshService.ConnectionState.DISCONNECTED -> Icons.TwoTone.CloudOff - } +private fun MeshService.ConnectionState.getConnectionIcon(): ImageVector = when (this) { + MeshService.ConnectionState.CONNECTED -> Icons.TwoTone.CloudDone + MeshService.ConnectionState.DEVICE_SLEEP -> Icons.TwoTone.CloudUpload + MeshService.ConnectionState.DISCONNECTED -> Icons.TwoTone.CloudOff } @Composable -private fun MeshService.ConnectionState.getTooltipString(): String { - return when (this) { - MeshService.ConnectionState.CONNECTED -> stringResource(R.string.connected) - MeshService.ConnectionState.DEVICE_SLEEP -> stringResource(R.string.device_sleeping) - MeshService.ConnectionState.DISCONNECTED -> stringResource(R.string.disconnected) - } +private fun MeshService.ConnectionState.getTooltipString(): String = when (this) { + MeshService.ConnectionState.CONNECTED -> stringResource(R.string.connected) + MeshService.ConnectionState.DEVICE_SLEEP -> stringResource(R.string.device_sleeping) + MeshService.ConnectionState.DISCONNECTED -> stringResource(R.string.disconnected) } @Composable -private fun TopLevelNavIcon( - dest: TopLevelDestination, - connectionState: MeshService.ConnectionState -) { +private fun TopLevelNavIcon(dest: TopLevelDestination, connectionState: MeshService.ConnectionState) { when (dest) { - TopLevelDestination.Connections -> Icon( - imageVector = connectionState.getConnectionIcon(), - contentDescription = stringResource(id = dest.label), - tint = connectionState.getConnectionColor(), - ) + TopLevelDestination.Connections -> + Icon( + imageVector = connectionState.getConnectionIcon(), + contentDescription = stringResource(id = dest.label), + tint = connectionState.getConnectionColor(), + ) - else -> Icon( - imageVector = dest.icon, - contentDescription = stringResource(id = dest.label), - ) + else -> Icon(imageVector = dest.icon, contentDescription = stringResource(id = dest.label)) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/IndoorAirQuality.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/IndoorAirQuality.kt index ac87c4f33..8466e185b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/IndoorAirQuality.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/IndoorAirQuality.kt @@ -58,42 +58,50 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.geeksville.mesh.R +import com.geeksville.mesh.ui.common.theme.IAQColors.IAQDangerouslyPolluted +import com.geeksville.mesh.ui.common.theme.IAQColors.IAQExcellent +import com.geeksville.mesh.ui.common.theme.IAQColors.IAQExtremelyPolluted +import com.geeksville.mesh.ui.common.theme.IAQColors.IAQGood +import com.geeksville.mesh.ui.common.theme.IAQColors.IAQHeavilyPolluted +import com.geeksville.mesh.ui.common.theme.IAQColors.IAQLightlyPolluted +import com.geeksville.mesh.ui.common.theme.IAQColors.IAQModeratelyPolluted +import com.geeksville.mesh.ui.common.theme.IAQColors.IAQSeverelyPolluted @Suppress("MagicNumber") enum class Iaq(val color: Color, val description: String, val range: IntRange) { - Excellent(Color(0xFF00E400), "Excellent", 0..50), - Good(Color(0xFF92D050), "Good", 51..100), - LightlyPolluted(Color(0xFFFFFF00), "Lightly Polluted", 101..150), - ModeratelyPolluted(Color(0xFFFF7300), "Moderately Polluted", 151..200), - HeavilyPolluted(Color(0xFFFF0000), "Heavily Polluted", 201..300), - SeverelyPolluted(Color(0xFF99004C), "Severely Polluted", 301..400), - ExtremelyPolluted(Color(0xFF663300), "Extremely Polluted", 401..500), - DangerouslyPolluted(Color(0xFF663300), "Dangerously Polluted", 501..Int.MAX_VALUE) + Excellent(IAQExcellent, "Excellent", 0..50), + Good(IAQGood, "Good", 51..100), + LightlyPolluted(IAQLightlyPolluted, "Lightly Polluted", 101..150), + ModeratelyPolluted(IAQModeratelyPolluted, "Moderately Polluted", 151..200), + HeavilyPolluted(IAQHeavilyPolluted, "Heavily Polluted", 201..300), + SeverelyPolluted(IAQSeverelyPolluted, "Severely Polluted", 301..400), + ExtremelyPolluted(IAQExtremelyPolluted, "Extremely Polluted", 401..500), + DangerouslyPolluted(IAQDangerouslyPolluted, "Dangerously Polluted", 501..Int.MAX_VALUE), } -fun getIaq(iaq: Int): Iaq { - return when { - iaq in Iaq.Excellent.range -> Iaq.Excellent - iaq in Iaq.Good.range -> Iaq.Good - iaq in Iaq.LightlyPolluted.range -> Iaq.LightlyPolluted - iaq in Iaq.ModeratelyPolluted.range -> Iaq.ModeratelyPolluted - iaq in Iaq.HeavilyPolluted.range -> Iaq.HeavilyPolluted - iaq in Iaq.SeverelyPolluted.range -> Iaq.SeverelyPolluted - iaq in Iaq.ExtremelyPolluted.range -> Iaq.ExtremelyPolluted - else -> Iaq.DangerouslyPolluted - } +fun getIaq(iaq: Int): Iaq = when { + iaq in Iaq.Excellent.range -> Iaq.Excellent + iaq in Iaq.Good.range -> Iaq.Good + iaq in Iaq.LightlyPolluted.range -> Iaq.LightlyPolluted + iaq in Iaq.ModeratelyPolluted.range -> Iaq.ModeratelyPolluted + iaq in Iaq.HeavilyPolluted.range -> Iaq.HeavilyPolluted + iaq in Iaq.SeverelyPolluted.range -> Iaq.SeverelyPolluted + iaq in Iaq.ExtremelyPolluted.range -> Iaq.ExtremelyPolluted + else -> Iaq.DangerouslyPolluted } -private fun getIaqDescriptionWithRange(iaqEnum: Iaq): String { - return if (iaqEnum.range.last == Int.MAX_VALUE) { - "${iaqEnum.description} (${iaqEnum.range.first}+)" - } else { - "${iaqEnum.description} (${iaqEnum.range.first}-${iaqEnum.range.last})" - } +private fun getIaqDescriptionWithRange(iaqEnum: Iaq): String = if (iaqEnum.range.last == Int.MAX_VALUE) { + "${iaqEnum.description} (${iaqEnum.range.first}+)" +} else { + "${iaqEnum.description} (${iaqEnum.range.first}-${iaqEnum.range.last})" } enum class IaqDisplayMode { - Pill, Dot, Text, Gauge, Gradient + Pill, + Dot, + Text, + Gauge, + Gradient, } @Suppress("LongMethod", "UnusedPrivateProperty") @@ -101,36 +109,28 @@ enum class IaqDisplayMode { fun IndoorAirQuality(iaq: Int, displayMode: IaqDisplayMode = IaqDisplayMode.Pill) { var isLegendOpen by remember { mutableStateOf(false) } val iaqEnum = getIaq(iaq) - val gradient = Brush.linearGradient( - colors = Iaq.entries.map { it.color }, - ) + val gradient = Brush.linearGradient(colors = Iaq.entries.map { it.color }) Column { when (displayMode) { IaqDisplayMode.Pill -> { Box( - modifier = Modifier - .clip(RoundedCornerShape(10.dp)) + modifier = + Modifier.clip(RoundedCornerShape(10.dp)) .background(iaqEnum.color) .width(125.dp) .height(30.dp) - .clickable { isLegendOpen = true } + .clickable { isLegendOpen = true }, ) { Row( - modifier = Modifier - .padding(4.dp) - .align(Alignment.CenterStart), - verticalAlignment = Alignment.CenterVertically + modifier = Modifier.padding(4.dp).align(Alignment.CenterStart), + verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = "IAQ $iaq", - color = Color.White, - fontWeight = FontWeight.Bold - ) + Text(text = "IAQ $iaq", color = Color.White, fontWeight = FontWeight.Bold) Icon( imageVector = if (iaq < 100) Icons.Default.ThumbUp else Icons.Filled.Warning, contentDescription = stringResource(R.string.air_quality_icon), - tint = Color.White + tint = Color.White, ) } } @@ -141,11 +141,7 @@ fun IndoorAirQuality(iaq: Int, displayMode: IaqDisplayMode = IaqDisplayMode.Pill Row(verticalAlignment = Alignment.CenterVertically) { Text(text = "$iaq") Spacer(modifier = Modifier.width(4.dp)) - Box( - modifier = Modifier - .size(10.dp) - .background(iaqEnum.color, shape = CircleShape) - ) + Box(modifier = Modifier.size(10.dp).background(iaqEnum.color, shape = CircleShape)) } } } @@ -154,18 +150,16 @@ fun IndoorAirQuality(iaq: Int, displayMode: IaqDisplayMode = IaqDisplayMode.Pill Text( text = getIaqDescriptionWithRange(iaqEnum), fontSize = 12.sp, - modifier = Modifier.clickable { isLegendOpen = true } + modifier = Modifier.clickable { isLegendOpen = true }, ) } IaqDisplayMode.Gauge -> { CircularProgressIndicator( progress = iaq / 500f, - modifier = Modifier - .size(60.dp) - .clickable { isLegendOpen = true }, + modifier = Modifier.size(60.dp).clickable { isLegendOpen = true }, strokeWidth = 8.dp, - color = iaqEnum.color + color = iaqEnum.color, ) Text(text = "$iaq") } @@ -173,13 +167,11 @@ fun IndoorAirQuality(iaq: Int, displayMode: IaqDisplayMode = IaqDisplayMode.Pill IaqDisplayMode.Gradient -> { Row( horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.clickable { isLegendOpen = true } + modifier = Modifier.clickable { isLegendOpen = true }, ) { LinearProgressIndicator( progress = iaq / 500f, - modifier = Modifier - .fillMaxWidth() - .height(20.dp), + modifier = Modifier.fillMaxWidth().height(20.dp), color = iaqEnum.color, ) Spacer(modifier = Modifier.width(8.dp)) @@ -191,14 +183,10 @@ fun IndoorAirQuality(iaq: Int, displayMode: IaqDisplayMode = IaqDisplayMode.Pill AlertDialog( onDismissRequest = { isLegendOpen = false }, shape = RoundedCornerShape(16.dp), - text = { - IAQScale() - }, + text = { IAQScale() }, confirmButton = { - TextButton(onClick = { isLegendOpen = false }) { - Text(text = stringResource(id = R.string.close)) - } - } + TextButton(onClick = { isLegendOpen = false }) { Text(text = stringResource(id = R.string.close)) } + }, ) } } @@ -210,28 +198,17 @@ fun IndoorAirQuality(iaq: Int, displayMode: IaqDisplayMode = IaqDisplayMode.Pill @Composable fun IAQScale(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .padding(16.dp), - horizontalAlignment = Alignment.Start - ) { + Column(modifier = modifier.padding(16.dp), horizontalAlignment = Alignment.Start) { Text( text = stringResource(R.string.indoor_air_quality_iaq), - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - ), + style = + MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold, textAlign = TextAlign.Center), modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(16.dp)) for (iaq in Iaq.entries) { Row(verticalAlignment = Alignment.CenterVertically) { - Box( - modifier = Modifier - .size(20.dp, 15.dp) - .clip(RoundedCornerShape(5.dp)) - .background(iaq.color) - ) + Box(modifier = Modifier.size(20.dp, 15.dp).clip(RoundedCornerShape(5.dp)).background(iaq.color)) Spacer(modifier = Modifier.width(8.dp)) Text(getIaqDescriptionWithRange(iaq), style = MaterialTheme.typography.bodyMedium) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/LoraSignalIndicator.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/LoraSignalIndicator.kt index efeebd655..35722835e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/LoraSignalIndicator.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/LoraSignalIndicator.kt @@ -36,16 +36,22 @@ import androidx.compose.material.icons.filled.SignalCellularAlt1Bar import androidx.compose.material.icons.filled.SignalCellularAlt2Bar import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp import com.geeksville.mesh.R +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusOrange +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow private const val SNR_GOOD_THRESHOLD = -7f private const val SNR_FAIR_THRESHOLD = -15f @@ -53,29 +59,27 @@ private const val SNR_FAIR_THRESHOLD = -15f private const val RSSI_GOOD_THRESHOLD = -115 private const val RSSI_FAIR_THRESHOLD = -126 +@Stable private enum class Quality( - val nameRes: Int, - val imageVector: ImageVector, - val color: Color + @Stable val nameRes: Int, + @Stable val imageVector: ImageVector, + @Stable val color: @Composable () -> Color, ) { - NONE(R.string.none_quality, Icons.Default.SignalCellularAlt1Bar, Color.Red), - BAD(R.string.bad, Icons.Default.SignalCellularAlt2Bar, Color(red = 247, green = 147, blue = 26)), - FAIR(R.string.fair, Icons.Default.SignalCellularAlt, Color(red = 255, green = 230, blue = 0)), - GOOD(R.string.good, Icons.Default.SignalCellular4Bar, Color(0xFF30C047)) + NONE(R.string.none_quality, Icons.Default.SignalCellularAlt1Bar, { colorScheme.StatusRed }), + BAD(R.string.bad, Icons.Default.SignalCellularAlt2Bar, { colorScheme.StatusOrange }), + FAIR(R.string.fair, Icons.Default.SignalCellularAlt, { colorScheme.StatusYellow }), + GOOD(R.string.good, Icons.Default.SignalCellular4Bar, { colorScheme.StatusGreen }), } /** - * Displays the `snr` and `rssi` color coded based on the signal quality, along with - * a human readable description and related icon. + * Displays the `snr` and `rssi` color coded based on the signal quality, along with a human readable description and + * related icon. */ @OptIn(ExperimentalLayoutApi::class) @Composable fun NodeSignalQuality(snr: Float, rssi: Int, modifier: Modifier = Modifier) { val quality = determineSignalQuality(snr, rssi) - FlowRow( - modifier = modifier, - maxLines = 1, - ) { + FlowRow(modifier = modifier, maxLines = 1) { Snr(snr) Spacer(Modifier.width(8.dp)) Rssi(rssi) @@ -89,44 +93,34 @@ fun NodeSignalQuality(snr: Float, rssi: Int, modifier: Modifier = Modifier) { Icon( imageVector = quality.imageVector, contentDescription = stringResource(R.string.signal_quality), - tint = quality.color + tint = quality.color.invoke(), ) } } -/** - * Displays the `snr` and `rssi` with color depending on the values respectively. - */ +/** Displays the `snr` and `rssi` with color depending on the values respectively. */ @Composable fun SnrAndRssi(snr: Float, rssi: Int) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Snr(snr) Rssi(rssi) } } -/** - * Displays a human readable description and icon representing the signal quality. - */ +/** Displays a human readable description and icon representing the signal quality. */ @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) + modifier = Modifier.fillMaxSize().padding(8.dp), ) { Icon( imageVector = quality.imageVector, contentDescription = stringResource(R.string.signal_quality), - tint = quality.color + tint = quality.color.invoke(), ) Text(text = "${stringResource(R.string.signal)} ${stringResource(quality.nameRes)}") } @@ -134,35 +128,29 @@ fun LoraSignalIndicator(snr: Float, rssi: Int) { @Composable fun Snr(snr: Float, fontSize: TextUnit = MaterialTheme.typography.labelLarge.fontSize) { - val color: Color = if (snr > SNR_GOOD_THRESHOLD) { - Quality.GOOD.color - } else if (snr > SNR_FAIR_THRESHOLD) { - Quality.FAIR.color - } else { - Quality.BAD.color - } + val color: Color = + if (snr > SNR_GOOD_THRESHOLD) { + Quality.GOOD.color.invoke() + } else if (snr > SNR_FAIR_THRESHOLD) { + Quality.FAIR.color.invoke() + } else { + 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, fontSize = fontSize) } @Composable fun Rssi(rssi: Int, fontSize: TextUnit = MaterialTheme.typography.labelLarge.fontSize) { - val color: Color = if (rssi > RSSI_GOOD_THRESHOLD) { - Quality.GOOD.color - } else if (rssi > RSSI_FAIR_THRESHOLD) { - Quality.FAIR.color - } else { - Quality.BAD.color - } - Text( - text = "%s %ddBm".format(stringResource(id = R.string.rssi), rssi), - color = color, - fontSize = fontSize - ) + val color: Color = + if (rssi > RSSI_GOOD_THRESHOLD) { + Quality.GOOD.color.invoke() + } else if (rssi > RSSI_FAIR_THRESHOLD) { + Quality.FAIR.color.invoke() + } else { + Quality.BAD.color.invoke() + } + Text(text = "%s %ddBm".format(stringResource(id = R.string.rssi), rssi), color = color, fontSize = fontSize) } private fun determineSignalQuality(snr: Float, rssi: Int): Quality = when { diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt index 9abc5ecf0..a95788346 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt @@ -40,6 +40,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -61,12 +62,15 @@ import com.geeksville.mesh.AppOnlyProtos import com.geeksville.mesh.R import com.geeksville.mesh.model.Channel import com.geeksville.mesh.model.getChannel +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow private const val PRECISE_POSITION_BITS = 32 /** - * Represents the various visual states of the security icon as an enum. - * Each enum constant encapsulates the icon, color, descriptive text, and optional badge details. + * Represents the various visual states of the security icon as an enum. Each enum constant encapsulates the icon, + * color, descriptive text, and optional badge details. * * @property icon The primary vector graphic for the icon. * @property color The tint color for the primary icon. @@ -78,53 +82,54 @@ private const val PRECISE_POSITION_BITS = 32 @Immutable enum class SecurityState( @Stable val icon: ImageVector, - @Stable val color: Color, + @Stable val color: @Composable () -> Color, @StringRes val descriptionResId: Int, @StringRes val helpTextResId: Int, @Stable val badgeIcon: ImageVector? = null, - @Stable val badgeIconColor: Color? = null, + @Stable val badgeIconColor: @Composable () -> Color? = { null }, ) { /** State for a secure channel (green lock). */ SECURE( icon = Icons.Filled.Lock, - color = Color.Green, + color = { colorScheme.StatusGreen }, descriptionResId = R.string.security_icon_secure, helpTextResId = R.string.security_icon_help_green_lock, ), - /** State for an insecure channel, - * not used for precise location, - * and MQTT not the primary concern for a higher warning. - * (yellow open lock) */ + /** + * State for an insecure channel, not used for precise location, and MQTT not the primary concern for a higher + * warning. (yellow open lock) + */ INSECURE_NO_PRECISE( icon = Icons.Filled.LockOpen, - color = Color.Yellow, + color = { colorScheme.StatusYellow }, descriptionResId = R.string.security_icon_insecure_no_precise, helpTextResId = R.string.security_icon_help_yellow_open_lock, ), - /** State for an insecure channel - * with precise location enabled, - * but MQTT not causing the highest - * warning. (red open lock) */ + /** + * State for an insecure channel with precise location enabled, but MQTT not causing the highest warning. (red open + * lock) + */ INSECURE_PRECISE_ONLY( icon = Icons.Filled.LockOpen, - color = Color.Red, + color = { colorScheme.StatusRed }, descriptionResId = R.string.security_icon_insecure_precise_only, helpTextResId = R.string.security_icon_help_red_open_lock, ), - /** State indicating an insecure channel - * with precise location and MQTT enabled - * (red open lock with yellow warning badge). */ + /** + * State indicating an insecure channel with precise location and MQTT enabled (red open lock with yellow warning + * badge). + */ INSECURE_PRECISE_MQTT_WARNING( icon = Icons.Filled.LockOpen, - color = Color.Red, + color = { colorScheme.StatusRed }, descriptionResId = R.string.security_icon_warning_precise_mqtt, helpTextResId = R.string.security_icon_help_warning_precise_mqtt, badgeIcon = Icons.Filled.Warning, - badgeIconColor = Color.Yellow, - ) + badgeIconColor = { colorScheme.StatusYellow }, + ), } /** @@ -155,8 +160,7 @@ private fun SecurityIconDisplay( Icon( imageVector = badgeIcon, contentDescription = stringResource(R.string.security_icon_badge_warning_description), - tint = badgeIconColor - ?: MaterialTheme.colorScheme.onError, // Default for contrast + tint = badgeIconColor ?: MaterialTheme.colorScheme.onError, // Default for contrast modifier = Modifier.size(16.dp), // Adjusted badge icon size ) } @@ -164,17 +168,13 @@ private fun SecurityIconDisplay( }, modifier = modifier, ) { - Icon( - imageVector = icon, - contentDescription = contentDescription, - tint = mainIconTint, - ) + Icon(imageVector = icon, contentDescription = contentDescription, tint = mainIconTint) } } /** - * Determines the [SecurityState] based on channel properties. - * The priority of states is: MQTT warning, then secure, then insecure variations. + * Determines the [SecurityState] based on channel properties. The priority of states is: MQTT warning, then secure, + * then insecure variations. * * @param isLowEntropyKey True if the channel uses a low entropy key (not securely encrypted). * @param isPreciseLocation True if precise location is enabled. @@ -196,15 +196,13 @@ private fun determineSecurityState( } /** - * Displays an icon representing the security status of a channel. - * Clicking the icon shows a detailed help dialog. + * Displays an icon representing the security status of a channel. Clicking the icon shows a detailed help dialog. * * @param securityState The current [SecurityState] to display. - * @param baseContentDescription The base content description for the icon, to which the specific - * state description will be appended. Defaults to a generic security icon description. - * @param externalOnClick Optional lambda to be invoked when the icon is clicked, - * in addition to its primary action (showing a help dialog). - * This allows callers to inject custom side effects. + * @param baseContentDescription The base content description for the icon, to which the specific state description will + * be appended. Defaults to a generic security icon description. + * @param externalOnClick Optional lambda to be invoked when the icon is clicked, in addition to its primary action + * (showing a help dialog). This allows callers to inject custom side effects. */ @Composable fun SecurityIcon( @@ -213,8 +211,7 @@ fun SecurityIcon( externalOnClick: (() -> Unit)? = null, ) { var showHelpDialog by rememberSaveable { mutableStateOf(false) } - val fullContentDescription = - baseContentDescription + " " + stringResource(id = securityState.descriptionResId) + val fullContentDescription = baseContentDescription + " " + stringResource(id = securityState.descriptionResId) IconButton( onClick = { @@ -224,18 +221,15 @@ fun SecurityIcon( ) { SecurityIconDisplay( icon = securityState.icon, - mainIconTint = securityState.color, + mainIconTint = securityState.color.invoke(), contentDescription = fullContentDescription, badgeIcon = securityState.badgeIcon, - badgeIconColor = securityState.badgeIconColor, + badgeIconColor = securityState.badgeIconColor.invoke(), ) } if (showHelpDialog) { - SecurityHelpDialog( - securityState = securityState, - onDismiss = { showHelpDialog = false }, - ) + SecurityHelpDialog(securityState = securityState, onDismiss = { showHelpDialog = false }) } } @@ -246,9 +240,8 @@ fun SecurityIcon( * @param isPreciseLocation Whether the channel has precise location enabled. Defaults to false. * @param isMqttEnabled Whether MQTT is enabled for the channel. Defaults to false. * @param baseContentDescription The base content description for the icon. - * @param externalOnClick Optional lambda to be invoked when the icon is clicked, - * in addition to its primary action (showing a help dialog). - * This allows callers to inject custom side effects. + * @param externalOnClick Optional lambda to be invoked when the icon is clicked, in addition to its primary action + * (showing a help dialog). This allows callers to inject custom side effects. */ @Composable fun SecurityIcon( @@ -267,13 +260,16 @@ fun SecurityIcon( } /** Extension property to check if the channel uses a low entropy PSK (not securely encrypted). */ -val Channel.isLowEntropyKey: Boolean get() = settings.psk.size() <= 1 +val Channel.isLowEntropyKey: Boolean + get() = settings.psk.size() <= 1 /** Extension property to check if the channel has precise location enabled. */ -val Channel.isPreciseLocation: Boolean get() = settings.moduleSettings.positionPrecision == PRECISE_POSITION_BITS +val Channel.isPreciseLocation: Boolean + get() = settings.moduleSettings.positionPrecision == PRECISE_POSITION_BITS /** Extension property to check if MQTT is enabled for the channel. */ -val Channel.isMqttEnabled: Boolean get() = settings.uplinkEnabled +val Channel.isMqttEnabled: Boolean + get() = settings.uplinkEnabled /** * Overload for [SecurityIcon] that takes a [Channel] object to determine its security state. @@ -296,8 +292,8 @@ fun SecurityIcon( ) /** - * Overload for [SecurityIcon] that takes an [AppOnlyProtos.ChannelSet] and a channel index. - * If the channel at the given index is not found, nothing is rendered. + * Overload for [SecurityIcon] that takes an [AppOnlyProtos.ChannelSet] and a channel index. If the channel at the given + * index is not found, nothing is rendered. * * @param channelSet The set of channels. * @param channelIndex The index of the channel within the set. @@ -321,9 +317,9 @@ fun SecurityIcon( } /** - * Overload for [SecurityIcon] that takes an [AppOnlyProtos.ChannelSet] and a channel name. - * If a channel with the given name is not found, nothing is rendered. - * This overload optimizes lookup by name by memoizing a map of channel names to settings. + * Overload for [SecurityIcon] that takes an [AppOnlyProtos.ChannelSet] and a channel name. If a channel with the given + * name is not found, nothing is rendered. This overload optimizes lookup by name by memoizing a map of channel names to + * settings. * * @param channelSet The set of channels. * @param channelName The name of the channel to find. @@ -337,11 +333,8 @@ fun SecurityIcon( baseContentDescription: String = stringResource(id = R.string.security_icon_description), externalOnClick: (() -> Unit)? = null, ) { - val channelByNameMap = remember(channelSet) { - channelSet.settingsList.associateBy { - Channel(it, channelSet.loraConfig).name - } - } + val channelByNameMap = + remember(channelSet) { channelSet.settingsList.associateBy { Channel(it, channelSet.loraConfig).name } } channelByNameMap[channelName]?.let { channelSetting -> SecurityIcon( @@ -353,21 +346,19 @@ fun SecurityIcon( } /** - * Displays a help dialog explaining the meaning of different security icons. - * The dialog can show details for a specific [SecurityState] or a list of all states. + * Displays a help dialog explaining the meaning of different security icons. The dialog can show details for a specific + * [SecurityState] or a list of all states. * * @param securityState The initial security state to display contextually. * @param onDismiss Lambda invoked when the dialog is dismissed. */ @Composable -private fun SecurityHelpDialog( - securityState: SecurityState, - onDismiss: () -> Unit, -) { +private fun SecurityHelpDialog(securityState: SecurityState, onDismiss: () -> Unit) { var showAll by rememberSaveable { mutableStateOf(false) } AlertDialog( - modifier = if (showAll) { + modifier = + if (showAll) { Modifier.fillMaxSize() } else { Modifier @@ -404,9 +395,7 @@ private fun SecurityHelpDialog( }, ) } - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.security_icon_help_dismiss)) - } + TextButton(onClick = onDismiss) { Text(stringResource(R.string.security_icon_help_dismiss)) } } }, ) @@ -422,24 +411,20 @@ private fun ContextualSecurityState(securityState: SecurityState) { Column(horizontalAlignment = Alignment.CenterHorizontally) { SecurityIconDisplay( icon = securityState.icon, - mainIconTint = securityState.color, + mainIconTint = securityState.color.invoke(), contentDescription = stringResource(securityState.descriptionResId), modifier = Modifier.size(48.dp), badgeIcon = securityState.badgeIcon, - badgeIconColor = securityState.badgeIconColor, + badgeIconColor = securityState.badgeIconColor.invoke(), ) Spacer(Modifier.height(16.dp)) - Text( - text = stringResource(securityState.helpTextResId), - style = MaterialTheme.typography.bodyMedium, - ) + Text(text = stringResource(securityState.helpTextResId), style = MaterialTheme.typography.bodyMedium) } } /** - * Displays a list of all possible security states with their icons and descriptions - * within the help dialog. Iterates over `SecurityState.entries` which is provided - * by the enum class. + * Displays a list of all possible security states with their icons and descriptions within the help dialog. Iterates + * over `SecurityState.entries` which is provided by the enum class. */ @Composable private fun AllSecurityStates() { @@ -447,25 +432,20 @@ private fun AllSecurityStates() { verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.verticalScroll(rememberScrollState()), ) { - SecurityState.entries.forEach { state -> // Uses enum entries + SecurityState.entries.forEach { state -> + // Uses enum entries Row(verticalAlignment = Alignment.CenterVertically) { SecurityIconDisplay( icon = state.icon, - mainIconTint = state.color, + mainIconTint = state.color.invoke(), contentDescription = stringResource(state.descriptionResId), modifier = Modifier.size(48.dp), badgeIcon = state.badgeIcon, - badgeIconColor = state.badgeIconColor, + badgeIconColor = state.badgeIconColor.invoke(), ) Column(modifier = Modifier.padding(start = 16.dp)) { - Text( - text = stringResource(state.descriptionResId), - style = MaterialTheme.typography.titleMedium, - ) - Text( - text = stringResource(state.helpTextResId), - style = MaterialTheme.typography.bodyMedium, - ) + Text(text = stringResource(state.descriptionResId), style = MaterialTheme.typography.titleMedium) + Text(text = stringResource(state.helpTextResId), style = MaterialTheme.typography.bodyMedium) } } if (state != SecurityState.entries.lastOrNull()) { @@ -505,7 +485,8 @@ private fun PreviewMqttEnabled() { @Composable private fun PreviewAllSecurityIconsWithDialog() { var showHelpDialogFor by remember { mutableStateOf(null) } - val stateLabels = remember { // Using SecurityState.entries to build the map keys + val stateLabels = remember { + // Using SecurityState.entries to build the map keys mapOf( SecurityState.SECURE to "Secure", SecurityState.INSECURE_NO_PRECISE to "Insecure (No Precise Location)", @@ -519,30 +500,16 @@ private fun PreviewAllSecurityIconsWithDialog() { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), ) { - Text( - text = "Security Icons Preview (Click for Help)", - style = MaterialTheme.typography.headlineSmall, - ) + Text(text = "Security Icons Preview (Click for Help)", style = MaterialTheme.typography.headlineSmall) - SecurityState.entries.forEach { state -> // Iterate over enum entries - val label = - stateLabels[state] ?: "Unknown State (${state.name})" // Fallback to enum name - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - SecurityIcon( - securityState = state, - externalOnClick = { showHelpDialogFor = state }, - ) + SecurityState.entries.forEach { state -> + // Iterate over enum entries + val label = stateLabels[state] ?: "Unknown State (${state.name})" // Fallback to enum name + Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) { + SecurityIcon(securityState = state, externalOnClick = { showHelpDialogFor = state }) Text(label) } } - showHelpDialogFor?.let { - SecurityHelpDialog( - securityState = it, - onDismiss = { showHelpDialogFor = null }, - ) - } + showHelpDialogFor?.let { SecurityHelpDialog(securityState = it, onDismiss = { showHelpDialogFor = null }) } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/theme/Color.kt b/app/src/main/java/com/geeksville/mesh/ui/common/theme/Color.kt index 561fcfb62..092286b60 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/theme/Color.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/theme/Color.kt @@ -19,18 +19,6 @@ package com.geeksville.mesh.ui.common.theme import androidx.compose.ui.graphics.Color -val MeshtasticGreen = Color(0xFF67EA94) -val MeshtasticAlt = Color(0xFF2C2D3C) - -val Yellow = Color(red = 255, green = 230, blue = 0) -val Orange = Color(red = 247, green = 147, blue = 26) -val Green = Color(0xFF30C047) - -val HyperlinkBlue = Color(0xFF43C3B0) -val InfantryBlue = Color(red = 75, green = 119, blue = 190) -val Purple = Color(0xFF9C27B0) -val Pink = Color(red = 255, green = 102, blue = 204) - val primaryLight = Color(0xFF306A42) val onPrimaryLight = Color(0xFFFFFFFF) val primaryContainerLight = Color(0xFFB3F1BF) diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/theme/CustomColors.kt b/app/src/main/java/com/geeksville/mesh/ui/common/theme/CustomColors.kt new file mode 100644 index 000000000..c99fd7100 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/common/theme/CustomColors.kt @@ -0,0 +1,90 @@ +/* + * 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.common.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +val MeshtasticGreen = Color(0xFF67EA94) +val MeshtasticAlt = Color(0xFF2C2D3C) +val HyperlinkBlue = Color(0xFF43C3B0) + +object IAQColors { + val IAQExcellent = Color(0xFF00E400) + val IAQGood = Color(0xFF92D050) + val IAQLightlyPolluted = Color(0xFFFFFF00) + val IAQModeratelyPolluted = Color(0xFFFF7300) + val IAQHeavilyPolluted = Color(0xFFFF0000) + val IAQSeverelyPolluted = Color(0xFF99004C) + val IAQExtremelyPolluted = Color(0xFF663300) + val IAQDangerouslyPolluted = Color(0xFF663300) +} + +object GraphColors { + val InfantryBlue = Color(red = 75, green = 119, blue = 190) + val Purple = Color(0xFF9C27B0) + val Pink = Color(red = 255, green = 102, blue = 204) + val Orange = Color(0xFFFF8800) + + val Green = Color.Green + val Red = Color.Red + val Blue = Color.Blue + val Yellow = Color.Yellow + val Magenta = Color.Magenta + val Cyan = Color.Cyan +} + +object StatusColors { + val ColorScheme.StatusGreen: Color + @Composable + get() = // If it might change based on theme + if (isSystemInDarkTheme()) { + Color(0xFF28A03B) // Example dark green + } else { + Color(0xFF30C047) + } + + val ColorScheme.StatusYellow: Color + @Composable + get() = + if (isSystemInDarkTheme()) { + Color(0xFFFFC107) + } else { + Color(0xFFFFD54F) + } + + val ColorScheme.StatusOrange: Color + @Composable + get() = + if (isSystemInDarkTheme()) { + Color(0xFFE07000) + } else { + Color(0xFFFF8800) + } + + val ColorScheme.StatusRed: Color + @Composable + get() = // If it might change based on theme + if (isSystemInDarkTheme()) { + Color(0xFFB00020) + } else { + Color(0xFFF44336) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/DeviceListItem.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/DeviceListItem.kt index 521b35743..6434a2314 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/DeviceListItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/DeviceListItem.kt @@ -35,11 +35,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import com.geeksville.mesh.R import com.geeksville.mesh.model.BTScanModel import com.geeksville.mesh.service.MeshService +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable @@ -50,68 +51,70 @@ fun DeviceListItem( onSelect: () -> Unit, modifier: Modifier = Modifier, ) { - val icon = if (device.isBLE) { - Icons.Default.Bluetooth - } else if (device.isUSB) { - Icons.Default.Usb - } else if (device.isTCP) { - Icons.Default.Wifi - } else if (device.isDisconnect) { // This is the "Disconnect" entry type - Icons.Default.Cancel - } else { - Icons.Default.Add - } - - val contentDescription = if (device.isBLE) { - stringResource(R.string.bluetooth) - } else if (device.isUSB) { - stringResource(R.string.serial) - } else if (device.isTCP) { - stringResource(R.string.network) - } else if (device.isDisconnect) { // This is the "Disconnect" entry type - stringResource(R.string.disconnect) - } else { - stringResource(R.string.add) - } - - val colors = when { - selected && device.isDisconnect -> { - ListItemDefaults.colors( - containerColor = MaterialTheme.colorScheme.errorContainer, - headlineColor = MaterialTheme.colorScheme.onErrorContainer, - leadingIconColor = MaterialTheme.colorScheme.onErrorContainer, - supportingColor = MaterialTheme.colorScheme.onErrorContainer, - trailingIconColor = MaterialTheme.colorScheme.onErrorContainer, - ) + val icon = + if (device.isBLE) { + Icons.Default.Bluetooth + } else if (device.isUSB) { + Icons.Default.Usb + } else if (device.isTCP) { + Icons.Default.Wifi + } else if (device.isDisconnect) { // This is the "Disconnect" entry type + Icons.Default.Cancel + } else { + Icons.Default.Add } - selected -> { // Standard selection for other device types - ListItemDefaults.colors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - headlineColor = MaterialTheme.colorScheme.onPrimaryContainer, - leadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer, - trailingIconColor = when (connectionState) { - MeshService.ConnectionState.CONNECTED -> Color(color = 0xFF30C047) - MeshService.ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.error - else -> MaterialTheme.colorScheme.onPrimaryContainer // Fallback for other states (e.g. connecting) - }, - ) + val contentDescription = + if (device.isBLE) { + stringResource(R.string.bluetooth) + } else if (device.isUSB) { + stringResource(R.string.serial) + } else if (device.isTCP) { + stringResource(R.string.network) + } else if (device.isDisconnect) { // This is the "Disconnect" entry type + stringResource(R.string.disconnect) + } else { + stringResource(R.string.add) } - else -> { - ListItemDefaults.colors() + val colors = + when { + selected && device.isDisconnect -> { + ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.errorContainer, + headlineColor = MaterialTheme.colorScheme.onErrorContainer, + leadingIconColor = MaterialTheme.colorScheme.onErrorContainer, + supportingColor = MaterialTheme.colorScheme.onErrorContainer, + trailingIconColor = MaterialTheme.colorScheme.onErrorContainer, + ) + } + + selected -> { // Standard selection for other device types + ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + headlineColor = MaterialTheme.colorScheme.onPrimaryContainer, + leadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer, + trailingIconColor = + when (connectionState) { + MeshService.ConnectionState.CONNECTED -> MaterialTheme.colorScheme.StatusGreen + MeshService.ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.StatusRed + else -> + MaterialTheme.colorScheme + .onPrimaryContainer // Fallback for other states (e.g. connecting) + }, + ) + } + + else -> { + ListItemDefaults.colors() + } } - } val useSelectable = modifier == Modifier ListItem( - modifier = if (useSelectable) { - modifier - .fillMaxWidth() - .selectable( - selected = selected, - onClick = onSelect, - ) + modifier = + if (useSelectable) { + modifier.fillMaxWidth().selectable(selected = selected, onClick = onSelect) } else { modifier.fillMaxWidth() }, @@ -119,7 +122,7 @@ fun DeviceListItem( leadingContent = { Icon( icon, // icon is already CloudOff if device.isDisconnect - contentDescription + contentDescription, ) }, supportingContent = { @@ -129,15 +132,9 @@ fun DeviceListItem( }, trailingContent = { if (device.isDisconnect) { - Icon( - imageVector = Icons.Default.CloudOff, - contentDescription = stringResource(R.string.disconnect), - ) + Icon(imageVector = Icons.Default.CloudOff, contentDescription = stringResource(R.string.disconnect)) } else if (connectionState == MeshService.ConnectionState.CONNECTED) { - Icon( - imageVector = Icons.Default.CloudDone, - contentDescription = stringResource(R.string.connected), - ) + Icon(imageVector = Icons.Default.CloudDone, contentDescription = stringResource(R.string.connected)) } else { Icon( imageVector = Icons.Default.CloudQueue, @@ -145,6 +142,6 @@ fun DeviceListItem( ) } }, - colors = colors + colors = colors, ) } 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 a7e17e911..0508bce55 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 @@ -55,6 +55,7 @@ import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -67,75 +68,70 @@ import com.geeksville.mesh.ui.common.components.BatteryInfo import com.geeksville.mesh.ui.common.components.OptionLabel import com.geeksville.mesh.ui.common.components.SlidingSelector import com.geeksville.mesh.ui.common.theme.AppTheme -import com.geeksville.mesh.ui.common.theme.Orange +import com.geeksville.mesh.ui.common.theme.GraphColors.Cyan +import com.geeksville.mesh.ui.common.theme.GraphColors.Green +import com.geeksville.mesh.ui.common.theme.GraphColors.Magenta +import com.geeksville.mesh.ui.common.theme.GraphColors.Orange import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT import com.geeksville.mesh.ui.metrics.CommonCharts.MAX_PERCENT_VALUE import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC import com.geeksville.mesh.util.GraphUtil import com.geeksville.mesh.util.GraphUtil.createPath import com.geeksville.mesh.util.GraphUtil.plotPoint -import androidx.compose.ui.tooling.preview.PreviewLightDark private enum class Device(val color: Color) { - BATTERY(Color.Green), - CH_UTIL(Color.Magenta), - AIR_UTIL(Color.Cyan) + BATTERY(Green), + CH_UTIL(Magenta), + AIR_UTIL(Cyan), } private const val CHART_WEIGHT = 1f private const val Y_AXIS_WEIGHT = 0.1f private const val CHART_WIDTH_RATIO = CHART_WEIGHT / (CHART_WEIGHT + Y_AXIS_WEIGHT) -private val LEGEND_DATA = listOf( - LegendData(nameRes = R.string.battery, color = Device.BATTERY.color, isLine = true), - LegendData(nameRes = R.string.channel_utilization, color = Device.CH_UTIL.color), - LegendData(nameRes = R.string.air_utilization, color = Device.AIR_UTIL.color), -) +private val LEGEND_DATA = + listOf( + LegendData(nameRes = R.string.battery, color = Device.BATTERY.color, isLine = true), + LegendData(nameRes = R.string.channel_utilization, color = Device.CH_UTIL.color), + LegendData(nameRes = R.string.air_utilization, color = Device.AIR_UTIL.color), + ) @Composable -fun DeviceMetricsScreen( - viewModel: MetricsViewModel = hiltViewModel(), -) { +fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) { val state by viewModel.state.collectAsStateWithLifecycle() var displayInfoDialog by remember { mutableStateOf(false) } val selectedTimeFrame by viewModel.timeFrame.collectAsState() val data = state.deviceMetricsFiltered(selectedTimeFrame) Column { - if (displayInfoDialog) { LegendInfoDialog( - pairedRes = listOf( + pairedRes = + listOf( Pair(R.string.channel_utilization, R.string.ch_util_definition), - Pair(R.string.air_utilization, R.string.air_util_definition) + Pair(R.string.air_utilization, R.string.air_util_definition), ), - onDismiss = { displayInfoDialog = false } + onDismiss = { displayInfoDialog = false }, ) } DeviceMetricsChart( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(fraction = 0.33f), + modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f), data.reversed(), selectedTimeFrame, - promptInfoDialog = { displayInfoDialog = true } + promptInfoDialog = { displayInfoDialog = true }, ) SlidingSelector( TimeFrame.entries.toList(), selectedTimeFrame, - onOptionSelected = { viewModel.setTimeFrame(it) } + onOptionSelected = { viewModel.setTimeFrame(it) }, ) { OptionLabel(stringResource(it.strRes)) } /* Device Metric Cards */ - LazyColumn( - modifier = Modifier.fillMaxSize() - ) { - items(data) { telemetry -> DeviceMetricsCard(telemetry) } - } + LazyColumn(modifier = Modifier.fillMaxSize()) { items(data) { telemetry -> DeviceMetricsCard(telemetry) } } } } @@ -145,26 +141,20 @@ private fun DeviceMetricsChart( modifier: Modifier = Modifier, telemetries: List, selectedTime: TimeFrame, - promptInfoDialog: () -> Unit + promptInfoDialog: () -> Unit, ) { val graphColor = MaterialTheme.colorScheme.onSurface ChartHeader(amount = telemetries.size) if (telemetries.isEmpty()) return - val (oldest, newest) = remember(key1 = telemetries) { - Pair( - telemetries.minBy { it.time }, - telemetries.maxBy { it.time } - ) - } + val (oldest, newest) = + remember(key1 = telemetries) { Pair(telemetries.minBy { it.time }, telemetries.maxBy { it.time }) } val timeDiff = newest.time - oldest.time val scrollState = rememberScrollState() val screenWidth = LocalWindowInfo.current.containerSize.width - val dp by remember(key1 = selectedTime) { - mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong())) - } + val dp by remember(key1 = selectedTime) { mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong())) } // Calculate visible time range based on scroll position and chart width val visibleTimeRange = run { @@ -180,21 +170,15 @@ private fun DeviceMetricsChart( visibleOldest to visibleNewest } - TimeLabels( - oldest = visibleTimeRange.first, - newest = visibleTimeRange.second, - ) + TimeLabels(oldest = visibleTimeRange.first, newest = visibleTimeRange.second) Spacer(modifier = Modifier.height(16.dp)) Row { Box( contentAlignment = Alignment.TopStart, - modifier = Modifier - .horizontalScroll(state = scrollState, reverseScrolling = true) - .weight(weight = 1f) + modifier = Modifier.horizontalScroll(state = scrollState, reverseScrolling = true).weight(weight = 1f), ) { - /* * The order of the colors are with respect to the ChUtil. * 25 - 49 Orange @@ -205,16 +189,10 @@ private fun DeviceMetricsChart( lineColors = listOf(graphColor, Orange, Color.Red, graphColor, graphColor), ) - TimeAxisOverlay( - modifier.width(dp), - oldest = oldest.time, - newest = newest.time, - selectedTime.lineInterval() - ) + TimeAxisOverlay(modifier.width(dp), oldest = oldest.time, newest = newest.time, selectedTime.lineInterval()) /* Plot Battery Line, ChUtil, and AirUtilTx */ Canvas(modifier = modifier.width(dp)) { - val height = size.height val width = size.width for (i in telemetries.indices) { @@ -230,7 +208,7 @@ private fun DeviceMetricsChart( color = Device.CH_UTIL.color, x = x, value = telemetry.deviceMetrics.channelUtilization, - divisor = MAX_PERCENT_VALUE + divisor = MAX_PERCENT_VALUE, ) /* Air Utilization Transmit */ @@ -239,7 +217,7 @@ private fun DeviceMetricsChart( color = Device.AIR_UTIL.color, x = x, value = telemetry.deviceMetrics.airUtilTx, - divisor = MAX_PERCENT_VALUE + divisor = MAX_PERCENT_VALUE, ) } @@ -247,37 +225,30 @@ private fun DeviceMetricsChart( var index = 0 while (index < telemetries.size) { val path = Path() - index = createPath( - telemetries = telemetries, - index = index, - path = path, - oldestTime = oldest.time, - timeRange = timeDiff, - width = width, - timeThreshold = selectedTime.timeThreshold() - ) { i -> - val telemetry = telemetries.getOrNull(i) ?: telemetries.last() - val ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE - val y = height - (ratio * height) - return@createPath y - } + index = + createPath( + telemetries = telemetries, + index = index, + path = path, + oldestTime = oldest.time, + timeRange = timeDiff, + width = width, + timeThreshold = selectedTime.timeThreshold(), + ) { i -> + val telemetry = telemetries.getOrNull(i) ?: telemetries.last() + val ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE + val y = height - (ratio * height) + return@createPath y + } drawPath( path = path, color = Device.BATTERY.color, - style = Stroke( - width = GraphUtil.RADIUS, - cap = StrokeCap.Round - ) + style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } } } - YAxisLabels( - modifier = modifier.weight(weight = Y_AXIS_WEIGHT), - graphColor, - minValue = 0f, - maxValue = 100f - ) + YAxisLabels(modifier = modifier.weight(weight = Y_AXIS_WEIGHT), graphColor, minValue = 0f, maxValue = 100f) } Spacer(modifier = Modifier.height(16.dp)) @@ -291,25 +262,26 @@ private fun DeviceMetricsChart( @Composable private fun DeviceMetricsChartPreview() { val now = (System.currentTimeMillis() / 1000).toInt() - val telemetries = List(20) { i -> - Telemetry.newBuilder() - .setTime(now - (19 - i) * 60 * 60) // 1-hour intervals, oldest first - .setDeviceMetrics( - TelemetryProtos.DeviceMetrics.newBuilder() - .setBatteryLevel(80 - i) - .setVoltage(3.7f - i * 0.02f) - .setChannelUtilization(10f + i * 2) - .setAirUtilTx(5f + i) - .setUptimeSeconds(3600 + i * 300) - ) - .build() - } + val telemetries = + List(20) { i -> + Telemetry.newBuilder() + .setTime(now - (19 - i) * 60 * 60) // 1-hour intervals, oldest first + .setDeviceMetrics( + TelemetryProtos.DeviceMetrics.newBuilder() + .setBatteryLevel(80 - i) + .setVoltage(3.7f - i * 0.02f) + .setChannelUtilization(10f + i * 2) + .setAirUtilTx(5f + i) + .setUptimeSeconds(3600 + i * 300), + ) + .build() + } AppTheme { DeviceMetricsChart( modifier = Modifier.height(400.dp), telemetries = telemetries, selectedTime = TimeFrame.TWENTY_FOUR_HOURS, - promptInfoDialog = {} + promptInfoDialog = {}, ) } } @@ -318,51 +290,32 @@ private fun DeviceMetricsChartPreview() { private fun DeviceMetricsCard(telemetry: Telemetry) { val deviceMetrics = telemetry.deviceMetrics val time = telemetry.time * MS_PER_SEC - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { + Card(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp)) { Surface { SelectionContainer { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) { + Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { /* Time, Battery, and Voltage */ - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( text = DATE_TIME_FORMAT.format(time), style = TextStyle(fontWeight = FontWeight.Bold), - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) - BatteryInfo( - batteryLevel = deviceMetrics.batteryLevel, - voltage = deviceMetrics.voltage - ) + BatteryInfo(batteryLevel = deviceMetrics.batteryLevel, voltage = deviceMetrics.voltage) } Spacer(modifier = Modifier.height(4.dp)) /* Channel Utilization and Air Utilization Tx */ - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - val text = stringResource(R.string.channel_air_util).format( - deviceMetrics.channelUtilization, - deviceMetrics.airUtilTx - ) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + val text = + stringResource(R.string.channel_air_util) + .format(deviceMetrics.channelUtilization, deviceMetrics.airUtilTx) Text( text = text, color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } } @@ -376,20 +329,19 @@ private fun DeviceMetricsCard(telemetry: Telemetry) { @Composable private fun DeviceMetricsCardPreview() { val now = (System.currentTimeMillis() / 1000).toInt() - val telemetry = Telemetry.newBuilder() - .setTime(now) - .setDeviceMetrics( - TelemetryProtos.DeviceMetrics.newBuilder() - .setBatteryLevel(75) - .setVoltage(3.65f) - .setChannelUtilization(22.5f) - .setAirUtilTx(12.0f) - .setUptimeSeconds(7200) - ) - .build() - AppTheme { - DeviceMetricsCard(telemetry = telemetry) - } + val telemetry = + Telemetry.newBuilder() + .setTime(now) + .setDeviceMetrics( + TelemetryProtos.DeviceMetrics.newBuilder() + .setBatteryLevel(75) + .setVoltage(3.65f) + .setChannelUtilization(22.5f) + .setAirUtilTx(12.0f) + .setUptimeSeconds(7200), + ) + .build() + AppTheme { DeviceMetricsCard(telemetry = telemetry) } } @Suppress("detekt:MagicNumber") // fake data @@ -397,19 +349,20 @@ private fun DeviceMetricsCardPreview() { @Composable private fun DeviceMetricsScreenPreview() { val now = (System.currentTimeMillis() / 1000).toInt() - val telemetries = List(24) { i -> - Telemetry.newBuilder() - .setTime(now - (23 - i) * 60 * 60) // 1-hour intervals, oldest first - .setDeviceMetrics( - TelemetryProtos.DeviceMetrics.newBuilder() - .setBatteryLevel(85 - i * 2) // Battery decreases over time - .setVoltage(3.8f - i * 0.01f) // Voltage decreases slightly - .setChannelUtilization(15f + i * 1.5f) // Channel utilization increases - .setAirUtilTx(8f + i * 0.8f) // Air utilization increases - .setUptimeSeconds(3600 + i * 3600) // Uptime increases by 1 hour each - ) - .build() - } + val telemetries = + List(24) { i -> + Telemetry.newBuilder() + .setTime(now - (23 - i) * 60 * 60) // 1-hour intervals, oldest first + .setDeviceMetrics( + TelemetryProtos.DeviceMetrics.newBuilder() + .setBatteryLevel(85 - i * 2) // Battery decreases over time + .setVoltage(3.8f - i * 0.01f) // Voltage decreases slightly + .setChannelUtilization(15f + i * 1.5f) // Channel utilization increases + .setAirUtilTx(8f + i * 0.8f) // Air utilization increases + .setUptimeSeconds(3600 + i * 3600), // Uptime increases by 1 hour each + ) + .build() + } AppTheme { Surface { @@ -418,35 +371,32 @@ private fun DeviceMetricsScreenPreview() { if (displayInfoDialog) { LegendInfoDialog( - pairedRes = listOf( + pairedRes = + listOf( Pair(R.string.channel_utilization, R.string.ch_util_definition), - Pair(R.string.air_utilization, R.string.air_util_definition) + Pair(R.string.air_utilization, R.string.air_util_definition), ), - onDismiss = { displayInfoDialog = false } + onDismiss = { displayInfoDialog = false }, ) } DeviceMetricsChart( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(fraction = 0.33f), + modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f), telemetries.reversed(), TimeFrame.TWENTY_FOUR_HOURS, - promptInfoDialog = { displayInfoDialog = true } + promptInfoDialog = { displayInfoDialog = true }, ) SlidingSelector( TimeFrame.entries.toList(), TimeFrame.TWENTY_FOUR_HOURS, - onOptionSelected = { /* Preview only */ } + onOptionSelected = { /* Preview only */ }, ) { OptionLabel(stringResource(it.strRes)) } /* Device Metric Cards */ - LazyColumn( - modifier = Modifier.fillMaxSize() - ) { + LazyColumn(modifier = Modifier.fillMaxSize()) { items(telemetries) { telemetry -> DeviceMetricsCard(telemetry) } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt index c7c2abbcf..9cc0f988c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt @@ -68,8 +68,13 @@ import com.geeksville.mesh.ui.common.components.IaqDisplayMode import com.geeksville.mesh.ui.common.components.IndoorAirQuality import com.geeksville.mesh.ui.common.components.OptionLabel import com.geeksville.mesh.ui.common.components.SlidingSelector -import com.geeksville.mesh.ui.common.theme.Pink -import com.geeksville.mesh.ui.common.theme.Purple +import com.geeksville.mesh.ui.common.theme.GraphColors.Blue +import com.geeksville.mesh.ui.common.theme.GraphColors.Green +import com.geeksville.mesh.ui.common.theme.GraphColors.Magenta +import com.geeksville.mesh.ui.common.theme.GraphColors.Pink +import com.geeksville.mesh.ui.common.theme.GraphColors.Purple +import com.geeksville.mesh.ui.common.theme.GraphColors.Red +import com.geeksville.mesh.ui.common.theme.GraphColors.Yellow import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC import com.geeksville.mesh.util.GraphUtil.createPath @@ -78,13 +83,13 @@ import com.geeksville.mesh.util.UnitConversions.celsiusToFahrenheit @Suppress("MagicNumber") private enum class Environment(val color: Color) { - TEMPERATURE(Color.Red), - RELATIVE_HUMIDITY(Color.Blue), + TEMPERATURE(Red), + RELATIVE_HUMIDITY(Blue), SOIL_TEMPERATURE(Pink), SOIL_MOISTURE(Purple), - BAROMETRIC_PRESSURE(Color.Green), - GAS_RESISTANCE(Color.Yellow), - IAQ(Color.Magenta) + BAROMETRIC_PRESSURE(Green), + GAS_RESISTANCE(Yellow), + IAQ(Magenta), } private const val CHART_WEIGHT = 1f @@ -92,105 +97,72 @@ private const val Y_AXIS_WEIGHT = 0.1f // EnvironmentMetrics can have 1 or 2 Y-axis labels depending on whether barometric pressure is plotted // We'll calculate this dynamically in the chart function -private val LEGEND_DATA_1 = listOf( - LegendData( - nameRes = R.string.temperature, - color = Environment.TEMPERATURE.color, - isLine = true - ), - LegendData( - nameRes = R.string.humidity, - color = Environment.HUMIDITY.color, - isLine = true - ), -) -private val LEGEND_DATA_2 = listOf( - LegendData( - nameRes = R.string.iaq, - color = Environment.IAQ.color, - isLine = true - ), - LegendData( - nameRes = R.string.baro_pressure, - color = Environment.BAROMETRIC_PRESSURE.color, - isLine = true +private val LEGEND_DATA_1 = + listOf( + LegendData(nameRes = R.string.temperature, color = Environment.TEMPERATURE.color, isLine = true), + LegendData(nameRes = R.string.humidity, color = Environment.HUMIDITY.color, isLine = true), + ) +private val LEGEND_DATA_2 = + listOf( + LegendData(nameRes = R.string.iaq, color = Environment.IAQ.color, isLine = true), + LegendData(nameRes = R.string.baro_pressure, color = Environment.BAROMETRIC_PRESSURE.color, isLine = true), + ) +private val LEGEND_DATA_3 = + listOf( + LegendData(nameRes = R.string.soil_temperature, color = Environment.SOIL_TEMPERATURE.color, isLine = true), + LegendData(nameRes = R.string.soil_moisture, color = Environment.SOIL_MOISTURE.color, isLine = true), ) -) -private val LEGEND_DATA_3 = listOf( - LegendData( - nameRes = R.string.soil_temperature, - color = Environment.SOIL_TEMPERATURE.color, - isLine = true - ), - LegendData( - nameRes = R.string.soil_moisture, - color = Environment.SOIL_MOISTURE.color, - isLine = true - ), -) @Composable -fun EnvironmentMetricsScreen( - viewModel: MetricsViewModel = hiltViewModel(), -) { +fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) { val state by viewModel.state.collectAsStateWithLifecycle() val environmentState by viewModel.environmentState.collectAsStateWithLifecycle() val selectedTimeFrame by viewModel.timeFrame.collectAsState() val graphData = environmentState.environmentMetricsFiltered(selectedTimeFrame, state.isFahrenheit) val data = graphData.metrics - val processedTelemetries: List = if (state.isFahrenheit) { - data.map { telemetry -> - val temperatureFahrenheit = - celsiusToFahrenheit(telemetry.environmentMetrics.temperature) - val soilTemperatureFahrenheit = - celsiusToFahrenheit(telemetry.environmentMetrics.soilTemperature) - telemetry.copy { - environmentMetrics = telemetry.environmentMetrics.copy { - temperature = temperatureFahrenheit } - environmentMetrics = telemetry.environmentMetrics.copy { - soilTemperature = soilTemperatureFahrenheit } + val processedTelemetries: List = + if (state.isFahrenheit) { + data.map { telemetry -> + val temperatureFahrenheit = celsiusToFahrenheit(telemetry.environmentMetrics.temperature) + val soilTemperatureFahrenheit = celsiusToFahrenheit(telemetry.environmentMetrics.soilTemperature) + telemetry.copy { + environmentMetrics = telemetry.environmentMetrics.copy { temperature = temperatureFahrenheit } + environmentMetrics = + telemetry.environmentMetrics.copy { soilTemperature = soilTemperatureFahrenheit } + } } + } else { + data } - } else { - data - } var displayInfoDialog by remember { mutableStateOf(false) } Column { if (displayInfoDialog) { LegendInfoDialog( - pairedRes = listOf( - Pair(R.string.iaq, R.string.iaq_definition) - ), - onDismiss = { displayInfoDialog = false } + pairedRes = listOf(Pair(R.string.iaq, R.string.iaq_definition)), + onDismiss = { displayInfoDialog = false }, ) } EnvironmentMetricsChart( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(fraction = 0.33f), + modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f), telemetries = processedTelemetries.reversed(), graphData = graphData, selectedTimeFrame, - promptInfoDialog = { displayInfoDialog = true } + promptInfoDialog = { displayInfoDialog = true }, ) SlidingSelector( TimeFrame.entries.toList(), selectedTimeFrame, - onOptionSelected = { viewModel.setTimeFrame(it) } + onOptionSelected = { viewModel.setTimeFrame(it) }, ) { OptionLabel(stringResource(it.strRes)) } - LazyColumn( - modifier = Modifier.fillMaxSize() - ) { - items(processedTelemetries) { telemetry -> - EnvironmentMetricsCard(telemetry, state.isFahrenheit) - } + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(processedTelemetries) { telemetry -> EnvironmentMetricsCard(telemetry, state.isFahrenheit) } } } } @@ -203,7 +175,7 @@ private fun EnvironmentMetricsChart( telemetries: List, graphData: EnvironmentGraphingData, selectedTime: TimeFrame, - promptInfoDialog: () -> Unit + promptInfoDialog: () -> Unit, ) { ChartHeader(amount = telemetries.size) if (telemetries.isEmpty()) { @@ -215,9 +187,7 @@ private fun EnvironmentMetricsChart( val scrollState = rememberScrollState() val screenWidth = LocalWindowInfo.current.containerSize.width - val dp by remember(key1 = selectedTime) { - mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong())) - } + val dp by remember(key1 = selectedTime) { mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong())) } val shouldPlot = graphData.shouldPlot @@ -237,10 +207,7 @@ private fun EnvironmentMetricsChart( visibleOldest to visibleNewest } - TimeLabels( - oldest = visibleTimeRange.first, - newest = visibleTimeRange.second - ) + TimeLabels(oldest = visibleTimeRange.first, newest = visibleTimeRange.second) Spacer(modifier = Modifier.height(16.dp)) @@ -257,26 +224,20 @@ private fun EnvironmentMetricsChart( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), Environment.BAROMETRIC_PRESSURE.color, minValue = pressureMin, - maxValue = pressureMax + maxValue = pressureMax, ) } Box( contentAlignment = Alignment.TopStart, - modifier = Modifier - .horizontalScroll(state = scrollState, reverseScrolling = true) - .weight(weight = 1f) + modifier = Modifier.horizontalScroll(state = scrollState, reverseScrolling = true).weight(weight = 1f), ) { - - HorizontalLinesOverlay( - modifier.width(dp), - lineColors = List(size = 5) { graphColor } - ) + HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor }) TimeAxisOverlay( modifier = modifier.width(dp), oldest = oldest, newest = newest, - selectedTime.lineInterval() + selectedTime.lineInterval(), ) Canvas(modifier = modifier.width(dp)) { @@ -286,7 +247,6 @@ private fun EnvironmentMetricsChart( var index: Int var first: Int for (metric in Environment.entries) { - if (!shouldPlot[metric.ordinal]) { continue } @@ -298,26 +258,27 @@ private fun EnvironmentMetricsChart( while (index < telemetries.size) { first = index val path = Path() - index = createPath( - telemetries = telemetries, - index = index, - path = path, - oldestTime = oldest, - timeRange = timeDiff, - width = width, - timeThreshold = selectedTime.timeThreshold() - ) { i -> - val telemetry = telemetries.getOrNull(i) ?: telemetries.last() - val ratio = (metric.getValue(telemetry) - min) / diff - val y = height - (ratio * height) - return@createPath y - } + index = + createPath( + telemetries = telemetries, + index = index, + path = path, + oldestTime = oldest, + timeRange = timeDiff, + width = width, + timeThreshold = selectedTime.timeThreshold(), + ) { i -> + val telemetry = telemetries.getOrNull(i) ?: telemetries.last() + val ratio = (metric.getValue(telemetry) - min) / diff + val y = height - (ratio * height) + return@createPath y + } drawPathWithGradient( path = path, color = metric.color, height = height, x1 = ((telemetries[index - 1].time - oldest).toFloat() / timeDiff) * width, - x2 = ((telemetries[first].time - oldest).toFloat() / timeDiff) * width + x2 = ((telemetries[first].time - oldest).toFloat() / timeDiff) * width, ) } } @@ -327,7 +288,7 @@ private fun EnvironmentMetricsChart( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), graphColor, minValue = rightMin, - maxValue = rightMax + maxValue = rightMax, ) } @@ -345,91 +306,72 @@ private fun EnvironmentMetricsChart( private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahrenheit: Boolean) { val envMetrics = telemetry.environmentMetrics val time = telemetry.time * MS_PER_SEC - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { + Card(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp)) { Surface { SelectionContainer { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) { + Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { /* Time and Temperature */ - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( text = DATE_TIME_FORMAT.format(time), style = TextStyle(fontWeight = FontWeight.Bold), - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) val textFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" Text( - text = textFormat.format( - stringResource(id = R.string.temperature), - envMetrics.temperature - ), + text = textFormat.format(stringResource(id = R.string.temperature), envMetrics.temperature), color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } Spacer(modifier = Modifier.height(4.dp)) /* Humidity and Barometric Pressure */ - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( - text = "%s %.2f%%".format( - stringResource(id = R.string.humidity), - envMetrics.relativeHumidity, - ), + text = + "%s %.2f%%".format(stringResource(id = R.string.humidity), envMetrics.relativeHumidity), color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) if (envMetrics.barometricPressure > 0) { Text( text = "%.2f hPa".format(envMetrics.barometricPressure), color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } } /* Soil Moisture and Soil Temperature */ val soilMoistureRange = 0..100 - if (telemetry.environmentMetrics.hasSoilTemperature() || - telemetry.environmentMetrics.soilMoisture in soilMoistureRange) { + if ( + telemetry.environmentMetrics.hasSoilTemperature() || + telemetry.environmentMetrics.soilMoisture in soilMoistureRange + ) { Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { val soilTemperatureTextFormat = if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" val soilMoistureTextFormat = "%s %d%%" Text( - text = soilMoistureTextFormat.format( + text = + soilMoistureTextFormat.format( stringResource(R.string.soil_moisture), - envMetrics.soilMoisture + envMetrics.soilMoisture, ), color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) Text( - text = soilTemperatureTextFormat.format( + text = + soilTemperatureTextFormat.format( stringResource(R.string.soil_temperature), - envMetrics.soilTemperature + envMetrics.soilTemperature, ), color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } } @@ -437,21 +379,14 @@ private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahre if (telemetry.environmentMetrics.hasIaq()) { Spacer(modifier = Modifier.height(4.dp)) /* Air Quality */ - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - - ) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Text( text = stringResource(R.string.iaq), color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) Spacer(modifier = Modifier.width(4.dp)) - IndoorAirQuality( - iaq = telemetry.environmentMetrics.iaq, - displayMode = IaqDisplayMode.Dot - ) + IndoorAirQuality(iaq = telemetry.environmentMetrics.iaq, displayMode = IaqDisplayMode.Dot) } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt index e67310879..09b25ee4a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt @@ -65,7 +65,8 @@ import com.geeksville.mesh.model.MetricsViewModel import com.geeksville.mesh.model.TimeFrame import com.geeksville.mesh.ui.common.components.OptionLabel import com.geeksville.mesh.ui.common.components.SlidingSelector -import com.geeksville.mesh.ui.common.theme.InfantryBlue +import com.geeksville.mesh.ui.common.theme.GraphColors.InfantryBlue +import com.geeksville.mesh.ui.common.theme.GraphColors.Red import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC import com.geeksville.mesh.util.GraphUtil @@ -74,70 +75,57 @@ import com.geeksville.mesh.util.GraphUtil.createPath @Suppress("MagicNumber") private enum class Power(val color: Color, val min: Float, val max: Float) { CURRENT(InfantryBlue, -500f, 500f), - VOLTAGE(Color.Red, 0f, 20f); + VOLTAGE(Red, 0f, 20f), + ; - /** - * Difference between the metrics `max` and `min` values. - */ + /** Difference between the metrics `max` and `min` values. */ fun difference() = max - min } private enum class PowerChannel(@StringRes val strRes: Int) { ONE(R.string.channel_1), TWO(R.string.channel_2), - THREE(R.string.channel_3) + THREE(R.string.channel_3), } private const val CHART_WEIGHT = 1f private const val Y_AXIS_WEIGHT = 0.1f private const val CHART_WIDTH_RATIO = CHART_WEIGHT / (CHART_WEIGHT + Y_AXIS_WEIGHT + Y_AXIS_WEIGHT) -private val LEGEND_DATA = listOf( - LegendData(nameRes = R.string.current, color = Power.CURRENT.color, isLine = true), - LegendData(nameRes = R.string.voltage, color = Power.VOLTAGE.color, isLine = true), -) +private val LEGEND_DATA = + listOf( + LegendData(nameRes = R.string.current, color = Power.CURRENT.color, isLine = true), + LegendData(nameRes = R.string.voltage, color = Power.VOLTAGE.color, isLine = true), + ) @Composable -fun PowerMetricsScreen( - viewModel: MetricsViewModel = hiltViewModel(), -) { +fun PowerMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) { val state by viewModel.state.collectAsStateWithLifecycle() val selectedTimeFrame by viewModel.timeFrame.collectAsState() var selectedChannel by remember { mutableStateOf(PowerChannel.ONE) } val data = state.powerMetricsFiltered(selectedTimeFrame) Column { - PowerMetricsChart( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(fraction = 0.33f), + modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f), telemetries = data.reversed(), selectedTimeFrame, selectedChannel, ) - SlidingSelector( - PowerChannel.entries.toList(), - selectedChannel, - onOptionSelected = { selectedChannel = it } - ) { + SlidingSelector(PowerChannel.entries.toList(), selectedChannel, onOptionSelected = { selectedChannel = it }) { OptionLabel(stringResource(it.strRes)) } Spacer(modifier = Modifier.height(2.dp)) SlidingSelector( TimeFrame.entries.toList(), selectedTimeFrame, - onOptionSelected = { viewModel.setTimeFrame(it) } + onOptionSelected = { viewModel.setTimeFrame(it) }, ) { OptionLabel(stringResource(it.strRes)) } - LazyColumn( - modifier = Modifier.fillMaxSize() - ) { - items(data) { telemetry -> PowerMetricsCard(telemetry) } - } + LazyColumn(modifier = Modifier.fillMaxSize()) { items(data) { telemetry -> PowerMetricsCard(telemetry) } } } } @@ -154,19 +142,16 @@ private fun PowerMetricsChart( return } - val (oldest, newest) = remember(key1 = telemetries) { - Pair( - telemetries.minBy { it.time }, - telemetries.maxBy { it.time } - ) - } + val (oldest, newest) = + remember(key1 = telemetries) { Pair(telemetries.minBy { it.time }, telemetries.maxBy { it.time }) } val timeDiff = newest.time - oldest.time val scrollState = rememberScrollState() val screenWidth = LocalWindowInfo.current.containerSize.width - val dp by remember(key1 = selectedTime) { - mutableStateOf(selectedTime.dp(screenWidth, time = (newest.time - oldest.time).toLong())) - } + val dp by + remember(key1 = selectedTime) { + mutableStateOf(selectedTime.dp(screenWidth, time = (newest.time - oldest.time).toLong())) + } // Calculate visible time range based on scroll position and chart width val visibleTimeRange = run { @@ -182,10 +167,7 @@ private fun PowerMetricsChart( visibleOldest to visibleNewest } - TimeLabels( - oldest = visibleTimeRange.first, - newest = visibleTimeRange.second - ) + TimeLabels(oldest = visibleTimeRange.first, newest = visibleTimeRange.second) Spacer(modifier = Modifier.height(16.dp)) @@ -202,21 +184,11 @@ private fun PowerMetricsChart( ) Box( contentAlignment = Alignment.TopStart, - modifier = Modifier - .horizontalScroll(state = scrollState, reverseScrolling = true) - .weight(1f) + modifier = Modifier.horizontalScroll(state = scrollState, reverseScrolling = true).weight(1f), ) { - HorizontalLinesOverlay( - modifier.width(dp), - lineColors = List(size = 5) { graphColor }, - ) + HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor }) - TimeAxisOverlay( - modifier.width(dp), - oldest = oldest.time, - newest = newest.time, - selectedTime.lineInterval() - ) + TimeAxisOverlay(modifier.width(dp), oldest = oldest.time, newest = newest.time, selectedTime.lineInterval()) /* Plot */ Canvas(modifier = modifier.width(dp)) { @@ -226,57 +198,50 @@ private fun PowerMetricsChart( var index = 0 while (index < telemetries.size) { val path = Path() - index = createPath( - telemetries = telemetries, - index = index, - path = path, - oldestTime = oldest.time, - timeRange = timeDiff, - width = width, - timeThreshold = selectedTime.timeThreshold() - ) { i -> - val telemetry = telemetries.getOrNull(i) ?: telemetries.last() - val ratio = retrieveVoltage(selectedChannel, telemetry) / voltageDiff - val y = height - (ratio * height) - return@createPath y - } + index = + createPath( + telemetries = telemetries, + index = index, + path = path, + oldestTime = oldest.time, + timeRange = timeDiff, + width = width, + timeThreshold = selectedTime.timeThreshold(), + ) { i -> + val telemetry = telemetries.getOrNull(i) ?: telemetries.last() + val ratio = retrieveVoltage(selectedChannel, telemetry) / voltageDiff + val y = height - (ratio * height) + return@createPath y + } drawPath( path = path, color = Power.VOLTAGE.color, - style = Stroke( - width = GraphUtil.RADIUS, - cap = StrokeCap.Round - ) + style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } /* Current */ index = 0 while (index < telemetries.size) { val path = Path() - index = createPath( - telemetries = telemetries, - index = index, - path = path, - oldestTime = oldest.time, - timeRange = timeDiff, - width = width, - timeThreshold = selectedTime.timeThreshold() - ) { i -> - val telemetry = telemetries.getOrNull(i) ?: telemetries.last() - val ratio = (retrieveCurrent( - selectedChannel, - telemetry - ) - Power.CURRENT.min) / currentDiff - val y = height - (ratio * height) - return@createPath y - } + index = + createPath( + telemetries = telemetries, + index = index, + path = path, + oldestTime = oldest.time, + timeRange = timeDiff, + width = width, + timeThreshold = selectedTime.timeThreshold(), + ) { i -> + val telemetry = telemetries.getOrNull(i) ?: telemetries.last() + val ratio = (retrieveCurrent(selectedChannel, telemetry) - Power.CURRENT.min) / currentDiff + val y = height - (ratio * height) + return@createPath y + } drawPath( path = path, color = Power.CURRENT.color, - style = Stroke( - width = GraphUtil.RADIUS, - cap = StrokeCap.Round, - ) + style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } } @@ -299,51 +264,39 @@ private fun PowerMetricsChart( @Composable private fun PowerMetricsCard(telemetry: Telemetry) { val time = telemetry.time * MS_PER_SEC - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { + Card(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp)) { Surface { SelectionContainer { - Row( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier - .padding(8.dp) - ) { + Row(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(8.dp)) { /* Time */ Row { Text( text = DATE_TIME_FORMAT.format(time), style = TextStyle(fontWeight = FontWeight.Bold), - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { if (telemetry.powerMetrics.hasCh1Current() || telemetry.powerMetrics.hasCh1Voltage()) { PowerChannelColumn( R.string.channel_1, telemetry.powerMetrics.ch1Voltage, - telemetry.powerMetrics.ch1Current + telemetry.powerMetrics.ch1Current, ) } if (telemetry.powerMetrics.hasCh2Current() || telemetry.powerMetrics.hasCh2Voltage()) { PowerChannelColumn( R.string.channel_2, telemetry.powerMetrics.ch2Voltage, - telemetry.powerMetrics.ch2Current + telemetry.powerMetrics.ch2Current, ) } if (telemetry.powerMetrics.hasCh3Current() || telemetry.powerMetrics.hasCh3Voltage()) { PowerChannelColumn( R.string.channel_3, telemetry.powerMetrics.ch3Voltage, - telemetry.powerMetrics.ch3Current + telemetry.powerMetrics.ch3Current, ) } } @@ -360,39 +313,31 @@ private fun PowerChannelColumn(@StringRes titleRes: Int, voltage: Float, current Text( text = stringResource(titleRes), style = TextStyle(fontWeight = FontWeight.Bold), - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) Text( text = "%.2fV".format(voltage), color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) Text( text = "%.1fmA".format(current), color = MaterialTheme.colorScheme.onSurface, - fontSize = MaterialTheme.typography.labelLarge.fontSize + fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } } -/** - * Retrieves the appropriate voltage depending on `channelSelected`. - */ -private fun retrieveVoltage(channelSelected: PowerChannel, telemetry: Telemetry): Float { - return when (channelSelected) { - PowerChannel.ONE -> telemetry.powerMetrics.ch1Voltage - PowerChannel.TWO -> telemetry.powerMetrics.ch2Voltage - PowerChannel.THREE -> telemetry.powerMetrics.ch3Voltage - } +/** Retrieves the appropriate voltage depending on `channelSelected`. */ +private fun retrieveVoltage(channelSelected: PowerChannel, telemetry: Telemetry): Float = when (channelSelected) { + PowerChannel.ONE -> telemetry.powerMetrics.ch1Voltage + PowerChannel.TWO -> telemetry.powerMetrics.ch2Voltage + PowerChannel.THREE -> telemetry.powerMetrics.ch3Voltage } -/** - * Retrieves the appropriate current depending on `channelSelected`. - */ -private fun retrieveCurrent(channelSelected: PowerChannel, telemetry: Telemetry): Float { - return when (channelSelected) { - PowerChannel.ONE -> telemetry.powerMetrics.ch1Current - PowerChannel.TWO -> telemetry.powerMetrics.ch2Current - PowerChannel.THREE -> telemetry.powerMetrics.ch3Current - } +/** Retrieves the appropriate current depending on `channelSelected`. */ +private fun retrieveCurrent(channelSelected: PowerChannel, telemetry: Telemetry): Float = when (channelSelected) { + PowerChannel.ONE -> telemetry.powerMetrics.ch1Current + PowerChannel.TWO -> telemetry.powerMetrics.ch2Current + PowerChannel.THREE -> telemetry.powerMetrics.ch3Current } diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt index 9c3f32008..e18a98d03 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt @@ -89,6 +89,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Switch @@ -142,9 +143,10 @@ import com.geeksville.mesh.service.ServiceAction import com.geeksville.mesh.ui.common.components.PreferenceCategory import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider import com.geeksville.mesh.ui.common.theme.AppTheme -import com.geeksville.mesh.ui.common.theme.Green -import com.geeksville.mesh.ui.common.theme.Orange -import com.geeksville.mesh.ui.common.theme.Yellow +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusOrange +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow import com.geeksville.mesh.ui.node.components.NodeActionDialogs import com.geeksville.mesh.ui.node.components.NodeMenuAction import com.geeksville.mesh.ui.radioconfig.NavCard @@ -188,23 +190,24 @@ fun NodeDetailScreen( val lastTracerouteTime by uiViewModel.lastTraceRouteTime.collectAsStateWithLifecycle() val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle() - val availableLogs by remember(state, environmentState) { - derivedStateOf { - buildSet { - if (state.hasDeviceMetrics()) add(LogsType.DEVICE) - if (state.hasPositionLogs()) { - add(LogsType.NODE_MAP) - add(LogsType.POSITIONS) + val availableLogs by + remember(state, environmentState) { + derivedStateOf { + buildSet { + if (state.hasDeviceMetrics()) add(LogsType.DEVICE) + if (state.hasPositionLogs()) { + add(LogsType.NODE_MAP) + add(LogsType.POSITIONS) + } + if (environmentState.hasEnvironmentMetrics()) add(LogsType.ENVIRONMENT) + if (state.hasSignalMetrics()) add(LogsType.SIGNAL) + if (state.hasPowerMetrics()) add(LogsType.POWER) + if (state.hasTracerouteLogs()) add(LogsType.TRACEROUTE) + if (state.hasHostMetrics()) add(LogsType.HOST) + if (state.hasPaxMetrics()) add(LogsType.PAX) } - if (environmentState.hasEnvironmentMetrics()) add(LogsType.ENVIRONMENT) - if (state.hasSignalMetrics()) add(LogsType.SIGNAL) - if (state.hasPowerMetrics()) add(LogsType.POWER) - if (state.hasTracerouteLogs()) add(LogsType.TRACEROUTE) - if (state.hasHostMetrics()) add(LogsType.HOST) - if (state.hasPaxMetrics()) add(LogsType.PAX) } } - } val node = state.node if (node != null) { @@ -229,9 +232,7 @@ fun NodeDetailScreen( modifier = modifier, ) } else { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } } @@ -269,17 +270,21 @@ private fun handleNodeAction( sealed interface NodeDetailAction { data class Navigate(val route: Route) : NodeDetailAction + data class TriggerServiceAction(val action: ServiceAction) : NodeDetailAction + data class HandleNodeMenuAction(val action: NodeMenuAction) : NodeDetailAction + data object ShareContact : NodeDetailAction } val Node.isEffectivelyUnmessageable: Boolean - get() = if (user.hasIsUnmessagable()) { - user.isUnmessagable - } else { - user.role?.isUnmessageableRole() == true - } + get() = + if (user.hasIsUnmessagable()) { + user.isUnmessagable + } else { + user.role?.isUnmessageableRole() == true + } private enum class LogsType(@StringRes val titleRes: Int, val icon: ImageVector, val route: Route) { DEVICE(R.string.device_metrics_log, Icons.Default.ChargingStation, NodeDetailRoutes.DeviceMetrics), @@ -343,19 +348,14 @@ private fun NodeDetailList( if (showFirmwareSheet) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) - ModalBottomSheet( - onDismissRequest = { showFirmwareSheet = false }, - sheetState = sheetState, - ) { + ModalBottomSheet(onDismissRequest = { showFirmwareSheet = false }, sheetState = sheetState) { selectedFirmware?.let { FirmwareReleaseSheetContent(firmwareRelease = it) } } } Column(modifier = modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState())) { if (metricsState.deviceHardware != null) { - PreferenceCategory(stringResource(R.string.device)) { - DeviceDetailsContent(metricsState) - } + PreferenceCategory(stringResource(R.string.device)) { DeviceDetailsContent(metricsState) } } PreferenceCategory(stringResource(R.string.details)) { NodeDetailsContent(node, ourNode, metricsState.displayUnits) @@ -406,11 +406,7 @@ private fun MetricsSection( PreferenceCategory(stringResource(id = R.string.logs)) { LogsType.entries.forEach { type -> if (availableLogs.contains(type)) { - NavCard( - title = stringResource(type.titleRes), - icon = type.icon, - enabled = true, - ) { + NavCard(title = stringResource(type.titleRes), icon = type.icon, enabled = true) { onAction(NodeDetailAction.Navigate(type.route)) } } @@ -461,14 +457,14 @@ private fun AdministrationSection( label = stringResource(R.string.latest_stable_firmware), icon = Icons.Default.Memory, value = latestStable.id.substringBeforeLast(".").replace("v", ""), - iconTint = Green, + iconTint = colorScheme.StatusGreen, onClick = { onFirmwareSelected(latestStable) }, ) NodeDetailRow( label = stringResource(R.string.latest_alpha_firmware), icon = Icons.Default.Memory, value = latestAlpha.id.substringBeforeLast(".").replace("v", ""), - iconTint = Yellow, + iconTint = colorScheme.StatusYellow, onClick = { onFirmwareSelected(latestAlpha) }, ) } @@ -483,11 +479,11 @@ private fun DeviceVersion.determineFirmwareStatusColor( val stableVersion = latestStable.asDeviceVersion() val alphaVersion = latestAlpha.asDeviceVersion() return when { - this < stableVersion -> MaterialTheme.colorScheme.error - this == stableVersion -> Green - this in stableVersion..alphaVersion -> Yellow - this > alphaVersion -> Orange - else -> MaterialTheme.colorScheme.onSurface + this < stableVersion -> colorScheme.StatusRed + this == stableVersion -> colorScheme.StatusGreen + this in stableVersion..alphaVersion -> colorScheme.StatusYellow + this > alphaVersion -> colorScheme.StatusOrange + else -> colorScheme.onSurface } } @@ -495,19 +491,13 @@ private fun DeviceVersion.determineFirmwareStatusColor( private fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease) { val context = LocalContext.current Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(16.dp) - .fillMaxWidth(), + modifier = Modifier.verticalScroll(rememberScrollState()).padding(16.dp).fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp), ) { Text(text = firmwareRelease.title, style = MaterialTheme.typography.titleLarge) Text(text = "Version: ${firmwareRelease.id}", style = MaterialTheme.typography.bodyMedium) Markdown(modifier = Modifier.padding(8.dp), content = firmwareRelease.releaseNotes) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { Button( onClick = { val intent = Intent(Intent.ACTION_VIEW, firmwareRelease.pageUrl.toUri()) @@ -515,10 +505,7 @@ private fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease) { }, modifier = Modifier.weight(1f), ) { - Icon( - imageVector = Icons.Default.Link, - contentDescription = stringResource(id = R.string.view_release), - ) + Icon(imageVector = Icons.Default.Link, contentDescription = stringResource(id = R.string.view_release)) Spacer(modifier = Modifier.width(8.dp)) Text(text = stringResource(id = R.string.view_release)) } @@ -529,10 +516,7 @@ private fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease) { }, modifier = Modifier.weight(1f), ) { - Icon( - imageVector = Icons.Default.Download, - contentDescription = stringResource(id = R.string.download), - ) + Icon(imageVector = Icons.Default.Download, contentDescription = stringResource(id = R.string.download)) Spacer(modifier = Modifier.width(8.dp)) Text(text = stringResource(id = R.string.download)) } @@ -550,12 +534,8 @@ private fun NodeDetailRow( onClick: (() -> Unit)? = null, ) { Row( - modifier = modifier - .fillMaxWidth() - .thenIf(onClick != null) { - clickable(onClick = onClick!!) - } - .padding(vertical = 8.dp), + modifier = + modifier.fillMaxWidth().thenIf(onClick != null) { clickable(onClick = onClick!!) }.padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -624,11 +604,7 @@ private fun DeviceActions( } @Composable -private fun RemoteDeviceActions( - node: Node, - lastTracerouteTime: Long?, - onAction: (NodeDetailAction) -> Unit, -) { +private fun RemoteDeviceActions(node: Node, lastTracerouteTime: Long?, onAction: (NodeDetailAction) -> Unit) { if (!node.isEffectivelyUnmessageable) { NodeActionButton( title = stringResource(id = R.string.direct_message), @@ -663,8 +639,8 @@ private fun DeviceDetailsContent(state: MetricsState) { val hwModelName = deviceHardware.displayName val isSupported = deviceHardware.activelySupported Box( - modifier = Modifier - .size(100.dp) + modifier = + Modifier.size(100.dp) .padding(4.dp) .clip(CircleShape) .background(color = Color(node.colors.second).copy(alpha = .5f), shape = CircleShape), @@ -674,14 +650,15 @@ private fun DeviceDetailsContent(state: MetricsState) { } NodeDetailRow(label = stringResource(R.string.hardware), icon = Icons.Default.Router, value = hwModelName) NodeDetailRow( - label = if (isSupported) { + label = + if (isSupported) { stringResource(R.string.supported) } else { stringResource(R.string.supported_by_community) }, icon = if (isSupported) Icons.TwoTone.Verified else ImageVector.vectorResource(R.drawable.unverified), value = "", - iconTint = if (isSupported) Color.Green else Color.Red, + iconTint = if (isSupported) colorScheme.StatusGreen else colorScheme.StatusRed, ) } @@ -737,11 +714,7 @@ private fun EncryptionErrorContent() { } @Composable -private fun MainNodeDetails( - node: Node, - ourNode: Node?, - displayUnits: ConfigProtos.Config.DisplayConfig.DisplayUnits, -) { +private fun MainNodeDetails(node: Node, ourNode: Node?, displayUnits: ConfigProtos.Config.DisplayConfig.DisplayUnits) { NodeDetailRow( label = stringResource(R.string.long_name), icon = Icons.TwoTone.Person, @@ -757,16 +730,8 @@ private fun MainNodeDetails( icon = Icons.Default.Numbers, value = node.num.toUInt().toString(), ) - NodeDetailRow( - label = stringResource(R.string.user_id), - icon = Icons.Default.Person, - value = node.user.id, - ) - NodeDetailRow( - label = stringResource(R.string.role), - icon = Icons.Default.Work, - value = node.user.role.name, - ) + NodeDetailRow(label = stringResource(R.string.user_id), icon = Icons.Default.Person, value = node.user.id) + NodeDetailRow(label = stringResource(R.string.role), icon = Icons.Default.Work, value = node.user.role.name) if (node.isEffectivelyUnmessageable) { NodeDetailRow( label = stringResource(R.string.unmonitored_or_infrastructure), @@ -804,25 +769,12 @@ private fun MainNodeDetails( @Composable private fun InfoCard(icon: ImageVector, text: String, value: String, rotateIcon: Float = 0f) { Card(modifier = Modifier.padding(4.dp).width(100.dp).height(100.dp)) { - Box( - modifier = Modifier - .padding(4.dp) - .width(100.dp) - .height(100.dp), - contentAlignment = Alignment.Center, - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { + Box(modifier = Modifier.padding(4.dp).width(100.dp).height(100.dp), contentAlignment = Alignment.Center) { + Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { Icon( imageVector = icon, contentDescription = text, - modifier = Modifier - .size(24.dp) - .thenIf(rotateIcon != 0f) { - rotate(rotateIcon) - }, + modifier = Modifier.size(24.dp).thenIf(rotateIcon != 0f) { rotate(rotateIcon) }, ) Text( textAlign = TextAlign.Center, @@ -843,32 +795,14 @@ private fun InfoCard(icon: ImageVector, text: String, value: String, rotateIcon: } @Composable -private fun DrawableInfoCard( - @DrawableRes iconRes: Int, - text: String, - value: String, - rotateIcon: Float = 0f, -) { +private fun DrawableInfoCard(@DrawableRes iconRes: Int, text: String, value: String, rotateIcon: Float = 0f) { Card(modifier = Modifier.padding(4.dp).width(100.dp).height(100.dp)) { - Box( - modifier = Modifier - .padding(4.dp) - .width(100.dp) - .height(100.dp), - contentAlignment = Alignment.Center, - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { + Box(modifier = Modifier.padding(4.dp).width(100.dp).height(100.dp), contentAlignment = Alignment.Center) { + Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { Icon( painter = painterResource(id = iconRes), contentDescription = text, - modifier = Modifier - .size(24.dp) - .thenIf(rotateIcon != 0f) { - rotate(rotateIcon) - }, + modifier = Modifier.size(24.dp).thenIf(rotateIcon != 0f) { rotate(rotateIcon) }, ) Text( textAlign = TextAlign.Center, @@ -895,118 +829,128 @@ private fun EnvironmentMetrics( isFahrenheit: Boolean = false, displayUnits: ConfigProtos.Config.DisplayConfig.DisplayUnits, ) { - val vectorMetrics = remember(node.environmentMetrics, isFahrenheit, displayUnits) { - buildList { - with(node.environmentMetrics) { - if (hasTemperature()) { - add( - VectorMetricInfo( - R.string.temperature, - temperature.toTempString(isFahrenheit), - Icons.Default.Thermostat, - ), - ) - } - if (hasRelativeHumidity()) { - add( - VectorMetricInfo( - R.string.humidity, - "%.0f%%".format(relativeHumidity), - Icons.Default.WaterDrop, - ), - ) - } - if (hasBarometricPressure()) { - add( - VectorMetricInfo( - R.string.pressure, - "%.0f hPa".format(barometricPressure), - Icons.Default.Speed, - ), - ) - } - if (hasGasResistance()) { - add( - VectorMetricInfo( - R.string.gas_resistance, - "%.0f MΩ".format(gasResistance), - Icons.Default.BlurOn, - ), - ) - } - if (hasVoltage()) add(VectorMetricInfo(R.string.voltage, "%.2fV".format(voltage), Icons.Default.Bolt)) - if (hasCurrent()) add(VectorMetricInfo(R.string.current, "%.1fmA".format(current), Icons.Default.Power)) - if (hasIaq()) add(VectorMetricInfo(R.string.iaq, iaq.toString(), Icons.Default.Air)) - if (hasDistance()) { - add( - VectorMetricInfo( - R.string.distance, - distance.toSmallDistanceString(displayUnits), - Icons.Default.Height, - ), - ) - } - if (hasLux()) add(VectorMetricInfo(R.string.lux, "%.0f lx".format(lux), Icons.Default.LightMode)) - if (hasUvLux()) add(VectorMetricInfo(R.string.uv_lux, "%.0f lx".format(uvLux), Icons.Default.LightMode)) - if (hasWindSpeed()) { - @Suppress("MagicNumber") - val normalizedBearing = (windDirection + 180) % 360 - add( - VectorMetricInfo( - R.string.wind, - windSpeed.toSpeedString(displayUnits), - Icons.Outlined.Navigation, - normalizedBearing.toFloat(), - ), - ) - } - if (hasWeight()) add(VectorMetricInfo(R.string.weight, "%.2f kg".format(weight), Icons.Default.Scale)) - } - } - } - val drawableMetrics = remember(node.environmentMetrics, isFahrenheit) { - buildList { - with(node.environmentMetrics) { - if (hasTemperature() && hasRelativeHumidity()) { - val dewPoint = UnitConversions.calculateDewPoint(temperature, relativeHumidity) - add( - DrawableMetricInfo( - R.string.dew_point, - dewPoint.toTempString(isFahrenheit), - R.drawable.ic_outlined_dew_point_24, - ), - ) - } - if (hasSoilTemperature()) { - add( - DrawableMetricInfo( - R.string.soil_temperature, - soilTemperature.toTempString(isFahrenheit), - R.drawable.soil_temperature, - ), - ) - } - if (hasSoilMoisture()) { - add( - DrawableMetricInfo( - R.string.soil_moisture, - "%d%%".format(soilMoisture), - R.drawable.soil_moisture, - ), - ) - } - if (hasRadiation()) { - add( - DrawableMetricInfo( - R.string.radiation, - "%.1f µR/h".format(radiation), - R.drawable.ic_filled_radioactive_24, - ), - ) + val vectorMetrics = + remember(node.environmentMetrics, isFahrenheit, displayUnits) { + buildList { + with(node.environmentMetrics) { + if (hasTemperature()) { + add( + VectorMetricInfo( + R.string.temperature, + temperature.toTempString(isFahrenheit), + Icons.Default.Thermostat, + ), + ) + } + if (hasRelativeHumidity()) { + add( + VectorMetricInfo( + R.string.humidity, + "%.0f%%".format(relativeHumidity), + Icons.Default.WaterDrop, + ), + ) + } + if (hasBarometricPressure()) { + add( + VectorMetricInfo( + R.string.pressure, + "%.0f hPa".format(barometricPressure), + Icons.Default.Speed, + ), + ) + } + if (hasGasResistance()) { + add( + VectorMetricInfo( + R.string.gas_resistance, + "%.0f MΩ".format(gasResistance), + Icons.Default.BlurOn, + ), + ) + } + if (hasVoltage()) { + add(VectorMetricInfo(R.string.voltage, "%.2fV".format(voltage), Icons.Default.Bolt)) + } + if (hasCurrent()) { + add(VectorMetricInfo(R.string.current, "%.1fmA".format(current), Icons.Default.Power)) + } + if (hasIaq()) add(VectorMetricInfo(R.string.iaq, iaq.toString(), Icons.Default.Air)) + if (hasDistance()) { + add( + VectorMetricInfo( + R.string.distance, + distance.toSmallDistanceString(displayUnits), + Icons.Default.Height, + ), + ) + } + if (hasLux()) add(VectorMetricInfo(R.string.lux, "%.0f lx".format(lux), Icons.Default.LightMode)) + if (hasUvLux()) { + add(VectorMetricInfo(R.string.uv_lux, "%.0f lx".format(uvLux), Icons.Default.LightMode)) + } + if (hasWindSpeed()) { + @Suppress("MagicNumber") + val normalizedBearing = (windDirection + 180) % 360 + add( + VectorMetricInfo( + R.string.wind, + windSpeed.toSpeedString(displayUnits), + Icons.Outlined.Navigation, + normalizedBearing.toFloat(), + ), + ) + } + if (hasWeight()) { + add(VectorMetricInfo(R.string.weight, "%.2f kg".format(weight), Icons.Default.Scale)) + } + } + } + } + val drawableMetrics = + remember(node.environmentMetrics, isFahrenheit) { + buildList { + with(node.environmentMetrics) { + if (hasTemperature() && hasRelativeHumidity()) { + val dewPoint = UnitConversions.calculateDewPoint(temperature, relativeHumidity) + add( + DrawableMetricInfo( + R.string.dew_point, + dewPoint.toTempString(isFahrenheit), + R.drawable.ic_outlined_dew_point_24, + ), + ) + } + if (hasSoilTemperature()) { + add( + DrawableMetricInfo( + R.string.soil_temperature, + soilTemperature.toTempString(isFahrenheit), + R.drawable.soil_temperature, + ), + ) + } + if (hasSoilMoisture()) { + add( + DrawableMetricInfo( + R.string.soil_moisture, + "%d%%".format(soilMoisture), + R.drawable.soil_moisture, + ), + ) + } + if (hasRadiation()) { + add( + DrawableMetricInfo( + R.string.radiation, + "%.1f µR/h".format(radiation), + R.drawable.ic_filled_radioactive_24, + ), + ) + } } } } - } FlowRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly, @@ -1033,24 +977,25 @@ private fun EnvironmentMetrics( @Composable private fun PowerMetrics(node: Node) { - val metrics = remember(node.powerMetrics) { - buildList { - with(node.powerMetrics) { - if (ch1Voltage != 0f) { - add(VectorMetricInfo(R.string.channel_1, "%.2fV".format(ch1Voltage), Icons.Default.Bolt)) - add(VectorMetricInfo(R.string.channel_1, "%.1fmA".format(ch1Current), Icons.Default.Power)) - } - if (ch2Voltage != 0f) { - add(VectorMetricInfo(R.string.channel_2, "%.2fV".format(ch2Voltage), Icons.Default.Bolt)) - add(VectorMetricInfo(R.string.channel_2, "%.1fmA".format(ch2Current), Icons.Default.Power)) - } - if (ch3Voltage != 0f) { - add(VectorMetricInfo(R.string.channel_3, "%.2fV".format(ch3Voltage), Icons.Default.Bolt)) - add(VectorMetricInfo(R.string.channel_3, "%.1fmA".format(ch3Current), Icons.Default.Power)) + val metrics = + remember(node.powerMetrics) { + buildList { + with(node.powerMetrics) { + if (ch1Voltage != 0f) { + add(VectorMetricInfo(R.string.channel_1, "%.2fV".format(ch1Voltage), Icons.Default.Bolt)) + add(VectorMetricInfo(R.string.channel_1, "%.1fmA".format(ch1Current), Icons.Default.Power)) + } + if (ch2Voltage != 0f) { + add(VectorMetricInfo(R.string.channel_2, "%.2fV".format(ch2Voltage), Icons.Default.Bolt)) + add(VectorMetricInfo(R.string.channel_2, "%.1fmA".format(ch2Current), Icons.Default.Power)) + } + if (ch3Voltage != 0f) { + add(VectorMetricInfo(R.string.channel_3, "%.2fV".format(ch3Voltage), Icons.Default.Bolt)) + add(VectorMetricInfo(R.string.channel_3, "%.1fmA".format(ch3Current), Icons.Default.Power)) + } } } } - } FlowRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -1066,10 +1011,11 @@ private const val COOL_DOWN_TIME_MS = 30000L @Composable fun TracerouteActionButton(title: String, lastTracerouteTime: Long?, onClick: () -> Unit) { - var isCoolingDown by remember(lastTracerouteTime) { - val timeSinceLast = System.currentTimeMillis() - (lastTracerouteTime ?: 0) - mutableStateOf(timeSinceLast < COOL_DOWN_TIME_MS) - } + var isCoolingDown by + remember(lastTracerouteTime) { + val timeSinceLast = System.currentTimeMillis() - (lastTracerouteTime ?: 0) + mutableStateOf(timeSinceLast < COOL_DOWN_TIME_MS) + } LaunchedEffect(lastTracerouteTime) { val timeSinceLast = System.currentTimeMillis() - (lastTracerouteTime ?: 0) @@ -1079,10 +1025,7 @@ fun TracerouteActionButton(title: String, lastTracerouteTime: Long?, onClick: () } } - val progress by animateFloatAsState( - targetValue = if (isCoolingDown) 1f else 0f, - label = "TracerouteCooldown", - ) + val progress by animateFloatAsState(targetValue = if (isCoolingDown) 1f else 0f, label = "TracerouteCooldown") Button( onClick = { @@ -1090,10 +1033,7 @@ fun TracerouteActionButton(title: String, lastTracerouteTime: Long?, onClick: () onClick() }, enabled = !isCoolingDown, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .height(48.dp), + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).height(48.dp), ) { Row(verticalAlignment = Alignment.CenterVertically) { if (progress > 0f) { @@ -1119,10 +1059,7 @@ fun TracerouteActionButton(title: String, lastTracerouteTime: Long?, onClick: () @Composable fun NodeActionButton( - modifier: Modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .height(48.dp), + modifier: Modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).height(48.dp), title: String, enabled: Boolean, icon: ImageVector? = null, @@ -1156,8 +1093,8 @@ fun NodeActionSwitch( ) { val interactionSource = remember { MutableInteractionSource() } Card( - modifier = Modifier - .fillMaxWidth() + modifier = + Modifier.fillMaxWidth() .padding(vertical = 4.dp) .height(48.dp) .toggleable(value = checked, enabled = enabled, role = Role.Switch, onValueChange = { onClick() }), @@ -1167,9 +1104,7 @@ fun NodeActionSwitch( ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 12.dp, horizontal = 16.dp), + modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp, horizontal = 16.dp), ) { if (icon != null) { Icon( @@ -1268,10 +1203,5 @@ private fun PreviewWindDirectionN45() { @Composable private fun PreviewWindDirectionItem(windDirection: Float, windSpeed: String = "5 m/s") { val normalizedBearing = (windDirection + 180) % 360 - InfoCard( - icon = Icons.Outlined.Navigation, - text = "Wind", - value = windSpeed, - rotateIcon = normalizedBearing, - ) + InfoCard(icon = Icons.Outlined.Navigation, text = "Wind", value = windSpeed, rotateIcon = normalizedBearing) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeKeyStatusIcon.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeKeyStatusIcon.kt index ab6070fd4..3f8e2435c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeKeyStatusIcon.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeKeyStatusIcon.kt @@ -36,6 +36,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -46,7 +47,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -58,72 +58,54 @@ import com.geeksville.mesh.R import com.geeksville.mesh.model.Channel import com.geeksville.mesh.ui.common.components.CopyIconButton import com.geeksville.mesh.ui.common.theme.AppTheme +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow import com.google.protobuf.ByteString @Composable -private fun KeyStatusDialog( - @StringRes title: Int, - @StringRes text: Int, - key: ByteString?, - onDismiss: () -> Unit = {} -) = Dialog( - onDismissRequest = onDismiss, -) { - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.background - ) { - LazyColumn( - contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally +private fun KeyStatusDialog(@StringRes title: Int, @StringRes text: Int, key: ByteString?, onDismiss: () -> Unit = {}) = + Dialog(onDismissRequest = onDismiss) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.background, ) { - item { - Text( - text = stringResource(id = title), - textAlign = TextAlign.Center, - ) - Spacer(Modifier.height(16.dp)) - Text( - text = stringResource(id = text), - textAlign = TextAlign.Center, - ) - Spacer(Modifier.height(16.dp)) - if (key != null && title == R.string.encryption_pkc) { - val keyString = Base64.encodeToString(key.toByteArray(), Base64.NO_WRAP) - Text( - text = stringResource(id = R.string.config_security_public_key) + ":", - textAlign = TextAlign.Center, - ) - Spacer(Modifier.height(8.dp)) - SelectionContainer { + LazyColumn( + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { + Text(text = stringResource(id = title), textAlign = TextAlign.Center) + Spacer(Modifier.height(16.dp)) + Text(text = stringResource(id = text), textAlign = TextAlign.Center) + Spacer(Modifier.height(16.dp)) + if (key != null && title == R.string.encryption_pkc) { + val keyString = Base64.encodeToString(key.toByteArray(), Base64.NO_WRAP) Text( - text = keyString, + text = stringResource(id = R.string.config_security_public_key) + ":", textAlign = TextAlign.Center, ) + Spacer(Modifier.height(8.dp)) + SelectionContainer { Text(text = keyString, textAlign = TextAlign.Center) } + Spacer(Modifier.height(8.dp)) + CopyIconButton(valueToCopy = keyString, modifier = Modifier.padding(start = 8.dp)) + Spacer(Modifier.height(16.dp)) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton( + onClick = onDismiss, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface, + ), + ) { + Text(text = stringResource(id = R.string.close)) + } } - Spacer(Modifier.height(8.dp)) - CopyIconButton( - valueToCopy = keyString, - modifier = Modifier.padding(start = 8.dp) - ) - Spacer(Modifier.height(16.dp)) - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - ) { - TextButton( - onClick = onDismiss, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { Text(text = stringResource(id = R.string.close)) } } } } } -} @Composable fun NodeKeyStatusIcon( @@ -134,32 +116,33 @@ fun NodeKeyStatusIcon( ) { var showEncryptionDialog by remember { mutableStateOf(false) } if (showEncryptionDialog) { - val (title, text) = when { - mismatchKey -> R.string.encryption_error to R.string.encryption_error_text - hasPKC -> R.string.encryption_pkc to R.string.encryption_pkc_text - else -> R.string.encryption_psk to R.string.encryption_psk_text - } + val (title, text) = + when { + mismatchKey -> R.string.encryption_error to R.string.encryption_error_text + hasPKC -> R.string.encryption_pkc to R.string.encryption_pkc_text + else -> R.string.encryption_psk to R.string.encryption_psk_text + } KeyStatusDialog(title, text, publicKey) { showEncryptionDialog = false } } - val (icon, tint) = when { - mismatchKey -> Icons.Default.KeyOff to Color.Red - hasPKC -> Icons.Default.Lock to Color(color = 0xFF30C047) - else -> ImageVector.vectorResource(R.drawable.ic_lock_open_right_24) to Color(color = 0xFFFEC30A) - } + val (icon, tint) = + when { + mismatchKey -> Icons.Default.KeyOff to colorScheme.StatusRed + hasPKC -> Icons.Default.Lock to colorScheme.StatusGreen + else -> ImageVector.vectorResource(R.drawable.ic_lock_open_right_24) to colorScheme.StatusYellow + } - IconButton( - onClick = { showEncryptionDialog = true }, - modifier = modifier, - ) { + IconButton(onClick = { showEncryptionDialog = true }, modifier = modifier) { Icon( imageVector = icon, - contentDescription = stringResource( - id = when { + contentDescription = + stringResource( + id = + when { mismatchKey -> R.string.encryption_error hasPKC -> R.string.encryption_pkc else -> R.string.encryption_psk - } + }, ), tint = tint, ) @@ -169,13 +152,7 @@ fun NodeKeyStatusIcon( @PreviewLightDark @Composable private fun KeyStatusDialogErrorPreview() { - AppTheme { - KeyStatusDialog( - title = R.string.encryption_error, - text = R.string.encryption_error_text, - key = null, - ) - } + AppTheme { KeyStatusDialog(title = R.string.encryption_error, text = R.string.encryption_error_text, key = null) } } @PreviewLightDark @@ -193,11 +170,5 @@ private fun KeyStatusDialogPkcPreview() { @PreviewLightDark @Composable private fun KeyStatusDialogPskPreview() { - AppTheme { - KeyStatusDialog( - title = R.string.encryption_psk, - text = R.string.encryption_psk_text, - key = null, - ) - } + AppTheme { KeyStatusDialog(title = R.string.encryption_psk, text = R.string.encryption_psk_text, key = null) } } 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 6042c08a7..7f74f62cd 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 @@ -36,24 +36,19 @@ import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.geeksville.mesh.R +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed +import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun NodeStatusIcons( - isThisNode: Boolean, - isUnmessageable: Boolean, - isFavorite: Boolean, - isConnected: Boolean -) { - Row( - modifier = Modifier.padding(4.dp) - ) { +fun NodeStatusIcons(isThisNode: Boolean, isUnmessageable: Boolean, isFavorite: Boolean, isConnected: Boolean) { + Row(modifier = Modifier.padding(4.dp)) { if (isThisNode) { TooltipBox( positionProvider = TooltipDefaults.rememberTooltipPositionProvider(), @@ -65,12 +60,12 @@ fun NodeStatusIcons( R.string.connected } else { R.string.disconnected - } - ) + }, + ), ) } }, - state = rememberTooltipState() + state = rememberTooltipState(), ) { if (isConnected) { @Suppress("MagicNumber") @@ -78,14 +73,14 @@ fun NodeStatusIcons( imageVector = Icons.TwoTone.CloudDone, contentDescription = stringResource(R.string.connected), modifier = Modifier.size(24.dp), // Smaller size for badge - tint = Color(0xFF4CAF50) + tint = MaterialTheme.colorScheme.StatusGreen, ) } else { Icon( imageVector = Icons.TwoTone.CloudOff, contentDescription = stringResource(R.string.not_connected), modifier = Modifier.size(24.dp), // Smaller size for badge - tint = MaterialTheme.colorScheme.error + tint = MaterialTheme.colorScheme.StatusRed, ) } } @@ -94,23 +89,14 @@ fun NodeStatusIcons( if (isUnmessageable) { TooltipBox( positionProvider = TooltipDefaults.rememberTooltipPositionProvider(), - tooltip = { - PlainTooltip { - Text(stringResource(R.string.unmonitored_or_infrastructure)) - } - }, - state = rememberTooltipState() + tooltip = { PlainTooltip { Text(stringResource(R.string.unmonitored_or_infrastructure)) } }, + state = rememberTooltipState(), ) { - IconButton( - onClick = {}, - modifier = Modifier - .size(24.dp), - ) { + IconButton(onClick = {}, modifier = Modifier.size(24.dp)) { Icon( imageVector = Icons.Rounded.NoCell, contentDescription = stringResource(R.string.unmessageable), - modifier = Modifier - .size(24.dp), // Smaller size for badge + modifier = Modifier.size(24.dp), // Smaller size for badge ) } } @@ -118,24 +104,15 @@ fun NodeStatusIcons( if (isFavorite && !isThisNode) { TooltipBox( positionProvider = TooltipDefaults.rememberTooltipPositionProvider(), - tooltip = { - PlainTooltip { - Text(stringResource(R.string.favorite)) - } - }, - state = rememberTooltipState() + tooltip = { PlainTooltip { Text(stringResource(R.string.favorite)) } }, + state = rememberTooltipState(), ) { - IconButton( - onClick = {}, - modifier = Modifier - .size(24.dp), - ) { + IconButton(onClick = {}, modifier = Modifier.size(24.dp)) { Icon( imageVector = Icons.Rounded.Star, contentDescription = stringResource(R.string.favorite), - modifier = Modifier - .size(24.dp), // Smaller size for badge - tint = Color(color = 0xFFFEC30A) + modifier = Modifier.size(24.dp), // Smaller size for badge + tint = MaterialTheme.colorScheme.StatusYellow, ) } } @@ -146,10 +123,5 @@ fun NodeStatusIcons( @Preview @Composable fun StatusIconsPreview() { - NodeStatusIcons( - isThisNode = true, - isUnmessageable = true, - isFavorite = true, - isConnected = false, - ) + NodeStatusIcons(isThisNode = true, isUnmessageable = true, isFavorite = true, isConnected = false) }