feat(metric): reimplement RoundedDiagram/RingDiagram in Compose
- Add MetricGauge, RingGauge, MetricGaugeRow composables - Add MetricGaugeContract with SensorType enum and AQI color mapping - Add AQI colors (SensorGreen, SensorYellow, etc.) to theme - Copy ic_temperature, ic_humidity, ic_pressure drawables from legacy - Integrate MetricGaugeRow into DashboardScreen - Add 9 preview composables for portfolio readiness Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,351 @@
|
|||||||
|
package org.db3.airmq.features.common.metric
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import org.db3.airmq.features.common.metric.getColorFromMetrics
|
||||||
|
import org.db3.airmq.features.common.metric.getSensorIconRes
|
||||||
|
import org.db3.airmq.ui.theme.AirMQTheme
|
||||||
|
import org.db3.airmq.ui.theme.LegacyNavGradientEnd
|
||||||
|
import org.db3.airmq.ui.theme.LegacyNavGradientStart
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single metric gauge with ring, value, units, and icon.
|
||||||
|
* Mirrors legacy [RoundedDiagram] layout and behavior.
|
||||||
|
*
|
||||||
|
* @param value Current value; null shows "?"
|
||||||
|
* @param sensorType Sensor type for icon, units, and progress logic
|
||||||
|
* @param selected When true, shows frosted background
|
||||||
|
* @param onClick Called when gauge is tapped
|
||||||
|
* @param modifier Modifier
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MetricGauge(
|
||||||
|
value: Float?,
|
||||||
|
sensorType: SensorType,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val (progress, progressColor) = when (sensorType) {
|
||||||
|
SensorType.HUMIDITY -> {
|
||||||
|
val p = value?.coerceIn(0f, 100f) ?: 0f
|
||||||
|
p to Color.White
|
||||||
|
}
|
||||||
|
SensorType.DUST -> {
|
||||||
|
val p = value?.coerceIn(0f, 100f) ?: 0f
|
||||||
|
val color = value?.let { getColorFromMetrics(it, sensorType) } ?: Color.White
|
||||||
|
p to color
|
||||||
|
}
|
||||||
|
else -> 0f to Color.White
|
||||||
|
}
|
||||||
|
|
||||||
|
val valueText = when {
|
||||||
|
value == null -> "?"
|
||||||
|
sensorType == SensorType.RADIOACTIVITY -> "%.2f".format(value)
|
||||||
|
else -> kotlin.math.round(value).toInt().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.widthIn(min = 96.dp)
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null
|
||||||
|
) { onClick() }
|
||||||
|
) {
|
||||||
|
if (selected) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.clip(RoundedCornerShape(18.dp))
|
||||||
|
.background(Color.White.copy(alpha = 0.1f))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
RingGauge(
|
||||||
|
progress = progress,
|
||||||
|
progressColor = progressColor,
|
||||||
|
modifier = Modifier.size(77.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = valueText,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = sensorType.units(),
|
||||||
|
color = Color.White.copy(alpha = 0.54f),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = getSensorIconRes(sensorType)),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.offset(y = 38.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Default sensor order for dashboard gauge row. */
|
||||||
|
private val DashboardGaugeOrder = listOf(
|
||||||
|
SensorType.DUST,
|
||||||
|
SensorType.RADIOACTIVITY,
|
||||||
|
SensorType.TEMPERATURE
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizontal row of metric gauges (dust, radioactivity, temperature).
|
||||||
|
*
|
||||||
|
* @param selectedSensor Currently selected sensor; null if none
|
||||||
|
* @param values Map of sensor type to value
|
||||||
|
* @param onGaugeSelected Called when a gauge is tapped
|
||||||
|
* @param modifier Modifier
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MetricGaugeRow(
|
||||||
|
selectedSensor: SensorType?,
|
||||||
|
values: Map<SensorType, Float?>,
|
||||||
|
onGaugeSelected: (SensorType) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
for (sensorType in DashboardGaugeOrder) {
|
||||||
|
MetricGauge(
|
||||||
|
value = values[sensorType],
|
||||||
|
sensorType = sensorType,
|
||||||
|
selected = selectedSensor == sensorType,
|
||||||
|
onClick = { onGaugeSelected(sensorType) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "Dust – value 6")
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMetricGaugeDust() {
|
||||||
|
AirMQTheme {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
androidx.compose.ui.graphics.Brush.verticalGradient(
|
||||||
|
colors = listOf(LegacyNavGradientStart, LegacyNavGradientEnd)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
MetricGauge(
|
||||||
|
value = 6f,
|
||||||
|
sensorType = SensorType.DUST,
|
||||||
|
selected = false,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "Radiation – value 0")
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMetricGaugeRadiation() {
|
||||||
|
AirMQTheme {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
androidx.compose.ui.graphics.Brush.verticalGradient(
|
||||||
|
colors = listOf(LegacyNavGradientStart, LegacyNavGradientEnd)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
MetricGauge(
|
||||||
|
value = 0f,
|
||||||
|
sensorType = SensorType.RADIOACTIVITY,
|
||||||
|
selected = false,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "Temperature – value 3")
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMetricGaugeTemperature() {
|
||||||
|
AirMQTheme {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
androidx.compose.ui.graphics.Brush.verticalGradient(
|
||||||
|
colors = listOf(LegacyNavGradientStart, LegacyNavGradientEnd)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
MetricGauge(
|
||||||
|
value = 3f,
|
||||||
|
sensorType = SensorType.TEMPERATURE,
|
||||||
|
selected = false,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "Humidity – value 65")
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMetricGaugeHumidity() {
|
||||||
|
AirMQTheme {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
androidx.compose.ui.graphics.Brush.verticalGradient(
|
||||||
|
colors = listOf(LegacyNavGradientStart, LegacyNavGradientEnd)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
MetricGauge(
|
||||||
|
value = 65f,
|
||||||
|
sensorType = SensorType.HUMIDITY,
|
||||||
|
selected = false,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "No data")
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMetricGaugeNoData() {
|
||||||
|
AirMQTheme {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
androidx.compose.ui.graphics.Brush.verticalGradient(
|
||||||
|
colors = listOf(LegacyNavGradientStart, LegacyNavGradientEnd)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
MetricGauge(
|
||||||
|
value = null,
|
||||||
|
sensorType = SensorType.DUST,
|
||||||
|
selected = false,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "Selected")
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMetricGaugeSelected() {
|
||||||
|
AirMQTheme {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
androidx.compose.ui.graphics.Brush.verticalGradient(
|
||||||
|
colors = listOf(LegacyNavGradientStart, LegacyNavGradientEnd)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
MetricGauge(
|
||||||
|
value = 6f,
|
||||||
|
sensorType = SensorType.DUST,
|
||||||
|
selected = true,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "Gauge row")
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMetricGaugeRow() {
|
||||||
|
AirMQTheme {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
androidx.compose.ui.graphics.Brush.verticalGradient(
|
||||||
|
colors = listOf(LegacyNavGradientStart, LegacyNavGradientEnd)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
MetricGaugeRow(
|
||||||
|
selectedSensor = SensorType.DUST,
|
||||||
|
values = mapOf(
|
||||||
|
SensorType.DUST to 6f,
|
||||||
|
SensorType.RADIOACTIVITY to 0f,
|
||||||
|
SensorType.TEMPERATURE to 3f
|
||||||
|
),
|
||||||
|
onGaugeSelected = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "Dust orange band")
|
||||||
|
@Composable
|
||||||
|
private fun PreviewMetricGaugeDustOrange() {
|
||||||
|
AirMQTheme {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
androidx.compose.ui.graphics.Brush.verticalGradient(
|
||||||
|
colors = listOf(LegacyNavGradientStart, LegacyNavGradientEnd)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
MetricGauge(
|
||||||
|
value = 40f,
|
||||||
|
sensorType = SensorType.DUST,
|
||||||
|
selected = false,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.db3.airmq.features.common.metric
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import org.db3.airmq.R
|
||||||
|
import org.db3.airmq.features.common.chart.getUnitsForSensor
|
||||||
|
import org.db3.airmq.ui.theme.SensorGreen
|
||||||
|
import org.db3.airmq.ui.theme.SensorOrange
|
||||||
|
import org.db3.airmq.ui.theme.SensorPink
|
||||||
|
import org.db3.airmq.ui.theme.SensorPurple
|
||||||
|
import org.db3.airmq.ui.theme.SensorRed
|
||||||
|
import org.db3.airmq.ui.theme.SensorYellow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sensor types supported by the metric gauge.
|
||||||
|
* Maps to legacy Chart sensor constants.
|
||||||
|
*/
|
||||||
|
enum class SensorType(val legacyKey: String) {
|
||||||
|
DUST("sensor_dust"),
|
||||||
|
RADIOACTIVITY("sensor_radioactivity"),
|
||||||
|
TEMPERATURE("sensor_temperature"),
|
||||||
|
HUMIDITY("sensor_humidity"),
|
||||||
|
PRESSURE("sensor_pressure"),
|
||||||
|
CO2("sensor_co2"),
|
||||||
|
VOC("sensor_voc");
|
||||||
|
|
||||||
|
fun units(): String = getUnitsForSensor(legacyKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AQI color bands for dust (PM2.5 µg/m³) per EPA scale.
|
||||||
|
* Returns progress ring color based on value.
|
||||||
|
*/
|
||||||
|
fun getColorFromMetrics(value: Float, sensorType: SensorType): Color = when (sensorType) {
|
||||||
|
SensorType.DUST -> when {
|
||||||
|
value <= 12f -> SensorGreen
|
||||||
|
value <= 35.4f -> SensorYellow
|
||||||
|
value <= 55.4f -> SensorOrange
|
||||||
|
value <= 150.4f -> SensorRed
|
||||||
|
value <= 250.4f -> SensorPink
|
||||||
|
else -> SensorPurple
|
||||||
|
}
|
||||||
|
else -> Color.White
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawable resource ID for the sensor icon.
|
||||||
|
*/
|
||||||
|
fun getSensorIconRes(sensorType: SensorType): Int = when (sensorType) {
|
||||||
|
SensorType.TEMPERATURE -> R.drawable.ic_temperature
|
||||||
|
SensorType.HUMIDITY -> R.drawable.ic_humidity
|
||||||
|
SensorType.PRESSURE -> R.drawable.ic_pressure
|
||||||
|
SensorType.RADIOACTIVITY -> R.drawable.ic_radiation
|
||||||
|
else -> R.drawable.ic_dust
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package org.db3.airmq.features.common.metric
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.db3.airmq.ui.theme.SensorGreen
|
||||||
|
|
||||||
|
private const val StartAngle = 135f
|
||||||
|
private const val FullSweepAngle = 270f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Circular progress gauge drawn as a 270° arc (7 o'clock to 4 o'clock).
|
||||||
|
* Mirrors legacy [RingDiagram] behavior.
|
||||||
|
*
|
||||||
|
* @param progress Progress 0–100, maps to arc sweep
|
||||||
|
* @param progressColor Color of the progress arc
|
||||||
|
* @param modifier Modifier
|
||||||
|
* @param size Total size of the gauge
|
||||||
|
* @param strokeWidth Stroke width of the arc
|
||||||
|
* @param backgroundColor Background ring color (default white38)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RingGauge(
|
||||||
|
progress: Float,
|
||||||
|
progressColor: Color,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp = 77.dp,
|
||||||
|
strokeWidth: Dp = 5.dp,
|
||||||
|
backgroundColor: Color = Color.White.copy(alpha = 0.38f)
|
||||||
|
) {
|
||||||
|
val animatedProgress by animateFloatAsState(
|
||||||
|
targetValue = progress.coerceIn(0f, 100f),
|
||||||
|
animationSpec = tween(durationMillis = 400),
|
||||||
|
label = "ring_progress"
|
||||||
|
)
|
||||||
|
val sweepDegrees = (FullSweepAngle / 100f) * animatedProgress
|
||||||
|
|
||||||
|
Canvas(
|
||||||
|
modifier = modifier.size(size)
|
||||||
|
) {
|
||||||
|
val strokePx = strokeWidth.toPx()
|
||||||
|
val halfStroke = strokePx / 2f
|
||||||
|
val topLeft = Offset(halfStroke, halfStroke)
|
||||||
|
val arcSize = androidx.compose.ui.geometry.Size(size.toPx() - strokePx, size.toPx() - strokePx)
|
||||||
|
|
||||||
|
// Background ring
|
||||||
|
drawArc(
|
||||||
|
color = backgroundColor,
|
||||||
|
startAngle = StartAngle,
|
||||||
|
sweepAngle = FullSweepAngle,
|
||||||
|
useCenter = false,
|
||||||
|
topLeft = topLeft,
|
||||||
|
size = arcSize,
|
||||||
|
style = Stroke(width = strokePx, cap = StrokeCap.Round)
|
||||||
|
)
|
||||||
|
// Progress ring
|
||||||
|
if (sweepDegrees > 0f) {
|
||||||
|
drawArc(
|
||||||
|
color = progressColor,
|
||||||
|
startAngle = StartAngle,
|
||||||
|
sweepAngle = sweepDegrees,
|
||||||
|
useCenter = false,
|
||||||
|
topLeft = topLeft,
|
||||||
|
size = arcSize,
|
||||||
|
style = Stroke(width = strokePx, cap = StrokeCap.Round)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "RingGauge – 40% progress")
|
||||||
|
@Composable
|
||||||
|
private fun PreviewRingGauge() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(100.dp)
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
RingGauge(
|
||||||
|
progress = 40f,
|
||||||
|
progressColor = SensorGreen
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,16 @@ package org.db3.airmq.features.dashboard
|
|||||||
|
|
||||||
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.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
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
|
||||||
@@ -13,6 +19,8 @@ import androidx.compose.ui.res.stringResource
|
|||||||
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.MockScreenScaffold
|
import org.db3.airmq.features.common.MockScreenScaffold
|
||||||
|
import org.db3.airmq.features.common.metric.MetricGaugeRow
|
||||||
|
import org.db3.airmq.features.common.metric.SensorType
|
||||||
import org.db3.airmq.features.common.ScreenAction
|
import org.db3.airmq.features.common.ScreenAction
|
||||||
import org.db3.airmq.features.common.chart.AirMQChart
|
import org.db3.airmq.features.common.chart.AirMQChart
|
||||||
import org.db3.airmq.features.common.chart.ChartConfig
|
import org.db3.airmq.features.common.chart.ChartConfig
|
||||||
@@ -36,30 +44,46 @@ fun DashboardScreen(
|
|||||||
title = stringResource(id = R.string.title_dashboard),
|
title = stringResource(id = R.string.title_dashboard),
|
||||||
subtitle = stringResource(id = R.string.dashboard_subtitle),
|
subtitle = stringResource(id = R.string.dashboard_subtitle),
|
||||||
content = {
|
content = {
|
||||||
Box(
|
var selectedSensor by remember { mutableStateOf<SensorType?>(SensorType.DUST) }
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(144.dp)
|
|
||||||
.background(
|
.background(
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
colors = listOf(LegacyNavGradientStart, LegacyNavGradientEnd)
|
colors = listOf(LegacyNavGradientStart, LegacyNavGradientEnd)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
AirMQChart(
|
MetricGaugeRow(
|
||||||
data = ChartDataset.Single(generateSineWaveData()),
|
selectedSensor = selectedSensor,
|
||||||
config = ChartConfig(
|
values = mapOf(
|
||||||
lineColor = Color.White,
|
SensorType.DUST to 6f,
|
||||||
fillColor = ChartFill,
|
SensorType.RADIOACTIVITY to 0f,
|
||||||
backgroundColor = ChartBackground,
|
SensorType.TEMPERATURE to 3f
|
||||||
labelColor = Color.White,
|
),
|
||||||
leftTimeLabel = stringResource(R.string.text_yesterday),
|
onGaugeSelected = { selectedSensor = it },
|
||||||
rightTimeLabel = stringResource(R.string.text_now),
|
modifier = Modifier.padding(vertical = 8.dp)
|
||||||
unit = "°C"
|
)
|
||||||
),
|
Box(
|
||||||
sensorType = "sensor_temperature",
|
modifier = Modifier
|
||||||
modifier = Modifier.fillMaxSize()
|
.fillMaxWidth()
|
||||||
)
|
.height(144.dp)
|
||||||
|
) {
|
||||||
|
AirMQChart(
|
||||||
|
data = ChartDataset.Single(generateSineWaveData()),
|
||||||
|
config = ChartConfig(
|
||||||
|
lineColor = Color.White,
|
||||||
|
fillColor = ChartFill,
|
||||||
|
backgroundColor = ChartBackground,
|
||||||
|
labelColor = Color.White,
|
||||||
|
leftTimeLabel = stringResource(R.string.text_yesterday),
|
||||||
|
rightTimeLabel = stringResource(R.string.text_now),
|
||||||
|
unit = "°C"
|
||||||
|
),
|
||||||
|
sensorType = "sensor_temperature",
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = listOf(
|
actions = listOf(
|
||||||
|
|||||||
@@ -44,3 +44,12 @@ val SensorDust25 = Color(0xFF4A90D9)
|
|||||||
val SensorDust10 = Color(0xFF81C784)
|
val SensorDust10 = Color(0xFF81C784)
|
||||||
val SensorDust1 = Color(0xFF90A4AE)
|
val SensorDust1 = Color(0xFF90A4AE)
|
||||||
val SensorRadioactivity = Color(0xFFE57373)
|
val SensorRadioactivity = Color(0xFFE57373)
|
||||||
|
|
||||||
|
// AQI / metric gauge colors (EPA scale for dust)
|
||||||
|
val SensorGreen = Color(0xFF00FF1E)
|
||||||
|
val SensorYellow = Color(0xFFFFBF00)
|
||||||
|
val SensorOrange = Color(0xFFFF6F00)
|
||||||
|
val SensorRed = Color(0xFFFF0000)
|
||||||
|
val SensorPink = Color(0xFFFF006A)
|
||||||
|
val SensorPurple = Color(0xFF6200FF)
|
||||||
|
val SensorGrey = Color(0xFF858585)
|
||||||
15
app/src/main/res/drawable/ic_humidity.xml
Normal file
15
app/src/main/res/drawable/ic_humidity.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M9.5,12.1c-0.11,-0.13 -0.27,-0.19 -0.46,-0.19c-0.2,0 -0.35,0.06 -0.46,0.19c-0.12,0.12 -0.17,0.29 -0.17,0.52v0.43c0,0.21 0.05,0.37 0.17,0.5c0.11,0.12 0.27,0.19 0.47,0.19c0.41,0 0.62,-0.27 0.62,-0.79V12.6C9.67,12.4 9.61,12.23 9.5,12.1zM9.5,12.1c-0.11,-0.13 -0.27,-0.19 -0.46,-0.19c-0.2,0 -0.35,0.06 -0.46,0.19c-0.12,0.12 -0.17,0.29 -0.17,0.52v0.43c0,0.21 0.05,0.37 0.17,0.5c0.11,0.12 0.27,0.19 0.47,0.19c0.41,0 0.62,-0.27 0.62,-0.79V12.6C9.67,12.4 9.61,12.23 9.5,12.1zM18.71,13.23c-0.43,-1.84 -1.3,-3.46 -2.29,-5.03C14.77,5.59 13,3.06 12.01,0c-0.24,0.61 -0.44,1.15 -0.66,1.68c-0.86,2.03 -2.07,3.86 -3.26,5.71C7.04,9 6.03,10.63 5.48,12.49c-0.55,1.9 -0.6,3.79 0.24,5.63c1.65,3.62 6.61,5.18 10.37,2.59C18.44,19.08 19.4,16.2 18.71,13.23zM7.65,14.33c-0.35,-0.32 -0.52,-0.76 -0.52,-1.31V12.6c0,-0.53 0.17,-0.96 0.52,-1.28c0.34,-0.33 0.81,-0.49 1.39,-0.49c0.59,0 1.05,0.16 1.39,0.48c0.34,0.33 0.51,0.77 0.51,1.32v0.42c0,0.53 -0.17,0.96 -0.51,1.28c-0.34,0.32 -0.8,0.48 -1.38,0.48S8.01,14.65 7.65,14.33zM10.5,18.34L9.31,17.6l4.19,-6.69l1.18,0.74L10.5,18.34zM16.88,16.58c0,0.53 -0.18,0.96 -0.52,1.28c-0.34,0.32 -0.79,0.48 -1.39,0.48c-0.57,0 -1.04,-0.16 -1.39,-0.48c-0.35,-0.32 -0.53,-0.76 -0.53,-1.31v-0.43c0,-0.53 0.18,-0.95 0.52,-1.28c0.35,-0.32 0.81,-0.48 1.39,-0.48c0.58,0 1.05,0.16 1.39,0.48c0.35,0.31 0.53,0.76 0.53,1.32V16.58zM9.04,11.91c-0.2,0 -0.35,0.06 -0.46,0.19c-0.12,0.12 -0.17,0.29 -0.17,0.52v0.43c0,0.21 0.05,0.37 0.17,0.5c0.11,0.12 0.27,0.19 0.47,0.19c0.41,0 0.62,-0.27 0.62,-0.79V12.6c0,-0.2 -0.06,-0.37 -0.17,-0.5S9.23,11.91 9.04,11.91zM9.5,12.1c-0.11,-0.13 -0.27,-0.19 -0.46,-0.19c-0.2,0 -0.35,0.06 -0.46,0.19c-0.12,0.12 -0.17,0.29 -0.17,0.52v0.43c0,0.21 0.05,0.37 0.17,0.5c0.11,0.12 0.27,0.19 0.47,0.19c0.41,0 0.62,-0.27 0.62,-0.79V12.6C9.67,12.4 9.61,12.23 9.5,12.1z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M15.6,16.12l-0.01,0.6c-0.02,0.37 -0.24,0.54 -0.62,0.54c-0.04,0 -0.09,-0.01 -0.13,-0.02c-0.01,0 -0.01,0 -0.02,0c-0.12,-0.02 -0.22,-0.08 -0.31,-0.18c-0.12,-0.14 -0.19,-0.3 -0.19,-0.48V16.1c0.02,-0.21 0.08,-0.37 0.18,-0.49c0.09,-0.08 0.2,-0.14 0.32,-0.16c0.05,-0.01 0.09,-0.01 0.14,-0.01c0.2,0 0.36,0.06 0.46,0.18C15.54,15.75 15.6,15.92 15.6,16.12z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M9.67,12.6v0.35c0,0.52 -0.21,0.79 -0.62,0.79c-0.2,0 -0.36,-0.07 -0.47,-0.19c-0.12,-0.13 -0.17,-0.29 -0.17,-0.5v-0.43c0,-0.23 0.05,-0.4 0.17,-0.52c0.11,-0.13 0.26,-0.19 0.46,-0.19c0.19,0 0.35,0.06 0.46,0.19S9.67,12.4 9.67,12.6z"/>
|
||||||
|
</vector>
|
||||||
36
app/src/main/res/drawable/ic_pressure.xml
Normal file
36
app/src/main/res/drawable/ic_pressure.xml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M4.59,19.15c-0.33,0 -0.65,-0.15 -0.87,-0.43c-1.54,-2.02 -2.29,-4.52 -2.11,-7.05C2.03,5.94 7.01,1.62 12.74,2.03c2.77,0.2 5.3,1.46 7.12,3.56c1.82,2.1 2.71,4.78 2.52,7.56c-0.14,2 -0.85,3.9 -2.05,5.5c-0.36,0.48 -1.04,0.58 -1.52,0.22c-0.48,-0.36 -0.58,-1.04 -0.22,-1.52c0.95,-1.26 1.51,-2.76 1.62,-4.35c0.16,-2.19 -0.55,-4.32 -1.99,-5.98c-1.44,-1.66 -3.44,-2.66 -5.63,-2.82c-4.52,-0.32 -8.47,3.1 -8.8,7.63c-0.14,2 0.45,3.98 1.67,5.58c0.36,0.48 0.27,1.16 -0.2,1.52C5.05,19.07 4.82,19.15 4.59,19.15z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M9.63,20.32v1.62H8.68v-4.59h1.79c0.34,0 0.65,0.06 0.91,0.19c0.26,0.13 0.46,0.31 0.6,0.54c0.14,0.23 0.21,0.5 0.21,0.79c0,0.45 -0.15,0.8 -0.46,1.06c-0.31,0.26 -0.73,0.39 -1.28,0.39H9.63zM9.63,19.55h0.85c0.25,0 0.44,-0.06 0.57,-0.18c0.13,-0.12 0.2,-0.29 0.2,-0.5c0,-0.22 -0.07,-0.41 -0.2,-0.55c-0.13,-0.14 -0.32,-0.21 -0.55,-0.21H9.63V19.55z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M14.73,21.94c-0.04,-0.08 -0.07,-0.18 -0.09,-0.31C14.42,21.88 14.13,22 13.78,22c-0.33,0 -0.61,-0.1 -0.83,-0.29c-0.22,-0.19 -0.33,-0.44 -0.33,-0.73c0,-0.36 0.13,-0.64 0.4,-0.83c0.27,-0.19 0.66,-0.29 1.16,-0.29h0.42v-0.2c0,-0.16 -0.04,-0.28 -0.12,-0.38c-0.08,-0.09 -0.21,-0.14 -0.38,-0.14c-0.15,0 -0.27,0.04 -0.36,0.11c-0.09,0.07 -0.13,0.17 -0.13,0.3h-0.91c0,-0.2 0.06,-0.38 0.18,-0.55c0.12,-0.17 0.29,-0.3 0.52,-0.4c0.22,-0.1 0.47,-0.14 0.75,-0.14c0.42,0 0.75,0.11 1,0.32c0.25,0.21 0.37,0.51 0.37,0.89v1.48c0,0.32 0.05,0.57 0.14,0.73v0.05H14.73zM13.98,21.3c0.13,0 0.26,-0.03 0.37,-0.09c0.11,-0.06 0.2,-0.14 0.25,-0.24v-0.59h-0.34c-0.46,0 -0.7,0.16 -0.73,0.47l0,0.05c0,0.11 0.04,0.21 0.12,0.28C13.73,21.27 13.84,21.3 13.98,21.3z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M16.75,7.72l-2.58,3.37c0.04,0.11 0.08,0.23 0.1,0.35c0.24,1.2 -0.54,2.37 -1.75,2.61c-0.22,0.04 -0.43,0.05 -0.64,0.03l-1.01,1.32c-0.08,0.11 -0.2,0.18 -0.34,0.21c-0.02,0 -0.05,0.01 -0.07,0.01c-0.18,0.01 -0.36,-0.05 -0.5,-0.18l-1.34,-1.28c-0.14,-0.13 -0.21,-0.31 -0.2,-0.49c0.01,-0.17 0.08,-0.32 0.21,-0.42l1.27,-1.06c-0.16,-1.16 0.61,-2.26 1.77,-2.49c0.39,-0.08 0.77,-0.05 1.12,0.07l3.25,-2.72c0.2,-0.16 0.47,-0.15 0.66,0.02C16.88,7.24 16.91,7.52 16.75,7.72z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M7.04,16.22m-0.64,0a0.64,0.64 0,1 1,1.28 0a0.64,0.64 0,1 1,-1.28 0"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M5.75,11.91m-0.64,0a0.64,0.64 0,1 1,1.28 0a0.64,0.64 0,1 1,-1.28 0"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M7.74,7.83m-0.64,0a0.64,0.64 0,1 1,1.28 0a0.64,0.64 0,1 1,-1.28 0"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M11.98,6.14m-0.64,0a0.64,0.64 0,1 1,1.28 0a0.64,0.64 0,1 1,-1.28 0"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M16.95,16.22m-0.64,0a0.64,0.64 0,1 1,1.28 0a0.64,0.64 0,1 1,-1.28 0"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M18.25,11.91m-0.64,0a0.64,0.64 0,1 1,1.28 0a0.64,0.64 0,1 1,-1.28 0"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_temperature.xml
Normal file
9
app/src/main/res/drawable/ic_temperature.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="36dp"
|
||||||
|
android:height="36dp"
|
||||||
|
android:viewportWidth="36"
|
||||||
|
android:viewportHeight="36">
|
||||||
|
<path
|
||||||
|
android:pathData="M22.68,8h-3.52V7.83c0,-0.7 -0.28,-1.33 -0.74,-1.79c-0.46,-0.46 -1.09,-0.74 -1.79,-0.74c-1.4,0 -2.53,1.13 -2.53,2.53v15.74c-0.9,0.74 -1.48,1.86 -1.48,3.11c0,2.21 1.8,4.01 4.01,4.01c2.21,0 4.01,-1.79 4.01,-4.01c0,-1.26 -0.58,-2.38 -1.48,-3.11v-1.41h1.96c0.39,0 0.7,-0.31 0.7,-0.7c0,-0.39 -0.31,-0.7 -0.7,-0.7h-1.96v-1.79h1.96c0.39,0 0.7,-0.31 0.7,-0.7c0,-0.39 -0.31,-0.7 -0.7,-0.7h-1.96v-1.79h1.96c0.39,0 0.7,-0.31 0.7,-0.7c0,-0.39 -0.31,-0.7 -0.7,-0.7h-1.96v-1.79h1.96c0.39,0 0.7,-0.31 0.7,-0.7c0,-0.39 -0.31,-0.7 -0.7,-0.7h-1.96V9.4h3.52c0.39,0 0.7,-0.31 0.7,-0.7S23.07,8 22.68,8zM16.63,28.43c-0.96,0 -1.74,-0.78 -1.74,-1.74c0,-0.21 0.04,-0.41 0.11,-0.6c0.16,-0.44 0.5,-0.8 0.93,-0.99V14.9c0,-0.19 0.08,-0.37 0.2,-0.49c0.13,-0.13 0.3,-0.2 0.49,-0.2c0.39,0 0.7,0.31 0.7,0.7v10.2c0.43,0.19 0.77,0.55 0.93,0.99c0.07,0.19 0.11,0.39 0.11,0.6C18.37,27.65 17.59,28.43 16.63,28.43z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
</vector>
|
||||||
Reference in New Issue
Block a user