Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions app/src/main/java/com/matedroid/data/local/SettingsDataStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ data class AppSettings(
val currencyCode: String = "EUR",
val showShortDrivesCharges: Boolean = false,
val teslamateBaseUrl: String = "",
val lastSelectedCarId: Int? = null
val lastSelectedCarId: Int? = null,
val unitsOverride: String = "auto" // "auto" | "metric" | "imperial"
) {
val isConfigured: Boolean
get() = serverUrl.isNotBlank()
Expand All @@ -74,6 +75,7 @@ class SettingsDataStore @Inject constructor(
private val teslamateBaseUrlKey = stringPreferencesKey("teslamate_base_url")
private val lastSelectedCarIdKey = intPreferencesKey("last_selected_car_id")
private val carImageOverridesKey = stringPreferencesKey("car_image_overrides")
private val unitsOverrideKey = stringPreferencesKey("units_override")

val settings: Flow<AppSettings> = context.dataStore.data.map { preferences ->
AppSettings(
Expand All @@ -84,14 +86,19 @@ class SettingsDataStore @Inject constructor(
currencyCode = preferences[currencyCodeKey] ?: "EUR",
showShortDrivesCharges = preferences[showShortDrivesChargesKey] ?: false,
teslamateBaseUrl = preferences[teslamateBaseUrlKey] ?: "",
lastSelectedCarId = preferences[lastSelectedCarIdKey]
lastSelectedCarId = preferences[lastSelectedCarIdKey],
unitsOverride = preferences[unitsOverrideKey] ?: "auto"
)
}

val showShortDrivesCharges: Flow<Boolean> = context.dataStore.data.map { preferences ->
preferences[showShortDrivesChargesKey] ?: false
}

val unitsOverride: Flow<String> = context.dataStore.data.map { preferences ->
preferences[unitsOverrideKey] ?: "auto"
}

/**
* Flow of car image overrides, keyed by car ID.
*/
Expand Down Expand Up @@ -160,6 +167,12 @@ class SettingsDataStore @Inject constructor(
}
}

suspend fun saveUnitsOverride(value: String) {
context.dataStore.edit { preferences ->
preferences[unitsOverrideKey] = value
}
}

suspend fun saveTeslamateBaseUrl(url: String) {
context.dataStore.edit { preferences ->
preferences[teslamateBaseUrlKey] = url
Expand Down
50 changes: 32 additions & 18 deletions app/src/main/java/com/matedroid/ui/screens/battery/BatteryScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ fun BatteryScreen(
BatteryHealthContent(
stats = stats,
palette = palette,
isImperial = uiState.units?.isImperial == true,
onCardClick = { viewModel.showDetail() }
)
} else {
Expand Down Expand Up @@ -171,6 +172,7 @@ fun BatteryScreen(
if (stats != null) {
BatteryDetailScreen(
stats = stats,
isImperial = uiState.units?.isImperial == true,
onClose = { viewModel.hideDetail() }
)
}
Expand All @@ -182,6 +184,7 @@ fun BatteryScreen(
private fun BatteryHealthContent(
stats: BatteryStats,
palette: CarColorPalette,
isImperial: Boolean,
onCardClick: () -> Unit
) {
Column(
Expand All @@ -192,18 +195,18 @@ private fun BatteryHealthContent(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Capacity Section
CapacityCard(stats = stats, palette = palette, onClick = onCardClick)
CapacityCard(stats = stats, palette = palette, isImperial = isImperial, onClick = onCardClick)

// Degradation Section
DegradationCard(stats = stats, palette = palette, onClick = onCardClick)

// Range Section
RangeCard(stats = stats, palette = palette, onClick = onCardClick)
RangeCard(stats = stats, palette = palette, isImperial = isImperial, onClick = onCardClick)
}
}

@Composable
private fun CapacityCard(stats: BatteryStats, palette: CarColorPalette, onClick: () -> Unit) {
private fun CapacityCard(stats: BatteryStats, palette: CarColorPalette, isImperial: Boolean, onClick: () -> Unit) {
var showTooltip by remember { mutableStateOf(false) }
val capacityTitle = stringResource(R.string.battery_capacity_title)
val capacityMessage = stringResource(R.string.battery_capacity_message)
Expand All @@ -213,6 +216,9 @@ private fun CapacityCard(stats: BatteryStats, palette: CarColorPalette, onClick:
val ratedLabel = stringResource(R.string.rated)
val infoLabel = stringResource(R.string.info)
val gotItLabel = stringResource(R.string.got_it)
val effUnit = if (isImperial) "Wh/mi" else "Wh/km"
val eff = if (isImperial) stats.ratedEfficiency * 1.60934 else stats.ratedEfficiency


if (showTooltip) {
InfoDialog(
Expand Down Expand Up @@ -300,7 +306,7 @@ private fun CapacityCard(stats: BatteryStats, palette: CarColorPalette, onClick:
Spacer(modifier = Modifier.width(8.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "%.1f Wh/km".format(stats.ratedEfficiency),
text = "%.1f $effUnit".format(eff),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = palette.onSurface
Expand Down Expand Up @@ -515,11 +521,13 @@ private fun LossValueCard(
}

@Composable
private fun RangeCard(stats: BatteryStats, palette: CarColorPalette, onClick: () -> Unit) {
private fun RangeCard(stats: BatteryStats, palette: CarColorPalette, isImperial: Boolean, onClick: () -> Unit) {
val rangeLabel = stringResource(R.string.range)
val maxRangeNewLabel = stringResource(R.string.max_range_new)
val maxRangeNowLabel = stringResource(R.string.max_range_now)
val rangeLossLabel = stringResource(R.string.range_loss)
val distUnit = if (isImperial) "mi" else "km"
fun fmtRange(v: Double) = "%,.1f $distUnit".format(if (isImperial) v * 0.621371 else v)

Card(
modifier = Modifier
Expand Down Expand Up @@ -548,13 +556,13 @@ private fun RangeCard(stats: BatteryStats, palette: CarColorPalette, onClick: ()
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
RangeValueCard(
value = "%,.1f km".format(stats.maxRangeNew),
value = fmtRange(stats.maxRangeNew),
label = maxRangeNewLabel,
iconColor = CapacityGreen,
modifier = Modifier.weight(1f)
)
RangeValueCard(
value = "%,.1f km".format(stats.maxRangeNow),
value = fmtRange(stats.maxRangeNow),
label = maxRangeNowLabel,
iconColor = CapacityYellow,
modifier = Modifier.weight(1f)
Expand All @@ -578,7 +586,7 @@ private fun RangeCard(stats: BatteryStats, palette: CarColorPalette, onClick: ()
Spacer(modifier = Modifier.width(8.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "%,.1f km".format(stats.rangeLoss),
text = fmtRange(stats.rangeLoss),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = palette.onSurface
Expand Down Expand Up @@ -639,6 +647,7 @@ private fun RangeValueCard(
@Composable
private fun BatteryDetailScreen(
stats: BatteryStats,
isImperial: Boolean,
onClose: () -> Unit
) {
Scaffold(
Expand Down Expand Up @@ -668,26 +677,28 @@ private fun BatteryDetailScreen(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Battery Status Section
BatteryStatusCard(stats = stats)
BatteryStatusCard(stats = stats, isImperial = isImperial)

// Range Information Section
RangeInformationCard(stats = stats)
RangeInformationCard(stats = stats, isImperial = isImperial)

// Estimated Total Capacity Section
EstimatedCapacityCard(stats = stats)
EstimatedCapacityCard(stats = stats, isImperial = isImperial)
}
}
}

@Composable
private fun BatteryStatusCard(stats: BatteryStats) {
private fun BatteryStatusCard(stats: BatteryStats, isImperial: Boolean) {
var showTooltip by remember { mutableStateOf(false) }
val currentChargeTitle = stringResource(R.string.current_charge_title)
val currentChargeMessage = stringResource(R.string.current_charge_message)
val currentChargeLabel = stringResource(R.string.current_charge)
val usablePercentLabel = stringResource(R.string.usable_percent, stats.usableBatteryLevel)
val infoLabel = stringResource(R.string.info)
val gotItLabel = stringResource(R.string.got_it)
val distUnit = if (isImperial) "mi" else "km"
fun fmtRange(v: Double) = "%,.1f $distUnit".format(if (isImperial) v * 0.621371 else v)

if (showTooltip) {
InfoDialog(
Expand Down Expand Up @@ -781,7 +792,7 @@ private fun BatteryStatusCard(stats: BatteryStats) {
}

@Composable
private fun RangeInformationCard(stats: BatteryStats) {
private fun RangeInformationCard(stats: BatteryStats, isImperial: Boolean) {
var showTooltip by remember { mutableStateOf(false) }
val rangeInfoTitle = stringResource(R.string.range_information_title)
val rangeInfoMessage = stringResource(R.string.range_information_message)
Expand All @@ -794,6 +805,8 @@ private fun RangeInformationCard(stats: BatteryStats) {
val idealRangeSubtitle = stringResource(R.string.ideal_range_subtitle)
val infoLabel = stringResource(R.string.info)
val gotItLabel = stringResource(R.string.got_it)
val distUnit = if (isImperial) "mi" else "km"
fun fmtRange(v: Double) = "%,.1f $distUnit".format(if (isImperial) v * 0.621371 else v)

if (showTooltip) {
InfoDialog(
Expand Down Expand Up @@ -839,7 +852,7 @@ private fun RangeInformationCard(stats: BatteryStats) {
RangeInfoRow(
title = estimatedRangeLabel,
subtitle = estimatedRangeSubtitle,
value = "%,.1f km".format(stats.estimatedRange),
value = fmtRange(stats.estimatedRange),
valueColor = RangeBlue
)

Expand All @@ -848,7 +861,7 @@ private fun RangeInformationCard(stats: BatteryStats) {
RangeInfoRow(
title = ratedRangeLabel,
subtitle = ratedRangeSubtitle,
value = "%,.1f km".format(stats.ratedRange),
value = fmtRange(stats.ratedRange),
valueColor = RangeBlue
)

Expand All @@ -857,7 +870,7 @@ private fun RangeInformationCard(stats: BatteryStats) {
RangeInfoRow(
title = idealRangeLabel,
subtitle = idealRangeSubtitle,
value = "%,.1f km".format(stats.idealRange),
value = fmtRange(stats.idealRange),
valueColor = RangeBlue
)
}
Expand Down Expand Up @@ -914,7 +927,7 @@ private fun RangeInfoRow(
}

@Composable
private fun EstimatedCapacityCard(stats: BatteryStats) {
private fun EstimatedCapacityCard(stats: BatteryStats, isImperial: Boolean) {
var showTooltip by remember { mutableStateOf(false) }
val estimatedCapacityTitle = stringResource(R.string.estimated_total_capacity_title)
val estimatedCapacityMessage = stringResource(R.string.estimated_total_capacity_message, stats.batteryLevel)
Expand All @@ -923,6 +936,7 @@ private fun EstimatedCapacityCard(stats: BatteryStats) {
val estimatedRangeDescription = stringResource(R.string.estimated_range_description, stats.batteryLevel, stats.ratedRange)
val infoLabel = stringResource(R.string.info)
val gotItLabel = stringResource(R.string.got_it)
val distUnit = if (isImperial) "mi" else "km"

if (showTooltip) {
InfoDialog(
Expand Down Expand Up @@ -991,7 +1005,7 @@ private fun EstimatedCapacityCard(stats: BatteryStats) {
}

Text(
text = "%,.1f km".format(stats.rangeAt100),
text = "%,.1f $distUnit".format(if (isImperial) stats.rangeAt100 * 0.621371 else stats.rangeAt100),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = StatusSuccess
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import androidx.lifecycle.viewModelScope
import com.matedroid.data.api.models.BatteryHealth
import com.matedroid.data.api.models.CarStatus
import com.matedroid.data.api.models.Units
import com.matedroid.data.local.SettingsDataStore
import com.matedroid.data.repository.ApiResult
import com.matedroid.data.repository.TeslamateRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
Expand Down Expand Up @@ -49,9 +51,20 @@ data class BatteryStats(

@HiltViewModel
class BatteryViewModel @Inject constructor(
private val repository: TeslamateRepository
private val repository: TeslamateRepository,
private val settingsDataStore: SettingsDataStore
) : ViewModel() {

// Helper
private suspend fun resolveUnits(apiUnits: Units?): Units? {
val override = settingsDataStore.unitsOverride.first()
return when (override) {
"imperial" -> apiUnits?.copy(unitOfLength = "mi")
"metric" -> apiUnits?.copy(unitOfLength = "km")
else -> apiUnits
}
}

private val _uiState = MutableStateFlow(BatteryUiState())
val uiState: StateFlow<BatteryUiState> = _uiState.asStateFlow()

Expand Down Expand Up @@ -107,7 +120,7 @@ class BatteryViewModel @Inject constructor(
isRefreshing = false,
batteryHealth = healthResult.data,
carStatus = statusResult.data.status,
units = statusResult.data.units,
units = resolveUnits(statusResult.data.units),
error = null
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ class DashboardViewModel @Inject constructor(
private val settingsDataStore: SettingsDataStore
) : ViewModel() {

private suspend fun resolveUnits(apiUnits: Units?): Units? {
val override = settingsDataStore.unitsOverride.first()
return when (override) {
"imperial" -> apiUnits?.copy(unitOfLength = "mi")
"metric" -> apiUnits?.copy(unitOfLength = "km")
else -> apiUnits
}
}

private val _uiState = MutableStateFlow(DashboardUiState())
val uiState: StateFlow<DashboardUiState> = _uiState.asStateFlow()

Expand Down Expand Up @@ -178,7 +187,7 @@ class DashboardViewModel @Inject constructor(
_uiState.update {
it.copy(
carStatus = status,
units = result.data.units,
units = resolveUnits(result.data.units),
error = null
)
}
Expand All @@ -201,7 +210,7 @@ class DashboardViewModel @Inject constructor(
_uiState.update {
it.copy(
carStatus = status,
units = result.data.units,
units = resolveUnits(result.data.units),
error = null
)
}
Expand Down Expand Up @@ -269,7 +278,7 @@ class DashboardViewModel @Inject constructor(
_uiState.update {
it.copy(
carStatus = status,
units = result.data.units
units = resolveUnits(result.data.units)
)
}
// Update address if location changed
Expand Down
Loading
Loading