diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c22a85..6865da5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Software Versions**: Tap the external link icon next to any version to view release notes on NotATeslaApp +- **Drives**: Filter drives by distance - Commute (< 10 km), Day trip (10-100 km), Road trip (> 100 km). Labels adapt to metric/imperial units. ### Changed - **Mileage**: Round all distance values to whole numbers for cleaner display (Total, Avg/Year, year cards, month cards) diff --git a/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt b/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt index b54887a..6210019 100644 --- a/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/drives/DrivesScreen.kt @@ -58,6 +58,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.matedroid.data.api.models.DriveData +import com.matedroid.data.api.models.Units import com.matedroid.ui.components.BarChartData import com.matedroid.ui.components.InteractiveBarChart import com.matedroid.ui.theme.CarColorPalette @@ -154,9 +155,12 @@ fun DrivesScreen( chartData = uiState.chartData, chartGranularity = uiState.chartGranularity, summary = uiState.summary, - selectedFilter = selectedFilter, + selectedDateFilter = selectedFilter, + selectedDistanceFilter = uiState.distanceFilter, + units = uiState.units, palette = palette, - onFilterSelected = { applyDateFilter(it) }, + onDateFilterSelected = { applyDateFilter(it) }, + onDistanceFilterSelected = { viewModel.setDistanceFilter(it) }, onDriveClick = onNavigateToDriveDetail ) } @@ -171,9 +175,12 @@ private fun DrivesContent( chartData: List, chartGranularity: DriveChartGranularity, summary: DrivesSummary, - selectedFilter: DriveDateFilter, + selectedDateFilter: DriveDateFilter, + selectedDistanceFilter: DriveDistanceFilter, + units: Units?, palette: CarColorPalette, - onFilterSelected: (DriveDateFilter) -> Unit, + onDateFilterSelected: (DriveDateFilter) -> Unit, + onDistanceFilterSelected: (DriveDistanceFilter) -> Unit, onDriveClick: (driveId: Int) -> Unit ) { LazyColumn( @@ -183,9 +190,18 @@ private fun DrivesContent( ) { item { DateFilterChips( - selectedFilter = selectedFilter, + selectedFilter = selectedDateFilter, palette = palette, - onFilterSelected = onFilterSelected + onFilterSelected = onDateFilterSelected + ) + } + + item { + DistanceFilterChips( + selectedFilter = selectedDistanceFilter, + units = units, + palette = palette, + onFilterSelected = onDistanceFilterSelected ) } @@ -266,6 +282,31 @@ private fun DateFilterChips( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DistanceFilterChips( + selectedFilter: DriveDistanceFilter, + units: Units?, + palette: CarColorPalette, + onFilterSelected: (DriveDistanceFilter) -> Unit +) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(DriveDistanceFilter.entries.toList()) { filter -> + FilterChip( + selected = filter == selectedFilter, + onClick = { onFilterSelected(filter) }, + label = { Text(filter.getLabel(units)) }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = palette.surface, + selectedLabelColor = palette.onSurface + ) + ) + } + } +} + @Composable private fun SummaryCard(summary: DrivesSummary, palette: CarColorPalette) { Card( diff --git a/app/src/main/java/com/matedroid/ui/screens/drives/DrivesViewModel.kt b/app/src/main/java/com/matedroid/ui/screens/drives/DrivesViewModel.kt index 723e7dc..bcd80e5 100644 --- a/app/src/main/java/com/matedroid/ui/screens/drives/DrivesViewModel.kt +++ b/app/src/main/java/com/matedroid/ui/screens/drives/DrivesViewModel.kt @@ -3,6 +3,7 @@ package com.matedroid.ui.screens.drives import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.matedroid.data.api.models.DriveData +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 @@ -25,6 +26,26 @@ enum class DriveChartGranularity { DAILY, WEEKLY, MONTHLY } +enum class DriveDistanceFilter( + val maxDistanceKm: Double?, + val minDistanceKm: Double? +) { + ALL(null, null), + COMMUTE(10.0, null), // < 10 km / 6 mi + DAY_TRIP(100.0, 10.0), // 10-100 km / 6-60 mi + ROAD_TRIP(null, 100.0); // > 100 km / 60 mi + + fun getLabel(units: Units?): String { + val isImperial = units?.isImperial == true + return when (this) { + ALL -> "All" + COMMUTE -> if (isImperial) "Commute (< 6 mi)" else "Commute (< 10 km)" + DAY_TRIP -> if (isImperial) "Day trip (6-60 mi)" else "Day trip (10-100 km)" + ROAD_TRIP -> if (isImperial) "Road trip (> 60 mi)" else "Road trip (> 100 km)" + } + } +} + data class DriveChartData( val label: String, val count: Int, @@ -41,7 +62,9 @@ data class DrivesUiState( val error: String? = null, val startDate: LocalDate? = null, val endDate: LocalDate? = null, - val summary: DrivesSummary = DrivesSummary() + val summary: DrivesSummary = DrivesSummary(), + val units: Units? = null, + val distanceFilter: DriveDistanceFilter = DriveDistanceFilter.ALL ) data class DrivesSummary( @@ -64,6 +87,7 @@ class DrivesViewModel @Inject constructor( private var carId: Int? = null private var showShortDrivesCharges: Boolean = false + private var allDrives: List = emptyList() companion object { private const val MIN_DURATION_MINUTES = 1 @@ -72,6 +96,7 @@ class DrivesViewModel @Inject constructor( fun setCarId(id: Int) { carId = id + loadUnits(id) } fun setDateFilter(startDate: LocalDate?, endDate: LocalDate?) { @@ -84,6 +109,11 @@ class DrivesViewModel @Inject constructor( loadDrives(null, null) } + fun setDistanceFilter(filter: DriveDistanceFilter) { + _uiState.update { it.copy(distanceFilter = filter) } + applyFiltersAndUpdateState() + } + fun refresh() { carId?.let { _uiState.update { it.copy(isRefreshing = true) } @@ -96,6 +126,17 @@ class DrivesViewModel @Inject constructor( _uiState.update { it.copy(error = null) } } + private fun loadUnits(carId: Int) { + viewModelScope.launch { + when (val result = repository.getCarStatus(carId)) { + is ApiResult.Success -> { + _uiState.update { it.copy(units = result.data.units) } + } + is ApiResult.Error -> { /* ignore, units will default to metric */ } + } + } + } + private fun loadDrives(startDate: LocalDate? = null, endDate: LocalDate? = null) { val id = carId ?: return @@ -114,32 +155,22 @@ class DrivesViewModel @Inject constructor( when (val result = repository.getDrives(id, startDateStr, endDateStr)) { is ApiResult.Success -> { - val allDrives = result.data + allDrives = result.data // Calculate summary and chart from ALL drives (including short ones) val summary = calculateSummary(allDrives) val granularity = determineGranularity(startDate, endDate) val chartData = calculateChartData(allDrives, granularity) - // Filter drives for display based on setting - // Hide drives under 1 minute OR with distance < 0.1 km - val displayedDrives = if (showShortDrivesCharges) { - allDrives - } else { - allDrives.filter { drive -> - (drive.durationMin ?: 0) >= MIN_DURATION_MINUTES && - (drive.distance ?: 0.0) >= MIN_DISTANCE_KM - } - } + _uiState.update { it.copy( - isLoading = false, - isRefreshing = false, - drives = displayedDrives, chartData = chartData, chartGranularity = granularity, summary = summary, error = null ) } + + applyFiltersAndUpdateState() } is ApiResult.Error -> { _uiState.update { @@ -154,6 +185,37 @@ class DrivesViewModel @Inject constructor( } } + private fun applyFiltersAndUpdateState() { + val state = _uiState.value + val distanceFilter = state.distanceFilter + + // First apply short drives filter + var filteredDrives = if (showShortDrivesCharges) { + allDrives + } else { + allDrives.filter { drive -> + (drive.durationMin ?: 0) >= MIN_DURATION_MINUTES && + (drive.distance ?: 0.0) >= MIN_DISTANCE_KM + } + } + + // Then apply distance filter + filteredDrives = filteredDrives.filter { drive -> + val distance = drive.distance ?: 0.0 + val minOk = distanceFilter.minDistanceKm?.let { distance >= it } ?: true + val maxOk = distanceFilter.maxDistanceKm?.let { distance < it } ?: true + minOk && maxOk + } + + _uiState.update { + it.copy( + isLoading = false, + isRefreshing = false, + drives = filteredDrives + ) + } + } + private fun determineGranularity(startDate: LocalDate?, endDate: LocalDate?): DriveChartGranularity { if (startDate == null || endDate == null) return DriveChartGranularity.MONTHLY val days = ChronoUnit.DAYS.between(startDate, endDate)