refactor(colors): consolidate colors in UI components (#2520)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-07-24 21:27:33 -05:00 committed by GitHub
parent c61d31c3b8
commit 6fd444c077
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1028 additions and 1386 deletions

View file

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

View file

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

View file

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