Add sleep tracking, statistics module and backup features

Features:
- Sleep tracking with timer and manual duration input
- Statistics module with 5 tabs (daily summary, feeding, diapers, sleep, growth)
- Export/Import backup functionality in settings
- Complete German, French and Italian translations
This commit is contained in:
2026-01-08 09:33:36 +01:00
parent 587fc5d3e3
commit 2355dd4390
32 changed files with 2841 additions and 9 deletions

View File

@@ -0,0 +1,343 @@
package utils
import it.danieleverducci.lunatracker.entities.LunaEvent
import java.util.Calendar
/**
* Data classes for statistics results
*/
data class DailySummary(
val date: Long,
val totalBottleMl: Int,
val bottleCount: Int,
val totalBreastfeedingMin: Int,
val breastfeedingCount: Int,
val breastfeedingLeftCount: Int,
val breastfeedingRightCount: Int,
val totalSleepMin: Int,
val sleepCount: Int,
val diaperPooCount: Int,
val diaperPeeCount: Int,
val totalFoodCount: Int,
val latestWeight: Int?,
val latestTemperature: Int?
)
data class FeedingStats(
val dailyBottleTotals: Map<Long, Int>,
val dailyBreastfeedingTotals: Map<Long, Int>,
val avgBottleMlPerDay: Float,
val avgBreastfeedingMinPerDay: Float,
val leftBreastCount: Int,
val rightBreastCount: Int,
val bothBreastCount: Int,
val avgBreastfeedingDuration: Float,
val avgFeedingIntervalMinutes: Long
)
data class DiaperStats(
val dailyPooCount: Map<Long, Int>,
val dailyPeeCount: Map<Long, Int>,
val avgDiapersPerDay: Float,
val avgPooPerDay: Float,
val avgPeePerDay: Float,
val lastPooTime: Long?
)
data class SleepStats(
val dailyTotals: Map<Long, Int>,
val avgSleepMinPerDay: Float,
val avgNapsPerDay: Float,
val avgNapDurationMin: Float,
val longestSleepMin: Int,
val lastSleepTime: Long?
)
data class WeightPoint(
val time: Long,
val weightGrams: Int
)
data class TemperaturePoint(
val time: Long,
val temperatureDeciCelsius: Int
)
/**
* Calculator for statistics based on LunaEvent data
*/
class StatisticsCalculator(private val events: List<LunaEvent>) {
private fun getStartOfDay(unixTimeSeconds: Long): Long {
val cal = Calendar.getInstance()
cal.timeInMillis = unixTimeSeconds * 1000
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
cal.set(Calendar.MILLISECOND, 0)
return cal.timeInMillis / 1000
}
private fun getEventsInRange(startUnix: Long, endUnix: Long): List<LunaEvent> {
return events.filter { it.time >= startUnix && it.time < endUnix }
}
private fun getEventsForDays(days: Int): List<LunaEvent> {
val now = System.currentTimeMillis() / 1000
val startOfToday = getStartOfDay(now)
val startTime = startOfToday - (days - 1) * 24 * 60 * 60
return events.filter { it.time >= startTime }
}
/**
* Get summary for a specific day (unix timestamp in seconds)
*/
fun getDailySummary(dayUnix: Long): DailySummary {
val startOfDay = getStartOfDay(dayUnix)
val endOfDay = startOfDay + 24 * 60 * 60
val dayEvents = getEventsInRange(startOfDay, endOfDay)
val bottleEvents = dayEvents.filter { it.type == LunaEvent.TYPE_BABY_BOTTLE }
val breastfeedingEvents = dayEvents.filter {
it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE ||
it.type == LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE ||
it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
}
val sleepEvents = dayEvents.filter { it.type == LunaEvent.TYPE_SLEEP }
val pooEvents = dayEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_POO }
val peeEvents = dayEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_PEE }
val foodEvents = dayEvents.filter { it.type == LunaEvent.TYPE_FOOD }
val weightEvents = dayEvents.filter { it.type == LunaEvent.TYPE_WEIGHT }
val tempEvents = dayEvents.filter { it.type == LunaEvent.TYPE_TEMPERATURE }
return DailySummary(
date = startOfDay,
totalBottleMl = bottleEvents.sumOf { it.quantity },
bottleCount = bottleEvents.size,
totalBreastfeedingMin = breastfeedingEvents.sumOf { it.quantity },
breastfeedingCount = breastfeedingEvents.size,
breastfeedingLeftCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE },
breastfeedingRightCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE },
totalSleepMin = sleepEvents.sumOf { it.quantity },
sleepCount = sleepEvents.size,
diaperPooCount = pooEvents.size,
diaperPeeCount = peeEvents.size,
totalFoodCount = foodEvents.size,
latestWeight = weightEvents.maxByOrNull { it.time }?.quantity,
latestTemperature = tempEvents.maxByOrNull { it.time }?.quantity
)
}
/**
* Get today's summary
*/
fun getTodaySummary(): DailySummary {
return getDailySummary(System.currentTimeMillis() / 1000)
}
/**
* Get feeding statistics for the last N days
*/
fun getFeedingStats(days: Int): FeedingStats {
val relevantEvents = getEventsForDays(days)
val now = System.currentTimeMillis() / 1000
val startOfToday = getStartOfDay(now)
// Daily totals
val dailyBottleTotals = mutableMapOf<Long, Int>()
val dailyBreastfeedingTotals = mutableMapOf<Long, Int>()
for (i in 0 until days) {
val dayStart = startOfToday - i * 24 * 60 * 60
dailyBottleTotals[dayStart] = 0
dailyBreastfeedingTotals[dayStart] = 0
}
val bottleEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_BABY_BOTTLE }
val breastfeedingEvents = relevantEvents.filter {
it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE ||
it.type == LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE ||
it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
}
bottleEvents.forEach { event ->
val dayStart = getStartOfDay(event.time)
dailyBottleTotals[dayStart] = (dailyBottleTotals[dayStart] ?: 0) + event.quantity
}
breastfeedingEvents.forEach { event ->
val dayStart = getStartOfDay(event.time)
dailyBreastfeedingTotals[dayStart] = (dailyBreastfeedingTotals[dayStart] ?: 0) + event.quantity
}
// Breastfeeding side distribution
val leftCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE }
val rightCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE }
val bothCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE }
// Average breastfeeding duration
val avgBreastfeedingDuration = if (breastfeedingEvents.isNotEmpty()) {
breastfeedingEvents.sumOf { it.quantity }.toFloat() / breastfeedingEvents.size
} else 0f
// Average feeding interval (all feeding events sorted by time)
val allFeedingEvents = (bottleEvents + breastfeedingEvents).sortedBy { it.time }
val avgFeedingIntervalMinutes = if (allFeedingEvents.size > 1) {
var totalInterval = 0L
for (i in 1 until allFeedingEvents.size) {
totalInterval += allFeedingEvents[i].time - allFeedingEvents[i-1].time
}
(totalInterval / (allFeedingEvents.size - 1)) / 60
} else 0L
return FeedingStats(
dailyBottleTotals = dailyBottleTotals,
dailyBreastfeedingTotals = dailyBreastfeedingTotals,
avgBottleMlPerDay = if (days > 0) dailyBottleTotals.values.sum().toFloat() / days else 0f,
avgBreastfeedingMinPerDay = if (days > 0) dailyBreastfeedingTotals.values.sum().toFloat() / days else 0f,
leftBreastCount = leftCount,
rightBreastCount = rightCount,
bothBreastCount = bothCount,
avgBreastfeedingDuration = avgBreastfeedingDuration,
avgFeedingIntervalMinutes = avgFeedingIntervalMinutes
)
}
/**
* Get diaper statistics for the last N days
*/
fun getDiaperStats(days: Int): DiaperStats {
val relevantEvents = getEventsForDays(days)
val now = System.currentTimeMillis() / 1000
val startOfToday = getStartOfDay(now)
val dailyPooCount = mutableMapOf<Long, Int>()
val dailyPeeCount = mutableMapOf<Long, Int>()
for (i in 0 until days) {
val dayStart = startOfToday - i * 24 * 60 * 60
dailyPooCount[dayStart] = 0
dailyPeeCount[dayStart] = 0
}
val pooEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_POO }
val peeEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_PEE }
pooEvents.forEach { event ->
val dayStart = getStartOfDay(event.time)
dailyPooCount[dayStart] = (dailyPooCount[dayStart] ?: 0) + 1
}
peeEvents.forEach { event ->
val dayStart = getStartOfDay(event.time)
dailyPeeCount[dayStart] = (dailyPeeCount[dayStart] ?: 0) + 1
}
val totalDiapers = pooEvents.size + peeEvents.size
return DiaperStats(
dailyPooCount = dailyPooCount,
dailyPeeCount = dailyPeeCount,
avgDiapersPerDay = if (days > 0) totalDiapers.toFloat() / days else 0f,
avgPooPerDay = if (days > 0) pooEvents.size.toFloat() / days else 0f,
avgPeePerDay = if (days > 0) peeEvents.size.toFloat() / days else 0f,
lastPooTime = pooEvents.maxByOrNull { it.time }?.time
)
}
/**
* Get sleep statistics for the last N days
*/
fun getSleepStats(days: Int): SleepStats {
val relevantEvents = getEventsForDays(days)
val now = System.currentTimeMillis() / 1000
val startOfToday = getStartOfDay(now)
val dailyTotals = mutableMapOf<Long, Int>()
for (i in 0 until days) {
val dayStart = startOfToday - i * 24 * 60 * 60
dailyTotals[dayStart] = 0
}
val sleepEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_SLEEP }
sleepEvents.forEach { event ->
val dayStart = getStartOfDay(event.time)
dailyTotals[dayStart] = (dailyTotals[dayStart] ?: 0) + event.quantity
}
val totalSleepMin = sleepEvents.sumOf { it.quantity }
val avgNapDuration = if (sleepEvents.isNotEmpty()) {
totalSleepMin.toFloat() / sleepEvents.size
} else 0f
val longestSleep = sleepEvents.maxOfOrNull { it.quantity } ?: 0
return SleepStats(
dailyTotals = dailyTotals,
avgSleepMinPerDay = if (days > 0) totalSleepMin.toFloat() / days else 0f,
avgNapsPerDay = if (days > 0) sleepEvents.size.toFloat() / days else 0f,
avgNapDurationMin = avgNapDuration,
longestSleepMin = longestSleep,
lastSleepTime = sleepEvents.maxByOrNull { it.time }?.time
)
}
/**
* Get weight history (all weight measurements)
*/
fun getWeightHistory(): List<WeightPoint> {
return events
.filter { it.type == LunaEvent.TYPE_WEIGHT && it.quantity > 0 }
.sortedBy { it.time }
.map { WeightPoint(it.time, it.quantity) }
}
/**
* Get temperature history
*/
fun getTemperatureHistory(): List<TemperaturePoint> {
return events
.filter { it.type == LunaEvent.TYPE_TEMPERATURE && it.quantity > 0 }
.sortedBy { it.time }
.map { TemperaturePoint(it.time, it.quantity) }
}
/**
* Calculate weight gain over the last N days
*/
fun getWeightGainForDays(days: Int): Int? {
val weights = getWeightHistory()
if (weights.size < 2) return null
val now = System.currentTimeMillis() / 1000
val startTime = now - days * 24 * 60 * 60
val recentWeight = weights.lastOrNull() ?: return null
val olderWeight = weights.filter { it.time <= startTime }.lastOrNull()
?: weights.firstOrNull()
?: return null
if (recentWeight.time == olderWeight.time) return null
return recentWeight.weightGrams - olderWeight.weightGrams
}
/**
* Get average daily values for a type of event over N days
*/
fun getAverageDailyCount(type: String, days: Int): Float {
val relevantEvents = getEventsForDays(days).filter { it.type == type }
return if (days > 0) relevantEvents.size.toFloat() / days else 0f
}
/**
* Get average daily quantity sum for a type of event over N days
*/
fun getAverageDailyQuantity(type: String, days: Int): Float {
val relevantEvents = getEventsForDays(days).filter { it.type == type }
val total = relevantEvents.sumOf { it.quantity }
return if (days > 0) total.toFloat() / days else 0f
}
}