feat: improve dashboard status indicators with chips and new icons (#69)

* feat: improve dashboard status indicators with chips and new icons

- Add StatusChip composable for pill-style status indicators
- Use distinct icons for temperatures: car icon (inside), sun icon (outside)
- Add Driving status with DriveEta icon when vehicle is driving
- Add red sentry mode dot next to lock indicator when active
- Add i18n strings for driving, sentry_mode_active, inside_temp, outside_temp

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: simplify status indicators to icon-only with tooltips

- Replace chip-style indicators with simple icons
- Add tooltips on tap for all status icons
- Power icon: green when online, grey otherwise
- Lock icon: grey when locked, light red when unlocked
- Sentry mode: red dot shown when active
- Plug icon: grey, shown when plugged in
- Temperatures: "Ext:" and "Int:" labels with thermometer
- Int label and value bold when climate is on

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: make status icon tooltips show on tap instead of long-press

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: make Int temperature green when climate is active

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add climate status tooltip to temperature indicators

Tapping on either Ext or Int temperature now shows tooltip
indicating whether climate is active or inactive.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Davide Ferrari
2026-01-18 20:52:12 +01:00
committed by GitHub
parent 6afc5bd328
commit 20e5226062
5 changed files with 260 additions and 80 deletions

View File

@@ -47,12 +47,17 @@ import androidx.compose.material.icons.filled.DriveEta
import androidx.compose.material.icons.filled.Terrain
import androidx.compose.material.icons.filled.Thermostat
import androidx.compose.material.icons.filled.Timeline
import androidx.compose.material.icons.filled.WbSunny
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Check
import com.matedroid.ui.icons.CustomIcons
import androidx.compose.material.icons.filled.Timer
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.material3.TextButton
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -82,7 +87,9 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import kotlinx.coroutines.launch
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
@@ -625,6 +632,41 @@ private fun CarImage(
}
}
/**
* An icon with a tooltip that appears on tap
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun StatusIcon(
icon: ImageVector,
tooltipText: String,
tint: Color,
modifier: Modifier = Modifier,
iconSize: Int = 18
) {
val tooltipState = rememberTooltipState(isPersistent = true)
val scope = rememberCoroutineScope()
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(tooltipText)
}
},
state = tooltipState
) {
Icon(
imageVector = icon,
contentDescription = tooltipText,
modifier = modifier
.size(iconSize.dp)
.clickable { scope.launch { tooltipState.show() } },
tint = tint
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun StatusIndicatorsRow(
status: CarStatus,
@@ -632,99 +674,141 @@ private fun StatusIndicatorsRow(
palette: CarColorPalette,
modifier: Modifier = Modifier
) {
val isSentryModeActive = status.sentryMode == true
val isClimateOn = status.isClimateOn == true
val isOnline = status.state?.lowercase() == "online"
val isLocked = status.locked == true
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Left side: State and Lock
Row(verticalAlignment = Alignment.CenterVertically) {
// State indicator - icon changes based on state
val stateIcon = when {
status.isCharging || status.pluggedIn == true -> Icons.Filled.PowerSettingsNew
status.state?.lowercase() == "online" -> Icons.Filled.Circle
status.state?.lowercase() in listOf("asleep", "offline", "suspended") -> Icons.Filled.Bedtime
else -> Icons.Filled.Circle
// Left side: Status icons
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Power/state icon - green when online, grey otherwise
StatusIcon(
icon = Icons.Filled.PowerSettingsNew,
tooltipText = status.state?.replaceFirstChar { it.uppercase() } ?: stringResource(R.string.unknown),
tint = if (isOnline) StatusSuccess else palette.onSurfaceVariant
)
// Lock icon - grey when locked, light red when unlocked
StatusIcon(
icon = if (isLocked) Icons.Filled.Lock else Icons.Filled.LockOpen,
tooltipText = stringResource(if (isLocked) R.string.locked else R.string.unlocked),
tint = if (isLocked) palette.onSurfaceVariant else StatusError.copy(alpha = 0.7f)
)
// Sentry mode red dot (if active)
if (isSentryModeActive) {
val sentryTooltipState = rememberTooltipState(isPersistent = true)
val scope = rememberCoroutineScope()
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(stringResource(R.string.sentry_mode_active))
}
},
state = sentryTooltipState
) {
Box(
modifier = Modifier
.size(12.dp)
.background(StatusError, RoundedCornerShape(6.dp))
.clickable { scope.launch { sentryTooltipState.show() } }
)
}
}
val stateColor = when {
status.isCharging -> StatusSuccess
status.pluggedIn == true -> palette.accent
status.state?.lowercase() == "online" -> StatusSuccess
else -> palette.onSurfaceVariant
}
Icon(
imageVector = stateIcon,
contentDescription = null,
modifier = Modifier.size(if (stateIcon == Icons.Filled.Circle) 10.dp else 16.dp),
tint = stateColor
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = status.state?.replaceFirstChar { it.uppercase() } ?: stringResource(R.string.unknown),
style = MaterialTheme.typography.labelMedium,
color = stateColor
)
Spacer(modifier = Modifier.width(12.dp))
// Locked indicator
val isLocked = status.locked == true
Icon(
imageVector = if (isLocked) Icons.Filled.Lock else Icons.Filled.LockOpen,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = if (isLocked) StatusSuccess else StatusWarning
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = stringResource(if (isLocked) R.string.locked else R.string.unlocked),
style = MaterialTheme.typography.labelMedium,
color = if (isLocked) StatusSuccess else StatusWarning
)
// Plug indicator (only when plugged in but not charging - charging already shows state)
if (status.pluggedIn == true && !status.isCharging) {
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Filled.Power,
contentDescription = stringResource(R.string.plugged_in),
modifier = Modifier.size(16.dp),
tint = palette.accent
// Plug icon (grey, if plugged in)
if (status.pluggedIn == true) {
StatusIcon(
icon = Icons.Filled.Power,
tooltipText = stringResource(R.string.plugged_in),
tint = palette.onSurfaceVariant
)
}
}
// Right side: Temperatures
Row(verticalAlignment = Alignment.CenterVertically) {
// Inside temp
Icon(
imageVector = Icons.Filled.Thermostat,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = palette.onSurfaceVariant
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = status.insideTemp?.let { UnitFormatter.formatTemperature(it, units) } ?: "--",
style = MaterialTheme.typography.labelMedium,
color = palette.onSurfaceVariant
)
// Right side: Temperature indicators with labels
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
val climateTooltip = stringResource(if (isClimateOn) R.string.climate_active else R.string.climate_inactive)
val scope = rememberCoroutineScope()
Spacer(modifier = Modifier.width(8.dp))
// Outside temp: "Ext:"
val extTooltipState = rememberTooltipState(isPersistent = true)
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = { PlainTooltip { Text(climateTooltip) } },
state = extTooltipState
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { scope.launch { extTooltipState.show() } }
) {
Text(
text = stringResource(R.string.temp_ext_label),
style = MaterialTheme.typography.labelMedium,
color = palette.onSurfaceVariant
)
Spacer(modifier = Modifier.width(2.dp))
Icon(
imageVector = Icons.Filled.Thermostat,
contentDescription = stringResource(R.string.outside_temp),
modifier = Modifier.size(14.dp),
tint = palette.onSurfaceVariant
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = status.outsideTemp?.let { UnitFormatter.formatTemperature(it, units) } ?: "--",
style = MaterialTheme.typography.labelMedium,
color = palette.onSurfaceVariant
)
}
}
// Outside temp
Icon(
imageVector = Icons.Filled.Thermostat,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = palette.accent
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = status.outsideTemp?.let { UnitFormatter.formatTemperature(it, units) } ?: "--",
style = MaterialTheme.typography.labelMedium,
color = palette.accent
)
// Inside temp: "Int:" (bold and green if climate is on)
val intTooltipState = rememberTooltipState(isPersistent = true)
val intColor = if (isClimateOn) StatusSuccess else palette.onSurfaceVariant
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = { PlainTooltip { Text(climateTooltip) } },
state = intTooltipState
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { scope.launch { intTooltipState.show() } }
) {
Text(
text = stringResource(R.string.temp_int_label),
style = MaterialTheme.typography.labelMedium,
fontWeight = if (isClimateOn) FontWeight.Bold else FontWeight.Normal,
color = intColor
)
Spacer(modifier = Modifier.width(2.dp))
Icon(
imageVector = Icons.Filled.Thermostat,
contentDescription = stringResource(R.string.inside_temp),
modifier = Modifier.size(14.dp),
tint = intColor
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = status.insideTemp?.let { UnitFormatter.formatTemperature(it, units) } ?: "--",
style = MaterialTheme.typography.labelMedium,
fontWeight = if (isClimateOn) FontWeight.Bold else FontWeight.Normal,
color = intColor
)
}
}
}
}
}

View File

@@ -48,6 +48,30 @@
<!-- Content description when car is plugged in but not charging -->
<string name="plugged_in">Connectat</string>
<!-- Vehicle state shown when driving -->
<string name="driving">Conduint</string>
<!-- Content description when sentry mode is active -->
<string name="sentry_mode_active">Mode sentinella actiu</string>
<!-- Content description for inside temperature indicator -->
<string name="inside_temp">Temperatura interior</string>
<!-- Content description for outside temperature indicator -->
<string name="outside_temp">Temperatura exterior</string>
<!-- Label for external/outside temperature -->
<string name="temp_ext_label">Ext:</string>
<!-- Label for internal/inside temperature -->
<string name="temp_int_label">Int:</string>
<!-- Tooltip shown when climate control is active -->
<string name="climate_active">Clima actiu</string>
<!-- Tooltip shown when climate control is inactive -->
<string name="climate_inactive">Clima inactiu</string>
<!-- Content description for the car image, indicates it's tappable -->
<string name="car_image_tap_for_stats">Imatge del cotxe - toca per estadístiques</string>

View File

@@ -48,6 +48,30 @@
<!-- Content description when car is plugged in but not charging -->
<string name="plugged_in">Conectado</string>
<!-- Vehicle state shown when driving -->
<string name="driving">Conduciendo</string>
<!-- Content description when sentry mode is active -->
<string name="sentry_mode_active">Modo centinela activo</string>
<!-- Content description for inside temperature indicator -->
<string name="inside_temp">Temperatura interior</string>
<!-- Content description for outside temperature indicator -->
<string name="outside_temp">Temperatura exterior</string>
<!-- Label for external/outside temperature -->
<string name="temp_ext_label">Ext:</string>
<!-- Label for internal/inside temperature -->
<string name="temp_int_label">Int:</string>
<!-- Tooltip shown when climate control is active -->
<string name="climate_active">Clima activo</string>
<!-- Tooltip shown when climate control is inactive -->
<string name="climate_inactive">Clima inactivo</string>
<!-- Content description for the car image, indicates it's tappable -->
<string name="car_image_tap_for_stats">Imagen del coche - toca para estadísticas</string>

View File

@@ -48,6 +48,30 @@
<!-- Content description when car is plugged in but not charging -->
<string name="plugged_in">Collegato</string>
<!-- Vehicle state shown when driving -->
<string name="driving">In guida</string>
<!-- Content description when sentry mode is active -->
<string name="sentry_mode_active">Modalità sentinella attiva</string>
<!-- Content description for inside temperature indicator -->
<string name="inside_temp">Temperatura interna</string>
<!-- Content description for outside temperature indicator -->
<string name="outside_temp">Temperatura esterna</string>
<!-- Label for external/outside temperature -->
<string name="temp_ext_label">Est:</string>
<!-- Label for internal/inside temperature -->
<string name="temp_int_label">Int:</string>
<!-- Tooltip shown when climate control is active -->
<string name="climate_active">Clima attivo</string>
<!-- Tooltip shown when climate control is inactive -->
<string name="climate_inactive">Clima inattivo</string>
<!-- Content description for the car image, indicates it's tappable -->
<string name="car_image_tap_for_stats">Immagine auto - tocca per statistiche</string>

View File

@@ -48,6 +48,30 @@
<!-- Content description when car is plugged in but not charging -->
<string name="plugged_in">Plugged in</string>
<!-- Vehicle state shown when driving -->
<string name="driving">Driving</string>
<!-- Content description when sentry mode is active -->
<string name="sentry_mode_active">Sentry mode active</string>
<!-- Content description for inside temperature indicator -->
<string name="inside_temp">Inside temperature</string>
<!-- Content description for outside temperature indicator -->
<string name="outside_temp">Outside temperature</string>
<!-- Label for external/outside temperature -->
<string name="temp_ext_label">Ext:</string>
<!-- Label for internal/inside temperature -->
<string name="temp_int_label">Int:</string>
<!-- Tooltip shown when climate control is active -->
<string name="climate_active">Climate active</string>
<!-- Tooltip shown when climate control is inactive -->
<string name="climate_inactive">Climate inactive</string>
<!-- Content description for the car image, indicates it's tappable -->
<string name="car_image_tap_for_stats">Car image - tap for stats</string>