mirror of
https://github.com/vide/matedroid.git
synced 2026-01-20 00:03:17 +08:00
feat(stats): add "Longest Range" record (max distance between charges) (#41)
* chore: bump minSdk from API 26 to API 29 This enables SQLite window functions (LAG, LEAD, etc.) for efficient record calculations without requiring sync pre-computation. Android 10 (API 29) was released in September 2019 and covers 95%+ of active Android devices as of 2026. * feat(stats): add SQL query for max distance between charges Uses SQLite LAG window function to efficiently find the maximum distance traveled between consecutive charges. Includes both all-time and date-range variants for year filtering support. * feat(stats): add MaxDistanceBetweenChargesRecord domain model Add new record type to QuickStats for tracking the maximum distance driven between two consecutive charging sessions. * feat(stats): integrate max distance between charges in repository Wire up the new DAO query to QuickStats for both all-time and year-filtered views. No sync required - data is computed instantly from the existing charges summary table. * feat(stats): display max distance between charges in Records UI Add 'Longest Range' record showing the maximum distance traveled between two consecutive charges. Displays with battery emoji and date range, tapping navigates to the ending charge detail. * feat(stats): add SQL query for max distance between charges Uses a self-join with correlated subquery to efficiently find the maximum distance traveled between consecutive charges. Includes both all-time and date-range variants for year filtering support. * fix(stats): use sum of drives instead of odometer diff for longest range The odometer difference between charges can include unlogged drives (e.g., when TeslaMate was down). Now we sum actual logged drives between charges, which gives accurate results even with data gaps. * feat(stats): add popup dialog for longest range record details When tapping the 'Longest Range' record, show a dialog with: - Total distance and date range summary - Scrollable list of all drives that make up the record - Each drive is tappable to navigate to drive details This provides full context about what drove the record instead of just navigating to a single charge. * docs: simplify changelog entries for users
This commit is contained in:
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Stats for Nerds**: New "Longest Range" record showing maximum distance traveled between charges (fixes #24)
|
||||
- Tap to see all drives that made up the record
|
||||
- **Dashboard**: Breathing glow effect around car image when charging
|
||||
- Glow pulses smoothly in opacity with 2-second cycle
|
||||
- Color shifts from palette accent toward AC (green) or DC (orange) charging color
|
||||
@@ -31,6 +33,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Dashboard**: AC charging details below SoC bar showing Voltage, Current, and Phases
|
||||
- **Domain**: Battery chemistry detection (LFP vs NMC) based on trim_badging
|
||||
|
||||
### Changed
|
||||
- **Requirements**: Minimum Android version raised from 8.0 to 10 (released 2019)
|
||||
|
||||
## [0.8.3] - 2026-01-11
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -33,7 +33,7 @@ A native Android application for viewing Tesla vehicle data from your self-hoste
|
||||
|
||||
## Requirements
|
||||
|
||||
- Android 8.0 (API 26) or higher
|
||||
- Android 10 (API 29) or higher
|
||||
- A running [Teslamate](https://github.com/adriankumpf/teslamate) instance
|
||||
- [TeslamateApi](https://github.com/tobiasehlert/teslamateapi) deployed and accessible
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.matedroid"
|
||||
minSdk = 26
|
||||
minSdk = 29
|
||||
targetSdk = 35
|
||||
versionCode = 13
|
||||
versionName = "0.8.2"
|
||||
|
||||
@@ -157,4 +157,84 @@ interface ChargeSummaryDao {
|
||||
ORDER BY year DESC
|
||||
""")
|
||||
suspend fun getYears(carId: Int): List<Int>
|
||||
|
||||
// === Range Records Queries ===
|
||||
|
||||
/**
|
||||
* Find the maximum distance traveled between two consecutive charges.
|
||||
* Sums actual logged drives between charges (not odometer diff, which can include unlogged drives).
|
||||
*/
|
||||
@Query("""
|
||||
SELECT
|
||||
prev.chargeId as fromChargeId,
|
||||
curr.chargeId as toChargeId,
|
||||
COALESCE((
|
||||
SELECT SUM(d.distance)
|
||||
FROM drives_summary d
|
||||
WHERE d.carId = curr.carId
|
||||
AND d.startDate > prev.startDate
|
||||
AND d.startDate < curr.startDate
|
||||
), 0) as distance,
|
||||
prev.startDate as fromDate,
|
||||
curr.startDate as toDate
|
||||
FROM charges_summary curr
|
||||
INNER JOIN charges_summary prev ON prev.carId = curr.carId
|
||||
AND prev.startDate = (
|
||||
SELECT MAX(p.startDate)
|
||||
FROM charges_summary p
|
||||
WHERE p.carId = curr.carId AND p.startDate < curr.startDate
|
||||
)
|
||||
WHERE curr.carId = :carId
|
||||
ORDER BY distance DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
suspend fun maxDistanceBetweenCharges(carId: Int): MaxDistanceBetweenChargesResult?
|
||||
|
||||
/**
|
||||
* Find the maximum distance traveled between two consecutive charges within a date range.
|
||||
* Both charges must be within the range.
|
||||
* Sums actual logged drives between charges (not odometer diff, which can include unlogged drives).
|
||||
*/
|
||||
@Query("""
|
||||
SELECT
|
||||
prev.chargeId as fromChargeId,
|
||||
curr.chargeId as toChargeId,
|
||||
COALESCE((
|
||||
SELECT SUM(d.distance)
|
||||
FROM drives_summary d
|
||||
WHERE d.carId = curr.carId
|
||||
AND d.startDate > prev.startDate
|
||||
AND d.startDate < curr.startDate
|
||||
), 0) as distance,
|
||||
prev.startDate as fromDate,
|
||||
curr.startDate as toDate
|
||||
FROM charges_summary curr
|
||||
INNER JOIN charges_summary prev ON prev.carId = curr.carId
|
||||
AND prev.startDate = (
|
||||
SELECT MAX(p.startDate)
|
||||
FROM charges_summary p
|
||||
WHERE p.carId = curr.carId AND p.startDate < curr.startDate
|
||||
)
|
||||
WHERE curr.carId = :carId
|
||||
AND prev.startDate >= :startDate
|
||||
AND curr.startDate < :endDate
|
||||
ORDER BY distance DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
suspend fun maxDistanceBetweenChargesInRange(
|
||||
carId: Int,
|
||||
startDate: String,
|
||||
endDate: String
|
||||
): MaxDistanceBetweenChargesResult?
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of max distance between charges query.
|
||||
*/
|
||||
data class MaxDistanceBetweenChargesResult(
|
||||
val fromChargeId: Int,
|
||||
val toChargeId: Int,
|
||||
val distance: Double,
|
||||
val fromDate: String,
|
||||
val toDate: String
|
||||
)
|
||||
|
||||
@@ -236,6 +236,21 @@ interface DriveSummaryDao {
|
||||
ORDER BY year DESC
|
||||
""")
|
||||
suspend fun getYears(carId: Int): List<Int>
|
||||
|
||||
// === Range Record Queries ===
|
||||
|
||||
/**
|
||||
* Get all drives between two dates (exclusive), ordered by start date.
|
||||
* Used for showing drives in a "longest range" record.
|
||||
*/
|
||||
@Query("""
|
||||
SELECT * FROM drives_summary
|
||||
WHERE carId = :carId
|
||||
AND startDate > :afterDate
|
||||
AND startDate < :beforeDate
|
||||
ORDER BY startDate ASC
|
||||
""")
|
||||
suspend fun getDrivesBetweenDates(carId: Int, afterDate: String, beforeDate: String): List<DriveSummary>
|
||||
}
|
||||
|
||||
data class BusiestDayResult(
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.matedroid.domain.model.DeepStats
|
||||
import com.matedroid.domain.model.DriveElevationRecord
|
||||
import com.matedroid.domain.model.DriveTempRecord
|
||||
import com.matedroid.domain.model.QuickStats
|
||||
import com.matedroid.domain.model.MaxDistanceBetweenChargesRecord
|
||||
import com.matedroid.domain.model.YearFilter
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -81,7 +82,17 @@ class StatsRepository @Inject constructor(
|
||||
firstDriveDate = driveSummaryDao.firstDriveDate(carId),
|
||||
firstChargeDate = chargeSummaryDao.firstChargeDate(carId),
|
||||
busiestDay = driveSummaryDao.busiestDay(carId),
|
||||
mostDistanceDay = driveSummaryDao.mostDistanceDay(carId)
|
||||
mostDistanceDay = driveSummaryDao.mostDistanceDay(carId),
|
||||
|
||||
maxDistanceBetweenCharges = chargeSummaryDao.maxDistanceBetweenCharges(carId)?.let {
|
||||
MaxDistanceBetweenChargesRecord(
|
||||
distance = it.distance,
|
||||
fromChargeId = it.fromChargeId,
|
||||
toChargeId = it.toChargeId,
|
||||
fromDate = it.fromDate,
|
||||
toDate = it.toDate
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -115,7 +126,17 @@ class StatsRepository @Inject constructor(
|
||||
firstDriveDate = driveSummaryDao.firstDriveDate(carId), // Always show first ever
|
||||
firstChargeDate = chargeSummaryDao.firstChargeDate(carId), // Always show first ever
|
||||
busiestDay = driveSummaryDao.busiestDayInRange(carId, startDate, endDate),
|
||||
mostDistanceDay = driveSummaryDao.mostDistanceDayInRange(carId, startDate, endDate)
|
||||
mostDistanceDay = driveSummaryDao.mostDistanceDayInRange(carId, startDate, endDate),
|
||||
|
||||
maxDistanceBetweenCharges = chargeSummaryDao.maxDistanceBetweenChargesInRange(carId, startDate, endDate)?.let {
|
||||
MaxDistanceBetweenChargesRecord(
|
||||
distance = it.distance,
|
||||
fromChargeId = it.fromChargeId,
|
||||
toChargeId = it.toChargeId,
|
||||
fromDate = it.fromDate,
|
||||
toDate = it.toDate
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -375,6 +396,12 @@ class StatsRepository @Inject constructor(
|
||||
return progress != null && progress.phase.isProcessing()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get drives between two dates (for range record details).
|
||||
*/
|
||||
suspend fun getDrivesBetweenDates(carId: Int, afterDate: String, beforeDate: String) =
|
||||
driveSummaryDao.getDrivesBetweenDates(carId, afterDate, beforeDate)
|
||||
|
||||
/**
|
||||
* Get the sync completion percentage for deep stats.
|
||||
* Returns 1.0 if sync is marked complete, regardless of actual count
|
||||
|
||||
@@ -62,7 +62,10 @@ data class QuickStats(
|
||||
val firstDriveDate: String?,
|
||||
val firstChargeDate: String?,
|
||||
val busiestDay: BusiestDayResult?,
|
||||
val mostDistanceDay: MostDistanceDayResult?
|
||||
val mostDistanceDay: MostDistanceDayResult?,
|
||||
|
||||
// === Range Records ===
|
||||
val maxDistanceBetweenCharges: MaxDistanceBetweenChargesRecord?
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -154,3 +157,14 @@ data class ChargePowerRecord(
|
||||
val powerKw: Int,
|
||||
val date: String?
|
||||
)
|
||||
|
||||
/**
|
||||
* Record for maximum distance traveled between two consecutive charges.
|
||||
*/
|
||||
data class MaxDistanceBetweenChargesRecord(
|
||||
val distance: Double, // km
|
||||
val fromChargeId: Int,
|
||||
val toChargeId: Int,
|
||||
val fromDate: String,
|
||||
val toDate: String
|
||||
)
|
||||
|
||||
@@ -72,8 +72,10 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.matedroid.data.local.entity.DriveSummary
|
||||
import com.matedroid.domain.model.CarStats
|
||||
import com.matedroid.domain.model.DeepStats
|
||||
import com.matedroid.domain.model.MaxDistanceBetweenChargesRecord
|
||||
import com.matedroid.domain.model.QuickStats
|
||||
import com.matedroid.domain.model.SyncPhase
|
||||
import com.matedroid.domain.model.YearFilter
|
||||
@@ -99,6 +101,20 @@ fun StatsScreen(
|
||||
val palette = CarColorPalettes.forExteriorColor(exteriorColor, isDarkTheme)
|
||||
var showSyncLogsDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// State for range record dialog
|
||||
var rangeRecordToShow by remember { mutableStateOf<MaxDistanceBetweenChargesRecord?>(null) }
|
||||
var rangeRecordDrives by remember { mutableStateOf<List<DriveSummary>>(emptyList()) }
|
||||
var isLoadingRangeRecordDrives by remember { mutableStateOf(false) }
|
||||
|
||||
// Load drives when range record dialog is opened
|
||||
LaunchedEffect(rangeRecordToShow) {
|
||||
rangeRecordToShow?.let { record ->
|
||||
isLoadingRangeRecordDrives = true
|
||||
rangeRecordDrives = viewModel.getDrivesForRangeRecord(record.fromDate, record.toDate)
|
||||
isLoadingRangeRecordDrives = false
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(carId) {
|
||||
viewModel.setCarId(carId)
|
||||
}
|
||||
@@ -126,6 +142,21 @@ fun StatsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Range record details dialog
|
||||
rangeRecordToShow?.let { record ->
|
||||
RangeRecordDialog(
|
||||
record = record,
|
||||
drives = rangeRecordDrives,
|
||||
isLoading = isLoadingRangeRecordDrives,
|
||||
palette = palette,
|
||||
onDriveClick = { driveId ->
|
||||
rangeRecordToShow = null
|
||||
onNavigateToDriveDetail(driveId)
|
||||
},
|
||||
onDismiss = { rangeRecordToShow = null }
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -178,6 +209,7 @@ fun StatsScreen(
|
||||
onNavigateToDriveDetail = onNavigateToDriveDetail,
|
||||
onNavigateToChargeDetail = onNavigateToChargeDetail,
|
||||
onNavigateToDayDetail = onNavigateToDayDetail,
|
||||
onRangeRecordClick = { rangeRecordToShow = it },
|
||||
onSyncProgressClick = if (BuildConfig.DEBUG) {
|
||||
{ showSyncLogsDialog = true }
|
||||
} else null
|
||||
@@ -272,6 +304,7 @@ private fun StatsContent(
|
||||
onNavigateToDriveDetail: (Int) -> Unit,
|
||||
onNavigateToChargeDetail: (Int) -> Unit,
|
||||
onNavigateToDayDetail: (String) -> Unit,
|
||||
onRangeRecordClick: (MaxDistanceBetweenChargesRecord) -> Unit,
|
||||
onSyncProgressClick: (() -> Unit)? = null
|
||||
) {
|
||||
LazyColumn(
|
||||
@@ -309,7 +342,8 @@ private fun StatsContent(
|
||||
currencySymbol = currencySymbol,
|
||||
onDriveClick = onNavigateToDriveDetail,
|
||||
onChargeClick = onNavigateToChargeDetail,
|
||||
onDayClick = onNavigateToDayDetail
|
||||
onDayClick = onNavigateToDayDetail,
|
||||
onRangeRecordClick = onRangeRecordClick
|
||||
)
|
||||
}
|
||||
|
||||
@@ -542,7 +576,8 @@ private fun RecordsCard(
|
||||
currencySymbol: String,
|
||||
onDriveClick: (Int) -> Unit,
|
||||
onChargeClick: (Int) -> Unit,
|
||||
onDayClick: (String) -> Unit
|
||||
onDayClick: (String) -> Unit,
|
||||
onRangeRecordClick: (MaxDistanceBetweenChargesRecord) -> Unit
|
||||
) {
|
||||
// Build list of record groups - each group starts on left column
|
||||
val groups = mutableListOf<RecordGroup>()
|
||||
@@ -552,6 +587,9 @@ private fun RecordsCard(
|
||||
quickStats.longestDrive?.let { drive ->
|
||||
driveRecords.add(RecordData("📏", "Longest Drive", "%.1f km".format(drive.distance), drive.startDate.take(10)) { onDriveClick(drive.driveId) })
|
||||
}
|
||||
quickStats.maxDistanceBetweenCharges?.let { record ->
|
||||
driveRecords.add(RecordData("🔋", "Longest Range", "%.1f km".format(record.distance), "${record.fromDate.take(10)} → ${record.toDate.take(10)}") { onRangeRecordClick(record) })
|
||||
}
|
||||
quickStats.fastestDrive?.let { drive ->
|
||||
driveRecords.add(RecordData("🏎️", "Top Speed", "${drive.speedMax} km/h", drive.startDate.take(10)) { onDriveClick(drive.driveId) })
|
||||
}
|
||||
@@ -1036,3 +1074,213 @@ private fun SyncLogsDialog(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog showing details of a "longest range" record with scrollable list of drives.
|
||||
*/
|
||||
@Composable
|
||||
private fun RangeRecordDialog(
|
||||
record: MaxDistanceBetweenChargesRecord,
|
||||
drives: List<DriveSummary>,
|
||||
isLoading: Boolean,
|
||||
palette: CarColorPalette,
|
||||
onDriveClick: (Int) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("🔋", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Longest Range")
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Summary info
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = palette.surface
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Total Distance",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = palette.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "%.1f km".format(record.distance),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = palette.onSurface
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "From",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = palette.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = record.fromDate.take(10),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = palette.onSurface
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = "To",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = palette.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = record.toDate.take(10),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = palette.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Drives header
|
||||
Text(
|
||||
text = "Drives (${drives.size})",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = palette.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Scrollable list of drives
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(250.dp)
|
||||
) {
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(32.dp))
|
||||
}
|
||||
} else if (drives.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No drives found",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = palette.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(drives) { drive ->
|
||||
DriveListItem(
|
||||
drive = drive,
|
||||
palette = palette,
|
||||
onClick = { onDriveClick(drive.driveId) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Close")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Single drive item in the range record dialog.
|
||||
*/
|
||||
@Composable
|
||||
private fun DriveListItem(
|
||||
drive: DriveSummary,
|
||||
palette: CarColorPalette,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() },
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = palette.surface.copy(alpha = 0.7f)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = drive.startDate.take(10),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = palette.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "${drive.startAddress.take(25)}${if (drive.startAddress.length > 25) "..." else ""}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = palette.onSurface,
|
||||
maxLines = 1
|
||||
)
|
||||
Text(
|
||||
text = "→ ${drive.endAddress.take(25)}${if (drive.endAddress.length > 25) "..." else ""}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = palette.onSurfaceVariant,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = "%.1f km".format(drive.distance),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = palette.onSurface
|
||||
)
|
||||
Text(
|
||||
text = "${drive.durationMin} min",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = palette.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = "View drive",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = palette.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.matedroid.data.repository.StatsRepository
|
||||
import com.matedroid.data.sync.DataSyncWorker
|
||||
import com.matedroid.data.sync.SyncLogCollector
|
||||
import com.matedroid.data.sync.SyncManager
|
||||
import com.matedroid.data.local.entity.DriveSummary
|
||||
import com.matedroid.domain.model.CarStats
|
||||
import com.matedroid.domain.model.SyncPhase
|
||||
import com.matedroid.domain.model.SyncProgress
|
||||
@@ -163,6 +164,14 @@ class StatsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch drives between two dates for displaying range record details.
|
||||
*/
|
||||
suspend fun getDrivesForRangeRecord(fromDate: String, toDate: String): List<DriveSummary> {
|
||||
val id = carId ?: return emptyList()
|
||||
return statsRepository.getDrivesBetweenDates(id, fromDate, toDate)
|
||||
}
|
||||
|
||||
private suspend fun loadStatsInternal() {
|
||||
val id = carId ?: return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user