mirror of
https://github.com/vide/matedroid.git
synced 2026-01-20 00:03:17 +08:00
feat: add Software Versions screen with update history
- Add SoftwareVersionsScreen showing historic software updates - Each version shows install date and duration installed - Trophy badge highlights the longest-installed version - Date filter chips: Last 6 months, Last year, All time - Navigate by tapping Software item in Vehicle Info card - Clickable InfoItem with visual feedback (primary color) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
203bacf8dd
commit
ea80b16623
@@ -18,6 +18,7 @@ import com.matedroid.ui.screens.drives.DriveDetailScreen
|
||||
import com.matedroid.ui.screens.drives.DrivesScreen
|
||||
import com.matedroid.ui.screens.mileage.MileageScreen
|
||||
import com.matedroid.ui.screens.settings.SettingsScreen
|
||||
import com.matedroid.ui.screens.updates.SoftwareVersionsScreen
|
||||
|
||||
sealed class Screen(val route: String) {
|
||||
data object Settings : Screen("settings")
|
||||
@@ -79,8 +80,14 @@ sealed class Screen(val route: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
data object Updates : Screen("updates/{carId}") {
|
||||
fun createRoute(carId: Int) = "updates/$carId"
|
||||
data object Updates : Screen("updates/{carId}?exteriorColor={exteriorColor}") {
|
||||
fun createRoute(carId: Int, exteriorColor: String? = null): String {
|
||||
return if (exteriorColor != null) {
|
||||
"updates/$carId?exteriorColor=$exteriorColor"
|
||||
} else {
|
||||
"updates/$carId"
|
||||
}
|
||||
}
|
||||
}
|
||||
data object PalettePreview : Screen("palette_preview")
|
||||
}
|
||||
@@ -129,6 +136,9 @@ fun NavGraph(
|
||||
},
|
||||
onNavigateToMileage = { carId, exteriorColor ->
|
||||
navController.navigate(Screen.Mileage.createRoute(carId, exteriorColor))
|
||||
},
|
||||
onNavigateToUpdates = { carId, exteriorColor ->
|
||||
navController.navigate(Screen.Updates.createRoute(carId, exteriorColor))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -272,6 +282,26 @@ fun NavGraph(
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = Screen.Updates.route,
|
||||
arguments = listOf(
|
||||
navArgument("carId") { type = NavType.IntType },
|
||||
navArgument("exteriorColor") {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
defaultValue = null
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val carId = backStackEntry.arguments?.getInt("carId") ?: return@composable
|
||||
val exteriorColor = backStackEntry.arguments?.getString("exteriorColor")
|
||||
SoftwareVersionsScreen(
|
||||
carId = carId,
|
||||
exteriorColor = exteriorColor,
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(Screen.PalettePreview.route) {
|
||||
PalettePreviewScreen(
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
|
||||
@@ -107,6 +107,7 @@ fun DashboardScreen(
|
||||
onNavigateToDrives: (carId: Int, exteriorColor: String?) -> Unit = { _, _ -> },
|
||||
onNavigateToBattery: (carId: Int, efficiency: Double?, exteriorColor: String?) -> Unit = { _, _, _ -> },
|
||||
onNavigateToMileage: (carId: Int, exteriorColor: String?) -> Unit = { _, _ -> },
|
||||
onNavigateToUpdates: (carId: Int, exteriorColor: String?) -> Unit = { _, _ -> },
|
||||
viewModel: DashboardViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
@@ -170,6 +171,11 @@ fun DashboardScreen(
|
||||
uiState.selectedCarId?.let { carId ->
|
||||
onNavigateToMileage(carId, uiState.selectedCarExterior?.exteriorColor)
|
||||
}
|
||||
},
|
||||
onNavigateToUpdates = {
|
||||
uiState.selectedCarId?.let { carId ->
|
||||
onNavigateToUpdates(carId, uiState.selectedCarExterior?.exteriorColor)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -261,7 +267,8 @@ private fun DashboardContent(
|
||||
onNavigateToCharges: () -> Unit = {},
|
||||
onNavigateToDrives: () -> Unit = {},
|
||||
onNavigateToBattery: () -> Unit = {},
|
||||
onNavigateToMileage: () -> Unit = {}
|
||||
onNavigateToMileage: () -> Unit = {},
|
||||
onNavigateToUpdates: () -> Unit = {}
|
||||
) {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val palette = CarColorPalettes.forExteriorColor(carExterior?.exteriorColor, isDarkTheme)
|
||||
@@ -292,7 +299,11 @@ private fun DashboardContent(
|
||||
}
|
||||
|
||||
// Vehicle Info Section
|
||||
VehicleInfoCard(status, units)
|
||||
VehicleInfoCard(
|
||||
status = status,
|
||||
units = units,
|
||||
onNavigateToUpdates = onNavigateToUpdates
|
||||
)
|
||||
}
|
||||
|
||||
// Fixed bottom quick links
|
||||
@@ -806,7 +817,11 @@ private fun SmallLocationMap(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VehicleInfoCard(status: CarStatus, units: Units?) {
|
||||
private fun VehicleInfoCard(
|
||||
status: CarStatus,
|
||||
units: Units?,
|
||||
onNavigateToUpdates: () -> Unit = {}
|
||||
) {
|
||||
val distanceUnit = UnitFormatter.getDistanceUnit(units)
|
||||
val pressureUnit = UnitFormatter.getPressureUnit(units)
|
||||
val tpms = status.tpmsDetails
|
||||
@@ -848,7 +863,8 @@ private fun VehicleInfoCard(status: CarStatus, units: Units?) {
|
||||
InfoItem(
|
||||
label = "Software",
|
||||
value = status.version ?: "--",
|
||||
icon = Icons.Filled.Settings
|
||||
icon = Icons.Filled.Settings,
|
||||
onClick = onNavigateToUpdates
|
||||
)
|
||||
}
|
||||
|
||||
@@ -958,24 +974,38 @@ private fun TirePressureItem(
|
||||
private fun InfoItem(
|
||||
label: String,
|
||||
value: String,
|
||||
icon: ImageVector
|
||||
icon: ImageVector,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
val modifier = if (onClick != null) {
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.padding(8.dp)
|
||||
} else {
|
||||
Modifier.padding(8.dp)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
tint = if (onClick != null) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = if (onClick != null) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (onClick != null) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
package com.matedroid.ui.screens.updates
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.EmojiEvents
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material.icons.filled.SystemUpdate
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.matedroid.ui.theme.CarColorPalette
|
||||
import com.matedroid.ui.theme.CarColorPalettes
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
enum class UpdatesDateFilter(val label: String, val months: Int?) {
|
||||
LAST_6_MONTHS("Last 6 months", 6),
|
||||
LAST_YEAR("Last year", 12),
|
||||
ALL_TIME("All time", null)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SoftwareVersionsScreen(
|
||||
carId: Int,
|
||||
exteriorColor: String? = null,
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: SoftwareVersionsViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
var selectedFilter by remember { mutableStateOf(UpdatesDateFilter.ALL_TIME) }
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val palette = CarColorPalettes.forExteriorColor(exteriorColor, isDarkTheme)
|
||||
|
||||
LaunchedEffect(carId) {
|
||||
viewModel.setCarId(carId)
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.error) {
|
||||
uiState.error?.let { error ->
|
||||
snackbarHostState.showSnackbar(error)
|
||||
viewModel.clearError()
|
||||
}
|
||||
}
|
||||
|
||||
fun applyDateFilter(filter: UpdatesDateFilter) {
|
||||
selectedFilter = filter
|
||||
viewModel.setFilter(filter.months)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Software Versions") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { padding ->
|
||||
PullToRefreshBox(
|
||||
isRefreshing = uiState.isRefreshing,
|
||||
onRefresh = { viewModel.refresh() },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
if (uiState.isLoading && !uiState.isRefreshing) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
SoftwareVersionsContent(
|
||||
updates = uiState.updates,
|
||||
longestInstalledId = uiState.longestInstalledId,
|
||||
selectedFilter = selectedFilter,
|
||||
palette = palette,
|
||||
onFilterSelected = { applyDateFilter(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SoftwareVersionsContent(
|
||||
updates: List<SoftwareVersionItem>,
|
||||
longestInstalledId: Int?,
|
||||
selectedFilter: UpdatesDateFilter,
|
||||
palette: CarColorPalette,
|
||||
onFilterSelected: (UpdatesDateFilter) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
DateFilterChips(
|
||||
selectedFilter = selectedFilter,
|
||||
palette = palette,
|
||||
onFilterSelected = onFilterSelected
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Update History",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
if (updates.isEmpty()) {
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No software updates found",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(updates, key = { it.id }) { update ->
|
||||
SoftwareVersionCard(
|
||||
update = update,
|
||||
isLongestInstalled = update.id == longestInstalledId,
|
||||
palette = palette
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DateFilterChips(
|
||||
selectedFilter: UpdatesDateFilter,
|
||||
palette: CarColorPalette,
|
||||
onFilterSelected: (UpdatesDateFilter) -> Unit
|
||||
) {
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(UpdatesDateFilter.entries.toList()) { filter ->
|
||||
FilterChip(
|
||||
selected = filter == selectedFilter,
|
||||
onClick = { onFilterSelected(filter) },
|
||||
label = { Text(filter.label) },
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = palette.surface,
|
||||
selectedLabelColor = palette.onSurface
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SoftwareVersionCard(
|
||||
update: SoftwareVersionItem,
|
||||
isLongestInstalled: Boolean,
|
||||
palette: CarColorPalette
|
||||
) {
|
||||
val dateFormatter = DateTimeFormatter.ofPattern("MMM d, yyyy")
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (update.isCurrent) {
|
||||
palette.surface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SystemUpdate,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = if (update.isCurrent) palette.accent else MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = update.version,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (update.isCurrent) palette.onSurface else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (update.isCurrent) {
|
||||
Text(
|
||||
text = "Current version",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = palette.accent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isLongestInstalled) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.EmojiEvents,
|
||||
contentDescription = "Longest installed",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "Longest",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// Install date
|
||||
Column {
|
||||
Text(
|
||||
text = "Installed",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = if (update.isCurrent) palette.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = update.installDate?.format(dateFormatter) ?: "Unknown",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (update.isCurrent) palette.onSurface else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
// Duration
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Schedule,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = if (update.isCurrent) palette.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "Duration",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = if (update.isCurrent) palette.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = formatDuration(update.durationDays),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (update.isCurrent) palette.onSurface else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDuration(days: Long?): String {
|
||||
if (days == null) return "Unknown"
|
||||
|
||||
return when {
|
||||
days == 0L -> "< 1 day"
|
||||
days == 1L -> "1 day"
|
||||
days < 30 -> "$days days"
|
||||
days < 60 -> {
|
||||
val weeks = days / 7
|
||||
if (weeks == 1L) "1 week" else "$weeks weeks"
|
||||
}
|
||||
days < 365 -> {
|
||||
val months = days / 30
|
||||
if (months == 1L) "~1 month" else "~$months months"
|
||||
}
|
||||
else -> {
|
||||
val years = days / 365
|
||||
val remainingMonths = (days % 365) / 30
|
||||
if (remainingMonths > 0) {
|
||||
if (years == 1L) "~1 year $remainingMonths mo" else "~$years years $remainingMonths mo"
|
||||
} else {
|
||||
if (years == 1L) "~1 year" else "~$years years"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.matedroid.ui.screens.updates
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.matedroid.data.api.models.UpdateData
|
||||
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.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
|
||||
data class SoftwareVersionsUiState(
|
||||
val isLoading: Boolean = true,
|
||||
val isRefreshing: Boolean = false,
|
||||
val updates: List<SoftwareVersionItem> = emptyList(),
|
||||
val longestInstalledId: Int? = null,
|
||||
val error: String? = null,
|
||||
val filterMonths: Int? = null // null = All time, 6 = Last 6 months, 12 = Last year
|
||||
)
|
||||
|
||||
data class SoftwareVersionItem(
|
||||
val id: Int,
|
||||
val version: String,
|
||||
val installDate: LocalDateTime?,
|
||||
val endDate: LocalDateTime?,
|
||||
val durationDays: Long?,
|
||||
val isCurrent: Boolean
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class SoftwareVersionsViewModel @Inject constructor(
|
||||
private val repository: TeslamateRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(SoftwareVersionsUiState())
|
||||
val uiState: StateFlow<SoftwareVersionsUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var carId: Int? = null
|
||||
private var allUpdates: List<UpdateData> = emptyList()
|
||||
|
||||
fun setCarId(id: Int) {
|
||||
if (carId != id) {
|
||||
carId = id
|
||||
loadUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
fun setFilter(months: Int?) {
|
||||
_uiState.update { it.copy(filterMonths = months) }
|
||||
applyFilter(months)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
carId?.let {
|
||||
_uiState.update { it.copy(isRefreshing = true) }
|
||||
loadUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_uiState.update { it.copy(error = null) }
|
||||
}
|
||||
|
||||
private fun loadUpdates() {
|
||||
val id = carId ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
val state = _uiState.value
|
||||
if (!state.isRefreshing) {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
}
|
||||
|
||||
when (val result = repository.getUpdates(id)) {
|
||||
is ApiResult.Success -> {
|
||||
allUpdates = result.data
|
||||
applyFilter(_uiState.value.filterMonths)
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
isRefreshing = false,
|
||||
error = result.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyFilter(months: Int?) {
|
||||
val cutoffDate = months?.let {
|
||||
LocalDate.now().minusMonths(it.toLong())
|
||||
}
|
||||
|
||||
val filteredUpdates = if (cutoffDate != null) {
|
||||
allUpdates.filter { update ->
|
||||
val startDate = parseDate(update.startDate)
|
||||
startDate == null || startDate.toLocalDate() >= cutoffDate
|
||||
}
|
||||
} else {
|
||||
allUpdates
|
||||
}
|
||||
|
||||
val items = filteredUpdates.mapIndexed { index, update ->
|
||||
val startDate = parseDate(update.startDate)
|
||||
val endDate = parseDate(update.endDate)
|
||||
val isCurrent = index == 0 && endDate == null
|
||||
|
||||
val durationDays = when {
|
||||
startDate == null -> null
|
||||
endDate != null -> Duration.between(startDate, endDate).toDays()
|
||||
isCurrent -> Duration.between(startDate, LocalDateTime.now()).toDays()
|
||||
else -> null
|
||||
}
|
||||
|
||||
SoftwareVersionItem(
|
||||
id = update.id ?: 0,
|
||||
version = update.version ?: "Unknown",
|
||||
installDate = startDate,
|
||||
endDate = endDate,
|
||||
durationDays = durationDays,
|
||||
isCurrent = isCurrent
|
||||
)
|
||||
}
|
||||
|
||||
// Find the version that stayed installed the longest (among displayed ones)
|
||||
val longestId = items
|
||||
.filter { it.durationDays != null }
|
||||
.maxByOrNull { it.durationDays!! }
|
||||
?.id
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
isRefreshing = false,
|
||||
updates = items,
|
||||
longestInstalledId = longestId,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(dateStr: String?): LocalDateTime? {
|
||||
if (dateStr == null) return null
|
||||
return try {
|
||||
LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_DATE_TIME)
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
LocalDateTime.parse(dateStr.replace(" ", "T"), DateTimeFormatter.ISO_DATE_TIME)
|
||||
} catch (e2: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user