diff --git a/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt
index c22ec64..de01919 100644
--- a/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt
+++ b/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt
@@ -23,6 +23,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.flow.collectLatest
+import org.db3.airmq.R
import org.db3.airmq.features.map.MapScreenContract.Action
import org.db3.airmq.features.map.MapScreenContract.Event
import org.db3.airmq.sdk.map.domain.MapItem
@@ -66,6 +67,13 @@ fun MapScreen(
MapTopControls(
selectedSensor = uiState.selectedTopSensor,
onSensorSelected = { viewModel.onEvent(Event.TopSensorSelected(it)) },
+ onHelpClick = {
+ Toast.makeText(
+ context,
+ context.getString(R.string.map_sensor_help_title),
+ Toast.LENGTH_SHORT
+ ).show()
+ },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 20.dp, end = 16.dp)
diff --git a/app/src/main/kotlin/org/db3/airmq/features/map/MapUiComponents.kt b/app/src/main/kotlin/org/db3/airmq/features/map/MapUiComponents.kt
index f9d7aa2..188b605 100644
--- a/app/src/main/kotlin/org/db3/airmq/features/map/MapUiComponents.kt
+++ b/app/src/main/kotlin/org/db3/airmq/features/map/MapUiComponents.kt
@@ -7,26 +7,38 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilterChip
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+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.rotate
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.db3.airmq.R
import org.db3.airmq.features.common.AirMqButton
@@ -36,38 +48,168 @@ import org.db3.airmq.features.map.MapScreenContract.DeviceSensorType
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
@Composable
fun MapTopControls(
selectedSensor: SensorType,
onSensorSelected: (SensorType) -> Unit,
+ onHelpClick: () -> Unit,
+ initiallyExpanded: Boolean = false,
modifier: Modifier = Modifier
) {
- Surface(
- modifier = modifier,
- shape = RoundedCornerShape(22.dp),
- tonalElevation = 2.dp,
- shadowElevation = 6.dp,
- color = Color.White
+ var isExpanded by remember(initiallyExpanded) { mutableStateOf(initiallyExpanded) }
+ val arrowRotation = if (isExpanded) 180f else 0f
+ val selectedLabel = when (selectedSensor) {
+ SensorType.DUST -> stringResource(id = R.string.map_sensor_air_quality)
+ SensorType.RADIOACTIVITY -> stringResource(id = R.string.map_sensor_radioactivity)
+ }
+
+ Box(
+ modifier = modifier
+ .width(234.dp)
+ .wrapContentHeight()
) {
- Row(
- modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
+ Column(
+ modifier = Modifier
+ .align(Alignment.TopStart)
+ .padding(start = 10.dp, top = 10.dp, end = 10.dp)
) {
- FilterChip(
- selected = selectedSensor == SensorType.DUST,
- onClick = { onSensorSelected(SensorType.DUST) },
- label = { Text(stringResource(id = R.string.map_sensor_dust)) }
- )
- FilterChip(
- selected = selectedSensor == SensorType.RADIOACTIVITY,
- onClick = { onSensorSelected(SensorType.RADIOACTIVITY) },
- label = { Text(stringResource(id = R.string.map_sensor_radioactivity)) }
+ Surface(
+ modifier = Modifier
+ .width(214.dp)
+ .height(36.dp)
+ .clickable { isExpanded = !isExpanded },
+ shape = RoundedCornerShape(18.dp),
+ shadowElevation = 10.dp,
+ color = Color.White
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(start = 12.dp, end = 48.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_arrow_down_dark),
+ contentDescription = null,
+ tint = Color(0x8A000000),
+ modifier = Modifier.rotate(arrowRotation)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = selectedLabel.uppercase(),
+ style = MaterialTheme.typography.labelLarge,
+ color = Color(0x61000000)
+ )
+ }
+ }
+
+ if (isExpanded) {
+ Surface(
+ modifier = Modifier
+ .width(214.dp)
+ .padding(top = 8.dp),
+ shape = RoundedCornerShape(18.dp),
+ shadowElevation = 10.dp,
+ color = Color.White
+ ) {
+ Column(modifier = Modifier.padding(top = 8.dp, bottom = 10.dp)) {
+ SensorTypeItem(
+ iconRes = R.drawable.ic_dust,
+ titleRes = R.string.map_sensor_air_quality,
+ selected = selectedSensor == SensorType.DUST,
+ onClick = {
+ onSensorSelected(SensorType.DUST)
+ isExpanded = false
+ }
+ )
+ HorizontalDivider(color = Color(0x1F000000))
+ SensorTypeItem(
+ iconRes = R.drawable.ic_radiation,
+ titleRes = R.string.map_sensor_radioactivity,
+ selected = selectedSensor == SensorType.RADIOACTIVITY,
+ onClick = {
+ onSensorSelected(SensorType.RADIOACTIVITY)
+ isExpanded = false
+ }
+ )
+ Text(
+ text = stringResource(id = R.string.map_sensor_help_link),
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(top = 12.dp)
+ .clickable {
+ onHelpClick()
+ isExpanded = false
+ },
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color(0x8A000000),
+ textDecoration = TextDecoration.Underline
+ )
+ }
+ }
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .size(56.dp)
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(Color(0xFF00FF6D), Color(0xFF03AEF9))
+ ),
+ shape = CircleShape
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(
+ id = if (selectedSensor == SensorType.RADIOACTIVITY) {
+ R.drawable.ic_radiation
+ } else {
+ R.drawable.ic_dust
+ }
+ ),
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
)
}
}
}
+@Composable
+private fun SensorTypeItem(
+ iconRes: Int,
+ titleRes: Int,
+ 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 = iconRes),
+ contentDescription = null,
+ tint = textColor,
+ modifier = Modifier.size(18.dp)
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ Text(
+ text = stringResource(id = titleRes),
+ style = MaterialTheme.typography.bodyMedium,
+ color = textColor
+ )
+ }
+}
+
@Composable
fun MapFloatingActions(
onSearchClick: () -> Unit,
@@ -340,3 +482,118 @@ private fun DeviceSensorRow(
)
}
}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewMapTopControlsCollapsedDust() {
+ AirMQTheme {
+ MapTopControls(
+ selectedSensor = SensorType.DUST,
+ onSensorSelected = {},
+ onHelpClick = {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewMapTopControlsExpandedDust() {
+ AirMQTheme {
+ MapTopControls(
+ selectedSensor = SensorType.DUST,
+ onSensorSelected = {},
+ onHelpClick = {},
+ initiallyExpanded = true
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewMapTopControlsExpandedRadioactivity() {
+ AirMQTheme {
+ MapTopControls(
+ selectedSensor = SensorType.RADIOACTIVITY,
+ onSensorSelected = {},
+ onHelpClick = {},
+ initiallyExpanded = true
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewMapSearchOverlayEmpty() {
+ AirMQTheme {
+ MapSearchOverlay(
+ query = "Minsk",
+ results = emptyList(),
+ onQueryChanged = {},
+ onClose = {},
+ onResultClick = {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewMapSearchOverlayWithResults() {
+ AirMQTheme {
+ MapSearchOverlay(
+ query = "Minsk",
+ results = listOf(
+ SearchResult(id = "1", title = "AirMQ #42", subtitle = "Minsk"),
+ SearchResult(id = "2", title = "AirMQ #91", subtitle = "Salihorsk")
+ ),
+ onQueryChanged = {},
+ onClose = {},
+ onResultClick = {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewMapDevicePanelDust() {
+ AirMQTheme {
+ MapDevicePanel(
+ data = DevicePanelState(
+ id = "42",
+ name = "AirMQ #42",
+ status = "Online",
+ selectedRange = TimeRange.DAY,
+ displayedDateRange = "Today",
+ selectedSensor = DeviceSensorType.DUST
+ ),
+ onClose = {},
+ onOpenDevice = {},
+ onRangeSelected = {},
+ onDateBack = {},
+ onDateForward = {},
+ onSensorSelected = {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewMapDevicePanelRadioactivity() {
+ AirMQTheme {
+ MapDevicePanel(
+ data = DevicePanelState(
+ id = "43",
+ name = "AirMQ #43",
+ status = "Offline",
+ selectedRange = TimeRange.WEEK,
+ displayedDateRange = "Last 7 days",
+ selectedSensor = DeviceSensorType.RADIOACTIVITY
+ ),
+ onClose = {},
+ onOpenDevice = {},
+ onRangeSelected = {},
+ onDateBack = {},
+ onDateForward = {},
+ onSensorSelected = {}
+ )
+ }
+}
diff --git a/app/src/main/res/drawable/ic_arrow_down_dark.xml b/app/src/main/res/drawable/ic_arrow_down_dark.xml
new file mode 100644
index 0000000..985760f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_down_dark.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_dust.xml b/app/src/main/res/drawable/ic_dust.xml
new file mode 100644
index 0000000..452b0f8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dust.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_radiation.xml b/app/src/main/res/drawable/ic_radiation.xml
new file mode 100644
index 0000000..f669bde
--- /dev/null
+++ b/app/src/main/res/drawable/ic_radiation.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 29a750f..c19d9e1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,7 +1,10 @@
AirMQ
Dust
+ Air quality
Radioactivity
+ What does this mean?
+ What do AirMQ sensors measure?
Search
My location
Back