feat(map): dropdown overlay above chart, sensor-based chart colors, supported sensors

Made-with: Cursor
This commit is contained in:
2026-03-04 22:25:21 +01:00
parent 8c54921661
commit e29e6ef498

View File

@@ -21,10 +21,14 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.runtime.getValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -32,6 +36,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
@@ -53,9 +58,35 @@ import org.db3.airmq.features.map.MapScreenContract.SearchResult
import org.db3.airmq.features.map.MapScreenContract.SensorType
import org.db3.airmq.features.map.MapScreenContract.TimeRange
import org.db3.airmq.ui.theme.AirMQTheme
import org.db3.airmq.ui.theme.ChartBackgroundWidget
import org.db3.airmq.ui.theme.ChartFillWidget
import org.db3.airmq.ui.theme.ChartLineWidget
private fun chartConfigForSensor(
sensor: DeviceSensorType,
leftTimeLabel: String = "",
rightTimeLabel: String = ""
): ChartConfig {
val (lineColor, unit) = when (sensor) {
DeviceSensorType.TEMPERATURE -> Color(0xFFFFB357) to "°C"
DeviceSensorType.DUST -> Color(0xFFEAB839) to "µg/m³"
DeviceSensorType.RADIOACTIVITY -> Color(0xFF8F3BB8) to "μSv/h"
}
val fillColor = lineColor.copy(alpha = 0.5f)
return ChartConfig(
lineColor = lineColor,
fillColor = fillColor,
backgroundColor = Color(0x0F000000),
labelColor = Color(0x8A000000),
roundCorners = false,
leftTimeLabel = leftTimeLabel,
rightTimeLabel = rightTimeLabel,
unit = unit
)
}
private fun sensorTypeString(sensor: DeviceSensorType): String = when (sensor) {
DeviceSensorType.TEMPERATURE -> "sensor_temperature"
DeviceSensorType.DUST -> "sensor_dust"
DeviceSensorType.RADIOACTIVITY -> "sensor_radioactivity"
}
@Composable
fun MapTopControls(
@@ -339,9 +370,8 @@ fun MapSearchOverlay(
}
@Composable
fun MapDevicePanel(
fun MapDevicePanelContent(
data: DevicePanelState,
onClose: () -> Unit,
onOpenDevice: () -> Unit,
onRangeSelected: (TimeRange) -> Unit,
onDateBack: () -> Unit,
@@ -349,25 +379,17 @@ fun MapDevicePanel(
onSensorSelected: (DeviceSensorType) -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White)
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
AirMQButton(
text = stringResource(id = R.string.button_close),
onClick = onClose,
style = AirMQButtonStyle.Text
)
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.Start
@@ -409,28 +431,58 @@ fun MapDevicePanel(
)
}
AirMQChart(
data = ChartDataset.Single(generateSineWaveData()),
config = ChartConfig(
lineColor = ChartLineWidget,
fillColor = ChartFillWidget,
backgroundColor = ChartBackgroundWidget,
labelColor = Color(0x8A000000),
leftTimeLabel = stringResource(R.string.filter_hour),
rightTimeLabel = stringResource(R.string.text_now),
unit = "°C"
),
sensorType = "sensor_temperature",
Box(
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
)
.height(256.dp)
) {
AirMQChart(
data = ChartDataset.Single(generateSineWaveData()),
config = chartConfigForSensor(
data.selectedSensor,
leftTimeLabel = stringResource(R.string.filter_hour),
rightTimeLabel = stringResource(R.string.text_now)
),
sensorType = sensorTypeString(data.selectedSensor),
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
.align(Alignment.TopCenter)
)
DeviceSensorRow(
selectedSensor = data.selectedSensor,
onSelected = onSensorSelected
)
DeviceSensorDropdown(
selectedSensor = data.selectedSensor,
supportedSensors = data.supportedSensors,
onSelected = onSensorSelected,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}
@Composable
fun MapDevicePanel(
data: DevicePanelState,
onOpenDevice: () -> Unit,
onRangeSelected: (TimeRange) -> Unit,
onDateBack: () -> Unit,
onDateForward: () -> Unit,
onSensorSelected: (DeviceSensorType) -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
MapDevicePanelContent(
data = data,
onOpenDevice = onOpenDevice,
onRangeSelected = onRangeSelected,
onDateBack = onDateBack,
onDateForward = onDateForward,
onSensorSelected = onSensorSelected
)
}
}
@@ -441,54 +493,198 @@ private fun TimeRangeRow(
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
FilterChip(
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
TimeRangeChip(
label = stringResource(id = R.string.filter_hour),
selected = selected == TimeRange.HOUR,
onClick = { onSelected(TimeRange.HOUR) },
label = { Text(stringResource(id = R.string.filter_hour)) }
onClick = { onSelected(TimeRange.HOUR) }
)
FilterChip(
TimeRangeChip(
label = stringResource(id = R.string.filter_day),
selected = selected == TimeRange.DAY,
onClick = { onSelected(TimeRange.DAY) },
label = { Text(stringResource(id = R.string.filter_day)) }
onClick = { onSelected(TimeRange.DAY) }
)
FilterChip(
TimeRangeChip(
label = stringResource(id = R.string.filter_week),
selected = selected == TimeRange.WEEK,
onClick = { onSelected(TimeRange.WEEK) },
label = { Text(stringResource(id = R.string.filter_week)) }
onClick = { onSelected(TimeRange.WEEK) }
)
FilterChip(
TimeRangeChip(
label = stringResource(id = R.string.filter_month),
selected = selected == TimeRange.MONTH,
onClick = { onSelected(TimeRange.MONTH) },
label = { Text(stringResource(id = R.string.filter_month)) }
onClick = { onSelected(TimeRange.MONTH) }
)
}
}
}
@Composable
private fun TimeRangeChip(
label: String,
selected: Boolean,
onClick: () -> Unit
) {
Box(
modifier = Modifier
.height(32.dp)
.clip(RoundedCornerShape(16.dp))
.then(
if (selected) {
Modifier.background(
brush = Brush.verticalGradient(
colors = listOf(Color(0xFF53AFA1), Color(0xFF247C84))
)
)
} else {
Modifier.background(Color.Transparent)
}
)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = if (selected) Color.White else Color(0x8A000000),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)
)
}
}
@Composable
private fun DeviceSensorRow(
private fun DeviceSensorDropdown(
selectedSensor: DeviceSensorType,
onSelected: (DeviceSensorType) -> Unit
supportedSensors: List<DeviceSensorType>,
onSelected: (DeviceSensorType) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
var isExpanded by remember { mutableStateOf(false) }
val sensors = supportedSensors.ifEmpty {
listOf(DeviceSensorType.TEMPERATURE, DeviceSensorType.DUST, DeviceSensorType.RADIOACTIVITY)
}
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
FilterChip(
selected = selectedSensor == DeviceSensorType.TEMPERATURE,
onClick = { onSelected(DeviceSensorType.TEMPERATURE) },
label = { Text(stringResource(id = R.string.sensor_temperature)) }
Column(
modifier = Modifier.width(204.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AnimatedVisibility(
visible = isExpanded,
enter = expandVertically(),
exit = shrinkVertically()
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp),
shadowElevation = 10.dp,
color = Color.White
) {
Column {
sensors.forEachIndexed { index, sensorType ->
if (index > 0) {
HorizontalDivider(color = Color(0x1F000000))
}
DeviceSensorDropdownOption(
sensorType = sensorType,
selected = selectedSensor == sensorType,
onClick = {
onSelected(sensorType)
isExpanded = false
}
)
}
}
}
}
val triggerShape = if (isExpanded) {
RoundedCornerShape(bottomStart = 18.dp, bottomEnd = 18.dp)
} else {
RoundedCornerShape(18.dp)
}
Surface(
modifier = Modifier
.fillMaxWidth()
.height(36.dp)
.clickable { isExpanded = !isExpanded },
shape = triggerShape,
shadowElevation = 10.dp,
color = Color.White
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = sensorIconRes(selectedSensor)),
contentDescription = null,
tint = Color(0x8A000000),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(id = sensorLabelRes(selectedSensor)),
style = MaterialTheme.typography.bodyMedium,
color = Color(0x8A000000)
)
Spacer(modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.drawable.ic_arrow_right_24),
contentDescription = null,
tint = Color(0x8A000000),
modifier = Modifier.size(24.dp)
)
}
}
}
}
}
private fun sensorIconRes(sensorType: DeviceSensorType): Int = when (sensorType) {
DeviceSensorType.TEMPERATURE -> R.drawable.ic_temperature
DeviceSensorType.DUST -> R.drawable.ic_dust
DeviceSensorType.RADIOACTIVITY -> R.drawable.ic_radiation
}
private fun sensorLabelRes(sensorType: DeviceSensorType): Int = when (sensorType) {
DeviceSensorType.TEMPERATURE -> R.string.sensor_temperature
DeviceSensorType.DUST -> R.string.sensor_dust
DeviceSensorType.RADIOACTIVITY -> R.string.sensor_radioactivity
}
@Composable
private fun DeviceSensorDropdownOption(
sensorType: DeviceSensorType,
selected: Boolean,
onClick: () -> Unit
) {
val textColor = if (selected) Color(0xFF1C1C1C) else Color(0xFF616161)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = sensorIconRes(sensorType)),
contentDescription = null,
tint = textColor,
modifier = Modifier.size(24.dp)
)
FilterChip(
selected = selectedSensor == DeviceSensorType.DUST,
onClick = { onSelected(DeviceSensorType.DUST) },
label = { Text(stringResource(id = R.string.sensor_dust)) }
)
FilterChip(
selected = selectedSensor == DeviceSensorType.RADIOACTIVITY,
onClick = { onSelected(DeviceSensorType.RADIOACTIVITY) },
label = { Text(stringResource(id = R.string.sensor_radioactivity)) }
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(id = sensorLabelRes(sensorType)),
style = MaterialTheme.typography.bodyMedium,
color = textColor
)
}
}
@@ -575,7 +771,6 @@ private fun PreviewMapDevicePanelDust() {
displayedDateRange = "Today",
selectedSensor = DeviceSensorType.DUST
),
onClose = {},
onOpenDevice = {},
onRangeSelected = {},
onDateBack = {},
@@ -598,7 +793,6 @@ private fun PreviewMapDevicePanelRadioactivity() {
displayedDateRange = "Last 7 days",
selectedSensor = DeviceSensorType.RADIOACTIVITY
),
onClose = {},
onOpenDevice = {},
onRangeSelected = {},
onDateBack = {},