Refine Compose manage UI parity and button behavior.

Apply legacy-accurate manage header/CTA styling, add reusable button icon support with previews, and include related map/navigation polish updates in this working tree.

Made-with: Cursor
This commit is contained in:
2026-03-01 19:02:13 +01:00
parent 9885162c4e
commit 7c00163304
9 changed files with 324 additions and 109 deletions

View File

@@ -5,12 +5,21 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -22,8 +31,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.db3.airmq.R
import org.db3.airmq.ui.theme.AirMQTheme
import org.db3.airmq.ui.theme.LegacyButtonContained import org.db3.airmq.ui.theme.LegacyButtonContained
import org.db3.airmq.ui.theme.LegacyButtonGradientEnd import org.db3.airmq.ui.theme.LegacyButtonGradientEnd
import org.db3.airmq.ui.theme.LegacyButtonGradientStart import org.db3.airmq.ui.theme.LegacyButtonGradientStart
@@ -32,7 +46,7 @@ import org.db3.airmq.ui.theme.LegacyButtonOnOutlined
import org.db3.airmq.ui.theme.LegacyButtonOnText import org.db3.airmq.ui.theme.LegacyButtonOnText
import org.db3.airmq.ui.theme.LegacyOutlineLight import org.db3.airmq.ui.theme.LegacyOutlineLight
enum class AirMqButtonStyle { enum class AirMQButtonStyle {
Contained, Contained,
Outlined, Outlined,
Text, Text,
@@ -40,52 +54,58 @@ enum class AirMqButtonStyle {
} }
private val LegacyButtonShape = RoundedCornerShape(18.dp) private val LegacyButtonShape = RoundedCornerShape(18.dp)
private val LegacyButtonHeight = 36.dp private val LegacyButtonHeight = 48.dp
private val LegacyDisabledContainer = Color(0xFFE0E0E0) private val LegacyDisabledContainer = Color(0xFFE0E0E0)
private val LegacyDisabledContent = Color(0x61000000) private val LegacyDisabledContent = Color(0x61000000)
@Composable @Composable
fun AirMqButton( fun AirMQButton(
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
style: AirMqButtonStyle = AirMqButtonStyle.Contained style: AirMQButtonStyle = AirMQButtonStyle.Contained,
leadingIconRes: Int? = null
) { ) {
when (style) { when (style) {
AirMqButtonStyle.Contained -> AirMqContainedButton( AirMQButtonStyle.Contained -> AirMQContainedButton(
text = text, text = text,
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled enabled = enabled,
leadingIconRes = leadingIconRes
) )
AirMqButtonStyle.Outlined -> AirMqOutlinedButton( AirMQButtonStyle.Outlined -> AirMQOutlinedButton(
text = text, text = text,
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled enabled = enabled,
leadingIconRes = leadingIconRes
) )
AirMqButtonStyle.Text -> AirMqTextButton( AirMQButtonStyle.Text -> AirMQTextButton(
text = text, text = text,
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled enabled = enabled,
leadingIconRes = leadingIconRes
) )
AirMqButtonStyle.Gradient -> AirMqGradientButton( AirMQButtonStyle.Gradient -> AirMQGradientButton(
text = text, text = text,
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled enabled = enabled,
leadingIconRes = leadingIconRes
) )
} }
} }
@Composable @Composable
fun AirMqContainedButton( fun AirMQContainedButton(
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true enabled: Boolean = true,
leadingIconRes: Int? = null
) { ) {
Button( Button(
onClick = onClick, onClick = onClick,
@@ -99,16 +119,21 @@ fun AirMqContainedButton(
disabledContentColor = LegacyDisabledContent disabledContentColor = LegacyDisabledContent
) )
) { ) {
Text(text = text, fontWeight = FontWeight.Medium) AirMQButtonLabel(
text = text,
leadingIconRes = leadingIconRes,
iconTint = if (enabled) LegacyButtonOnContained else LegacyDisabledContent
)
} }
} }
@Composable @Composable
fun AirMqOutlinedButton( fun AirMQOutlinedButton(
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true enabled: Boolean = true,
leadingIconRes: Int? = null
) { ) {
OutlinedButton( OutlinedButton(
onClick = onClick, onClick = onClick,
@@ -124,16 +149,21 @@ fun AirMqOutlinedButton(
disabledContentColor = LegacyDisabledContent disabledContentColor = LegacyDisabledContent
) )
) { ) {
Text(text = text, fontWeight = FontWeight.Medium) AirMQButtonLabel(
text = text,
leadingIconRes = leadingIconRes,
iconTint = if (enabled) LegacyButtonOnOutlined else LegacyDisabledContent
)
} }
} }
@Composable @Composable
fun AirMqTextButton( fun AirMQTextButton(
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true enabled: Boolean = true,
leadingIconRes: Int? = null
) { ) {
TextButton( TextButton(
onClick = onClick, onClick = onClick,
@@ -145,16 +175,21 @@ fun AirMqTextButton(
disabledContentColor = LegacyDisabledContent disabledContentColor = LegacyDisabledContent
) )
) { ) {
Text(text = text, fontWeight = FontWeight.Medium) AirMQButtonLabel(
text = text,
leadingIconRes = leadingIconRes,
iconTint = if (enabled) LegacyButtonOnText else LegacyDisabledContent
)
} }
} }
@Composable @Composable
fun AirMqGradientButton( fun AirMQGradientButton(
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true enabled: Boolean = true,
leadingIconRes: Int? = null
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState() val isPressed by interactionSource.collectIsPressedAsState()
@@ -183,6 +218,7 @@ fun AirMqGradientButton(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = LegacyButtonHeight)
.clip(LegacyButtonShape) .clip(LegacyButtonShape)
.background( .background(
brush = Brush.horizontalGradient( brush = Brush.horizontalGradient(
@@ -192,10 +228,104 @@ fun AirMqGradientButton(
.background(overlay), .background(overlay),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( AirMQButtonLabel(
text = text, text = text,
fontWeight = FontWeight.Medium, leadingIconRes = leadingIconRes,
color = if (enabled) LegacyButtonOnContained else LegacyDisabledContent iconTint = if (enabled) LegacyButtonOnContained else LegacyDisabledContent,
textColor = if (enabled) LegacyButtonOnContained else LegacyDisabledContent
)
}
}
}
@Composable
private fun AirMQButtonLabel(
text: String,
leadingIconRes: Int?,
iconTint: Color,
textColor: Color = Color.Unspecified
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (leadingIconRes != null) {
Icon(
painter = painterResource(id = leadingIconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(6.dp))
}
Text(
text = text.uppercase(),
fontWeight = FontWeight.Medium,
color = textColor,
textAlign = TextAlign.Start
)
}
}
@Preview(name = "Buttons - All Styles", showBackground = true)
@Composable
private fun AirMQButtonsPreviewAllStyles() {
AirMQTheme {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
AirMQButton(
text = "Contained",
onClick = {},
style = AirMQButtonStyle.Contained,
modifier = Modifier.fillMaxWidth()
)
AirMQButton(
text = "Outlined",
onClick = {},
style = AirMQButtonStyle.Outlined,
modifier = Modifier.fillMaxWidth()
)
AirMQButton(
text = "Text",
onClick = {},
style = AirMQButtonStyle.Text,
modifier = Modifier.fillMaxWidth()
)
AirMQButton(
text = "Gradient",
onClick = {},
style = AirMQButtonStyle.Gradient,
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Preview(name = "Buttons - Gradient With Icon", showBackground = true)
@Composable
private fun AirMQButtonsPreviewGradientWithIcon() {
AirMQTheme {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
AirMQButton(
text = "Sign In",
onClick = {},
style = AirMQButtonStyle.Gradient,
leadingIconRes = R.drawable.ic_account,
modifier = Modifier.fillMaxWidth()
)
AirMQButton(
text = "Sign In",
onClick = {},
style = AirMQButtonStyle.Gradient,
leadingIconRes = R.drawable.ic_account,
enabled = false,
modifier = Modifier.fillMaxWidth()
) )
} }
} }

View File

@@ -18,7 +18,7 @@ import androidx.compose.ui.unit.dp
data class ScreenAction( data class ScreenAction(
val label: String, val label: String,
val onClick: () -> Unit, val onClick: () -> Unit,
val style: AirMqButtonStyle = AirMqButtonStyle.Contained val style: AirMQButtonStyle = AirMQButtonStyle.Contained
) )
@Composable @Composable
@@ -61,7 +61,7 @@ private fun ScreenContent(
} }
content?.invoke() content?.invoke()
actions.forEach { action -> actions.forEach { action ->
AirMqButton( AirMQButton(
text = action.label, text = action.label,
onClick = action.onClick, onClick = action.onClick,
style = action.style, style = action.style,

View File

@@ -1,6 +1,7 @@
package org.db3.airmq.features.manage package org.db3.airmq.features.manage
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -10,12 +11,17 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -27,6 +33,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -34,8 +41,8 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import org.db3.airmq.R import org.db3.airmq.R
import org.db3.airmq.features.common.AirMqButton import org.db3.airmq.features.common.AirMQButton
import org.db3.airmq.features.common.AirMqButtonStyle import org.db3.airmq.features.common.AirMQButtonStyle
import org.db3.airmq.features.manage.ManageScreenContract.Action import org.db3.airmq.features.manage.ManageScreenContract.Action
import org.db3.airmq.features.manage.ManageScreenContract.DeviceItem import org.db3.airmq.features.manage.ManageScreenContract.DeviceItem
import org.db3.airmq.features.manage.ManageScreenContract.Event import org.db3.airmq.features.manage.ManageScreenContract.Event
@@ -92,10 +99,12 @@ private fun ManageScreenContent(
ProfileHeader( ProfileHeader(
name = uiState.userName, name = uiState.userName,
email = uiState.userEmail, email = uiState.userEmail,
isAnonymous = uiState.userMode == UserMode.ANONYMOUS,
onSettingsClick = { onEvent(Event.SettingsClicked) } onSettingsClick = { onEvent(Event.SettingsClicked) }
) )
when (uiState.userMode) { when (uiState.userMode) {
UserMode.ANONYMOUS -> AnonymousContent( UserMode.ANONYMOUS -> AnonymousContent(
modifier = Modifier.weight(1f),
devicesLabel = uiState.devicesLabel, devicesLabel = uiState.devicesLabel,
onSignIn = { onEvent(Event.SignInClicked) } onSignIn = { onEvent(Event.SignInClicked) }
) )
@@ -107,18 +116,19 @@ private fun ManageScreenContent(
onOpenLocation = { onEvent(Event.DeviceLocationClicked(it)) } onOpenLocation = { onEvent(Event.DeviceLocationClicked(it)) }
) )
} }
if (uiState.userMode == UserMode.AUTHORIZED) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
AirMqButton( AirMQButton(
text = stringResource(id = R.string.manage_open_widget_constructor), text = stringResource(id = R.string.manage_open_widget_constructor),
onClick = onOpenWidgetConstructor, onClick = onOpenWidgetConstructor,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) )
AirMqButton( AirMQButton(
text = stringResource(id = R.string.back_to_dashboard), text = stringResource(id = R.string.back_to_dashboard),
onClick = onBackToDashboard, onClick = onBackToDashboard,
style = AirMqButtonStyle.Outlined, style = AirMQButtonStyle.Outlined,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
@@ -126,11 +136,13 @@ private fun ManageScreenContent(
} }
} }
} }
}
@Composable @Composable
private fun ProfileHeader( private fun ProfileHeader(
name: String, name: String,
email: String, email: String,
isAnonymous: Boolean,
onSettingsClick: () -> Unit onSettingsClick: () -> Unit
) { ) {
Column( Column(
@@ -141,39 +153,44 @@ private fun ProfileHeader(
colors = listOf(LegacyNavGradientEnd, LegacyNavGradientStart) colors = listOf(LegacyNavGradientEnd, LegacyNavGradientStart)
) )
) )
.padding(16.dp) .statusBarsPadding()
.padding(horizontal = 16.dp)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
horizontalArrangement = Arrangement.SpaceBetween, .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.Top
) { ) {
Text( if (isAnonymous) {
text = stringResource(id = R.string.title_manage), Image(
style = MaterialTheme.typography.headlineSmall, painter = painterResource(id = R.drawable.placeholder_avatar_round),
color = Color.White contentDescription = stringResource(id = R.string.content_desc_user_pic),
modifier = Modifier
.padding(top = 24.dp)
.size(96.dp)
) )
AirMqButton( } else {
text = stringResource(id = R.string.title_settings),
onClick = onSettingsClick,
style = AirMqButtonStyle.Text
)
}
Spacer(modifier = Modifier.height(12.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Box( Box(
modifier = Modifier modifier = Modifier
.background(color = Color.White.copy(alpha = 0.25f), shape = CircleShape) .padding(top = 24.dp)
.padding(horizontal = 14.dp, vertical = 10.dp) .size(96.dp)
.background(color = Color.White.copy(alpha = 0.25f), shape = CircleShape),
contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = name.firstOrNull()?.uppercase() ?: "A", text = name.firstOrNull()?.uppercase() ?: "A",
color = Color.White, color = Color.White,
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Medium
) )
} }
Spacer(modifier = Modifier.padding(horizontal = 8.dp)) }
Column { Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier
.weight(1f)
.padding(top = 24.dp)
) {
Text( Text(
text = name, text = name,
color = Color.White, color = Color.White,
@@ -186,31 +203,50 @@ private fun ProfileHeader(
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
} }
IconButton(
onClick = onSettingsClick,
modifier = Modifier
.padding(top = 24.dp)
.size(36.dp)
) {
Icon(
painter = painterResource(id = R.drawable.ic_settings),
contentDescription = stringResource(id = R.string.content_settings),
tint = Color.White
)
} }
} }
Spacer(modifier = Modifier.height(24.dp))
}
} }
@Composable @Composable
private fun AnonymousContent( private fun AnonymousContent(
modifier: Modifier = Modifier,
devicesLabel: String, devicesLabel: String,
onSignIn: () -> Unit onSignIn: () -> Unit
) { ) {
Column( Box(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(horizontal = 16.dp, vertical = 12.dp)
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
text = devicesLabel, text = devicesLabel,
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Light),
color = Color(0xFFBDBDBD),
modifier = Modifier.align(Alignment.Center)
) )
Spacer(modifier = Modifier.height(16.dp)) AirMQButton(
AirMqButton(
text = stringResource(id = R.string.button_sign_in), text = stringResource(id = R.string.button_sign_in),
onClick = onSignIn, onClick = onSignIn,
style = AirMqButtonStyle.Gradient, style = AirMQButtonStyle.Gradient,
modifier = Modifier.fillMaxWidth() leadingIconRes = R.drawable.ic_account,
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(0.46f)
.height(48.dp)
.padding(bottom = 16.dp)
) )
} }
} }
@@ -230,10 +266,10 @@ private fun AuthorizedContent(
) { ) {
Text(text = devicesLabel, style = MaterialTheme.typography.titleMedium) Text(text = devicesLabel, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
AirMqButton( AirMQButton(
text = stringResource(id = R.string.button_setup), text = stringResource(id = R.string.button_setup),
onClick = onOpenSetup, onClick = onOpenSetup,
style = AirMqButtonStyle.Gradient, style = AirMQButtonStyle.Gradient,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
@@ -273,20 +309,20 @@ private fun DeviceRow(
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
AirMqButton( AirMQButton(
text = stringResource(id = R.string.button_open), text = stringResource(id = R.string.button_open),
onClick = onOpenDevice, onClick = onOpenDevice,
style = AirMqButtonStyle.Contained, style = AirMQButtonStyle.Contained,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
AirMqButton( AirMQButton(
text = if (item.hasLocation) { text = if (item.hasLocation) {
stringResource(id = R.string.button_view_on_map) stringResource(id = R.string.button_view_on_map)
} else { } else {
stringResource(id = R.string.manage_set_location) stringResource(id = R.string.manage_set_location)
}, },
onClick = onOpenLocation, onClick = onOpenLocation,
style = AirMqButtonStyle.Outlined, style = AirMQButtonStyle.Outlined,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }
@@ -300,7 +336,10 @@ private fun ManageScreenAnonymousPreview() {
AirMQTheme { AirMQTheme {
ManageScreenContent( ManageScreenContent(
uiState = State( uiState = State(
userMode = UserMode.ANONYMOUS userMode = UserMode.ANONYMOUS,
userName = "Anonymous user",
userEmail = "Your preferences are not being synced, please sign in",
devicesLabel = "Sign in to add devices"
), ),
onEvent = {}, onEvent = {},
onOpenWidgetConstructor = {}, onOpenWidgetConstructor = {},

View File

@@ -41,8 +41,8 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.db3.airmq.R import org.db3.airmq.R
import org.db3.airmq.features.common.AirMqButton import org.db3.airmq.features.common.AirMQButton
import org.db3.airmq.features.common.AirMqButtonStyle import org.db3.airmq.features.common.AirMQButtonStyle
import org.db3.airmq.features.map.MapScreenContract.DevicePanelState import org.db3.airmq.features.map.MapScreenContract.DevicePanelState
import org.db3.airmq.features.map.MapScreenContract.DeviceSensorType import org.db3.airmq.features.map.MapScreenContract.DeviceSensorType
import org.db3.airmq.features.map.MapScreenContract.SearchResult import org.db3.airmq.features.map.MapScreenContract.SearchResult
@@ -279,10 +279,10 @@ fun MapSearchOverlay(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
AirMqButton( AirMQButton(
text = stringResource(id = R.string.content_back), text = stringResource(id = R.string.content_back),
onClick = onClose, onClick = onClose,
style = AirMqButtonStyle.Outlined style = AirMQButtonStyle.Outlined
) )
OutlinedTextField( OutlinedTextField(
value = query, value = query,
@@ -356,10 +356,10 @@ fun MapDevicePanel(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
AirMqButton( AirMQButton(
text = stringResource(id = R.string.button_close), text = stringResource(id = R.string.button_close),
onClick = onClose, onClick = onClose,
style = AirMqButtonStyle.Text style = AirMQButtonStyle.Text
) )
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@@ -372,10 +372,10 @@ fun MapDevicePanel(
color = Color.Gray color = Color.Gray
) )
} }
AirMqButton( AirMQButton(
text = stringResource(id = R.string.button_open), text = stringResource(id = R.string.button_open),
onClick = onOpenDevice, onClick = onOpenDevice,
style = AirMqButtonStyle.Outlined style = AirMQButtonStyle.Outlined
) )
} }
@@ -386,19 +386,19 @@ fun MapDevicePanel(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
AirMqButton( AirMQButton(
text = stringResource(id = R.string.map_arrow_left), text = stringResource(id = R.string.map_arrow_left),
onClick = onDateBack, onClick = onDateBack,
style = AirMqButtonStyle.Text style = AirMQButtonStyle.Text
) )
Text( Text(
text = data.displayedDateRange, text = data.displayedDateRange,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
AirMqButton( AirMQButton(
text = stringResource(id = R.string.map_arrow_right), text = stringResource(id = R.string.map_arrow_right),
onClick = onDateForward, onClick = onDateForward,
style = AirMqButtonStyle.Text style = AirMQButtonStyle.Text
) )
} }

View File

@@ -42,6 +42,8 @@ class MapViewModel @Inject constructor(
private val _actions = MutableSharedFlow<Action>(extraBufferCapacity = 1) private val _actions = MutableSharedFlow<Action>(extraBufferCapacity = 1)
val actions: SharedFlow<Action> = _actions.asSharedFlow() val actions: SharedFlow<Action> = _actions.asSharedFlow()
private var showOfflineDevices = false
init { init {
refreshMapItems() refreshMapItems()
} }
@@ -158,14 +160,15 @@ class MapViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(isLoading = true) _uiState.value = _uiState.value.copy(isLoading = true)
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val result = runCatching { val result = runCatching {
val showOfflineDevices = settingsService.getOfflineDevicesVisible() showOfflineDevices = settingsService.getOfflineDevicesVisible()
mapService.fetchMapItems(showOfflineDevices = showOfflineDevices) mapService.fetchMapItems(showOfflineDevices = showOfflineDevices)
} }
_uiState.value = result.fold( _uiState.value = result.fold(
onSuccess = { items -> onSuccess = { items ->
domainItems = items domainItems = items
val searchPanelState = _uiState.value.searchPanelState val searchPanelState = _uiState.value.searchPanelState
val markers = items.toMarkers(_uiState.value.selectedTopSensor) val selectedSensorType = _uiState.value.selectedTopSensor
val markers = items.toMarkers(selectedSensorType)
_uiState.value.copy( _uiState.value.copy(
isLoading = false, isLoading = false,
items = markers, items = markers,
@@ -223,7 +226,17 @@ class MapViewModel @Inject constructor(
} }
private fun List<MapItem>.toMarkers(sensorType: SensorType): List<MapMarker> { private fun List<MapItem>.toMarkers(sensorType: SensorType): List<MapMarker> {
return map { item -> return mapNotNull { item ->
val value = when (sensorType) {
SensorType.DUST -> item.dustValue
SensorType.RADIOACTIVITY -> item.radioactivityValue
}
// Return null if device is offline or value is missing
if (!showOfflineDevices && (!item.isOnline || value == null )) {
return@mapNotNull null
}
MapMarker( MapMarker(
id = item.id, id = item.id,
title = item.title, title = item.title,
@@ -232,10 +245,7 @@ class MapViewModel @Inject constructor(
longitude = item.longitude, longitude = item.longitude,
isOnline = item.isOnline, isOnline = item.isOnline,
sensorType = sensorType, sensorType = sensorType,
value = when (sensorType) { value = value,
SensorType.DUST -> item.dustValue
SensorType.RADIOACTIVITY -> item.radioactivityValue
},
isOwned = false // TODO: derive from authenticated user when ownership/auth is implemented. isOwned = false // TODO: derive from authenticated user when ownership/auth is implemented.
) )
} }

View File

@@ -1,6 +1,7 @@
package org.db3.airmq.features.navigation package org.db3.airmq.features.navigation
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -64,6 +65,7 @@ fun AirMQNavGraph(modifier: Modifier = Modifier) {
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
contentWindowInsets = WindowInsets(0, 0, 0, 0),
bottomBar = { bottomBar = {
if (showBottomBar) { if (showBottomBar) {
Box( Box(

View File

@@ -0,0 +1,10 @@
<vector android:height="24dp"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:tools="http://schemas.android.com/tools"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"
tools:ignore="VectorPath" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="96dp"
android:height="96dp"
android:viewportWidth="96"
android:viewportHeight="96">
<path
android:pathData="M48,96L48,96C21.49,96 0,74.51 0,48v0C0,21.49 21.49,0 48,0h0c26.51,0 48,21.49 48,48v0C96,74.51 74.51,96 48,96z"
android:fillColor="#E6E6E6" />
<path
android:pathData="M46.79,83.76c-5.16,-0.07 -11.1,-1.5 -16.25,-5.39c-2.98,-2.26 -4.96,-5.17 -5.51,-8.96c-0.37,-2.58 0.36,-4.1 2.65,-5.32c0.23,-0.12 0.36,-0.5 0.47,-0.78c0.45,-1.22 0.9,-2.43 1.29,-3.67c0.15,-0.46 0.33,-1.02 0.19,-1.43c-0.66,-2.06 -1.49,-4.06 -2.13,-6.13c-0.99,-3.2 -0.94,-6.48 -0.51,-9.78c0.44,-3.45 0.7,-6.93 1.19,-10.38c0.46,-3.21 0.93,-6.44 1.73,-9.58c0.88,-3.48 3.38,-5.83 6.52,-7.34c7.65,-3.68 15.4,-3.67 23.06,-0.01c4.51,2.15 6.78,5.95 7.34,10.83c0.48,4.15 0.92,8.31 1.42,12.46c0.24,2 0.69,3.97 0.89,5.97c0.37,3.65 -0.27,7.17 -1.65,10.56c-0.26,0.64 -0.42,1.33 -0.72,1.95c-0.64,1.31 -0.99,2.57 0.25,3.75c0.1,0.1 0.17,0.27 0.2,0.42c0.3,1.77 0.98,3.12 2.59,4.3c1.72,1.26 1.32,3.48 0.85,5.41c-1.03,4.25 -3.84,7.09 -7.46,9.22C58.5,82.61 53.38,83.75 46.79,83.76zM45.49,38.67c-3.49,-2.2 -6.81,-4.38 -11,-4.36c-1.49,0 -2.5,1.18 -2.56,2.76c-0.07,1.69 1.08,3.52 2.6,4.2C37.59,42.63 42.66,41.29 45.49,38.67zM50.27,38.81c3.31,2.18 6.45,3.6 10.17,2.73c2.12,-0.49 3.29,-1.93 3.54,-4.06c0.27,-2.3 -1.64,-3.68 -4.32,-3.11c-3.11,0.66 -5.86,2.11 -8.49,3.82C50.94,38.34 50.73,38.5 50.27,38.81zM48.23,58.86c2,-0.09 3.9,-0.78 5.13,-1.85c0.97,-0.84 0.93,-1.6 -0.17,-2.18c-3.46,-1.82 -6.95,-1.83 -10.42,-0.02c-1.13,0.59 -1.21,1.36 -0.24,2.17C44.18,58.34 46.11,58.92 48.23,58.86z"
android:fillColor="#808080" />
</vector>