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