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.FilterChip
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text 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.getValue
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -32,6 +36,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color 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.SensorType
import org.db3.airmq.features.map.MapScreenContract.TimeRange import org.db3.airmq.features.map.MapScreenContract.TimeRange
import org.db3.airmq.ui.theme.AirMQTheme import org.db3.airmq.ui.theme.AirMQTheme
import org.db3.airmq.ui.theme.ChartBackgroundWidget
import org.db3.airmq.ui.theme.ChartFillWidget private fun chartConfigForSensor(
import org.db3.airmq.ui.theme.ChartLineWidget 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 @Composable
fun MapTopControls( fun MapTopControls(
@@ -339,9 +370,8 @@ fun MapSearchOverlay(
} }
@Composable @Composable
fun MapDevicePanel( fun MapDevicePanelContent(
data: DevicePanelState, data: DevicePanelState,
onClose: () -> Unit,
onOpenDevice: () -> Unit, onOpenDevice: () -> Unit,
onRangeSelected: (TimeRange) -> Unit, onRangeSelected: (TimeRange) -> Unit,
onDateBack: () -> Unit, onDateBack: () -> Unit,
@@ -349,25 +379,17 @@ fun MapDevicePanel(
onSensorSelected: (DeviceSensorType) -> Unit, onSensorSelected: (DeviceSensorType) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Card( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), .fillMaxWidth()
colors = CardDefaults.cardColors(containerColor = Color.White) .padding(horizontal = 16.dp, vertical = 14.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) { ) {
Column( Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(14.dp) 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( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.Start horizontalAlignment = Alignment.Start
@@ -409,28 +431,58 @@ fun MapDevicePanel(
) )
} }
AirMQChart( Box(
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",
modifier = Modifier modifier = Modifier
.fillMaxWidth() .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( DeviceSensorDropdown(
selectedSensor = data.selectedSensor, selectedSensor = data.selectedSensor,
onSelected = onSensorSelected 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( Row(
modifier = Modifier.fillMaxWidth(), 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, selected = selected == TimeRange.HOUR,
onClick = { onSelected(TimeRange.HOUR) }, onClick = { onSelected(TimeRange.HOUR) }
label = { Text(stringResource(id = R.string.filter_hour)) }
) )
FilterChip( TimeRangeChip(
label = stringResource(id = R.string.filter_day),
selected = selected == TimeRange.DAY, selected = selected == TimeRange.DAY,
onClick = { onSelected(TimeRange.DAY) }, onClick = { onSelected(TimeRange.DAY) }
label = { Text(stringResource(id = R.string.filter_day)) }
) )
FilterChip( TimeRangeChip(
label = stringResource(id = R.string.filter_week),
selected = selected == TimeRange.WEEK, selected = selected == TimeRange.WEEK,
onClick = { onSelected(TimeRange.WEEK) }, onClick = { onSelected(TimeRange.WEEK) }
label = { Text(stringResource(id = R.string.filter_week)) }
) )
FilterChip( TimeRangeChip(
label = stringResource(id = R.string.filter_month),
selected = selected == TimeRange.MONTH, selected = selected == TimeRange.MONTH,
onClick = { onSelected(TimeRange.MONTH) }, onClick = { onSelected(TimeRange.MONTH) }
label = { Text(stringResource(id = R.string.filter_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 @Composable
private fun DeviceSensorRow( private fun DeviceSensorDropdown(
selectedSensor: DeviceSensorType, selectedSensor: DeviceSensorType,
onSelected: (DeviceSensorType) -> Unit supportedSensors: List<DeviceSensorType>,
onSelected: (DeviceSensorType) -> Unit,
modifier: Modifier = Modifier
) { ) {
Row( var isExpanded by remember { mutableStateOf(false) }
modifier = Modifier.fillMaxWidth(), val sensors = supportedSensors.ifEmpty {
horizontalArrangement = Arrangement.spacedBy(8.dp) listOf(DeviceSensorType.TEMPERATURE, DeviceSensorType.DUST, DeviceSensorType.RADIOACTIVITY)
}
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) { ) {
FilterChip( Column(
selected = selectedSensor == DeviceSensorType.TEMPERATURE, modifier = Modifier.width(204.dp),
onClick = { onSelected(DeviceSensorType.TEMPERATURE) }, horizontalAlignment = Alignment.CenterHorizontally
label = { Text(stringResource(id = R.string.sensor_temperature)) } ) {
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( Spacer(modifier = Modifier.width(16.dp))
selected = selectedSensor == DeviceSensorType.DUST, Text(
onClick = { onSelected(DeviceSensorType.DUST) }, text = stringResource(id = sensorLabelRes(sensorType)),
label = { Text(stringResource(id = R.string.sensor_dust)) } style = MaterialTheme.typography.bodyMedium,
) color = textColor
FilterChip(
selected = selectedSensor == DeviceSensorType.RADIOACTIVITY,
onClick = { onSelected(DeviceSensorType.RADIOACTIVITY) },
label = { Text(stringResource(id = R.string.sensor_radioactivity)) }
) )
} }
} }
@@ -575,7 +771,6 @@ private fun PreviewMapDevicePanelDust() {
displayedDateRange = "Today", displayedDateRange = "Today",
selectedSensor = DeviceSensorType.DUST selectedSensor = DeviceSensorType.DUST
), ),
onClose = {},
onOpenDevice = {}, onOpenDevice = {},
onRangeSelected = {}, onRangeSelected = {},
onDateBack = {}, onDateBack = {},
@@ -598,7 +793,6 @@ private fun PreviewMapDevicePanelRadioactivity() {
displayedDateRange = "Last 7 days", displayedDateRange = "Last 7 days",
selectedSensor = DeviceSensorType.RADIOACTIVITY selectedSensor = DeviceSensorType.RADIOACTIVITY
), ),
onClose = {},
onOpenDevice = {}, onOpenDevice = {},
onRangeSelected = {}, onRangeSelected = {},
onDateBack = {}, onDateBack = {},