style(car): align visual patterns with main app design system

- Rename SignalQuality enum to match core: POOR→BAD, UNKNOWN→NONE
- Add last heard time to node list subtitles (was only in detail view)
- Add message timestamps in conversation view
- Align string resources with core terminology (bad/none vs poor/unknown)
- Use DateFormatter.formatRelativeTime() consistently across all screens

Ensures car experience uses consistent terminology and information
density with the main Meshtastic app.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-21 20:29:43 -05:00
parent d5097f57de
commit 1d4b3be493
7 changed files with 36 additions and 15 deletions

View File

@@ -61,8 +61,8 @@ enum class SignalQuality {
EXCELLENT,
GOOD,
FAIR,
POOR,
UNKNOWN,
BAD,
NONE,
}
data class TopologyHeader(val totalNodes: Int, val onlineNodes: Int, val meshName: String?)

View File

@@ -25,6 +25,7 @@ import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.feature.car.R
import org.meshtastic.feature.car.service.MessageSnapshot
@@ -42,7 +43,13 @@ class ConversationScreen(
val listBuilder = ItemList.Builder()
messages.forEach { msg ->
listBuilder.addItem(Row.Builder().setTitle(msg.senderName).addText(msg.text).build())
val timeText =
if (msg.timestamp != 0L) {
"${DateFormatter.formatRelativeTime(msg.timestamp)}"
} else {
""
}
listBuilder.addItem(Row.Builder().setTitle(msg.senderName).addText("${msg.text}$timeText").build())
}
val actionStrip =

View File

@@ -39,6 +39,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.feature.car.R
import org.meshtastic.feature.car.model.NodeUi
@@ -197,12 +198,18 @@ class HomeScreen(carContext: CarContext, private val stateCoordinator: CarStateC
SignalQuality.EXCELLENT -> carContext.getString(R.string.car_signal_excellent)
SignalQuality.GOOD -> carContext.getString(R.string.car_signal_good)
SignalQuality.FAIR -> carContext.getString(R.string.car_signal_fair)
SignalQuality.POOR -> carContext.getString(R.string.car_signal_poor)
SignalQuality.UNKNOWN -> carContext.getString(R.string.car_signal_unknown)
SignalQuality.BAD -> carContext.getString(R.string.car_signal_bad)
SignalQuality.NONE -> carContext.getString(R.string.car_signal_none)
}
val battery = node.batteryPercent?.let { "$it%" } ?: ""
val lastHeard =
if (node.lastHeard != 0L) {
"${DateFormatter.formatRelativeTime(node.lastHeard)}"
} else {
""
}
val status = if (!node.isOnline) "${carContext.getString(R.string.car_status_offline)}" else ""
return "$signal$battery$status"
return "$signal$battery$lastHeard$status"
}
private fun buildDisconnectedTemplate(): Template = PaneTemplate.Builder(

View File

@@ -26,6 +26,7 @@ import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.feature.car.R
import org.meshtastic.feature.car.model.NodeDashboardUiState
import org.meshtastic.feature.car.model.NodeUi
@@ -86,11 +87,17 @@ class NodeDashboardScreen(
SignalQuality.EXCELLENT -> carContext.getString(R.string.car_signal_excellent)
SignalQuality.GOOD -> carContext.getString(R.string.car_signal_good)
SignalQuality.FAIR -> carContext.getString(R.string.car_signal_fair)
SignalQuality.POOR -> carContext.getString(R.string.car_signal_poor)
SignalQuality.UNKNOWN -> carContext.getString(R.string.car_signal_unknown)
SignalQuality.BAD -> carContext.getString(R.string.car_signal_bad)
SignalQuality.NONE -> carContext.getString(R.string.car_signal_none)
}
val battery = node.batteryPercent?.let { "$it%" } ?: ""
val lastHeard =
if (node.lastHeard != 0L) {
"${DateFormatter.formatRelativeTime(node.lastHeard)}"
} else {
""
}
val status = if (!node.isOnline) "${carContext.getString(R.string.car_status_offline)}" else ""
return "$signal$battery$status"
return "$signal$battery$lastHeard$status"
}
}

View File

@@ -102,8 +102,8 @@ class NodeDetailScreen(
SignalQuality.EXCELLENT -> carContext.getString(R.string.car_signal_excellent)
SignalQuality.GOOD -> carContext.getString(R.string.car_signal_good)
SignalQuality.FAIR -> carContext.getString(R.string.car_signal_fair)
SignalQuality.POOR -> carContext.getString(R.string.car_signal_poor)
SignalQuality.UNKNOWN -> carContext.getString(R.string.car_signal_unknown)
SignalQuality.BAD -> carContext.getString(R.string.car_signal_bad)
SignalQuality.NONE -> carContext.getString(R.string.car_signal_none)
}
private fun formatLastHeard(epochMillis: Long): String {

View File

@@ -288,12 +288,12 @@ class CarStateCoordinator(
@Suppress("MagicNumber")
private fun determineSignalQuality(snr: Float, rssi: Int): SignalQuality = when {
snr == Float.MAX_VALUE || rssi == Int.MAX_VALUE -> SignalQuality.UNKNOWN
snr == Float.MAX_VALUE || rssi == Int.MAX_VALUE -> SignalQuality.NONE
snr > SNR_GOOD_THRESHOLD && rssi > RSSI_GOOD_THRESHOLD -> SignalQuality.EXCELLENT
snr > SNR_GOOD_THRESHOLD && rssi > RSSI_FAIR_THRESHOLD -> SignalQuality.GOOD
snr > SNR_FAIR_THRESHOLD && rssi > RSSI_GOOD_THRESHOLD -> SignalQuality.GOOD
snr > SNR_FAIR_THRESHOLD -> SignalQuality.FAIR
else -> SignalQuality.POOR
else -> SignalQuality.BAD
}
}
}

View File

@@ -23,11 +23,11 @@
<string name="car_reconnecting">Radio connection lost. Will reconnect automatically.</string>
<string name="car_reconnecting_title">Reconnecting…</string>
<string name="car_reconnecting_body">The app will automatically reconnect when the radio is available.</string>
<string name="car_signal_bad">Bad</string>
<string name="car_signal_excellent">Excellent</string>
<string name="car_signal_fair">Fair</string>
<string name="car_signal_good">Good</string>
<string name="car_signal_poor">Poor</string>
<string name="car_signal_unknown">Unknown</string>
<string name="car_signal_none">None</string>
<string name="car_status_battery">Battery</string>
<string name="car_status_last_heard">Last Heard</string>
<string name="car_status_offline">Offline</string>