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 dccc89a8e2
commit b6110c2cbb
32 changed files with 2841 additions and 9 deletions

View File

@@ -76,6 +76,12 @@ class MainActivity : AppCompatActivity() {
var bfTimerHandler: Handler? = null
var bfTimerRunnable: Runnable? = null
// Sleep timer state
var sleepTimerStartTime: Long = 0
var sleepTimerDialog: AlertDialog? = null
var sleepTimerHandler: Handler? = null
var sleepTimerRunnable: Runnable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -134,6 +140,9 @@ class MainActivity : AppCompatActivity() {
findViewById<View>(R.id.button_settings).setOnClickListener {
showSettings()
}
findViewById<View>(R.id.button_statistics).setOnClickListener {
showStatistics()
}
findViewById<View>(R.id.button_no_connection_retry).setOnClickListener {
// This may happen at start, when logbook is still null: better ask the logbook list
loadLogbookList()
@@ -164,6 +173,12 @@ class MainActivity : AppCompatActivity() {
startActivity(i)
}
fun showStatistics() {
val i = Intent(this, StatisticsActivity::class.java)
i.putExtra(StatisticsActivity.EXTRA_LOGBOOK_NAME, logbook?.name ?: "")
startActivity(i)
}
fun showLogbook() {
// Show logbook
if (logbook == null)
@@ -204,6 +219,9 @@ class MainActivity : AppCompatActivity() {
// Check for ongoing breastfeeding timer
restoreBreastfeedingTimerIfNeeded()
// Check for ongoing sleep timer
restoreSleepTimerIfNeeded()
if (logbook != null) {
// Already running: reload data for currently selected logbook
loadLogbook(logbook!!.name)
@@ -220,6 +238,10 @@ class MainActivity : AppCompatActivity() {
bfTimerRunnable?.let { bfTimerHandler?.removeCallbacks(it) }
bfTimerDialog?.dismiss()
// Clean up sleep timer UI (state is preserved in SharedPreferences)
sleepTimerRunnable?.let { sleepTimerHandler?.removeCallbacks(it) }
sleepTimerDialog?.dismiss()
super.onStop()
}
@@ -452,6 +474,117 @@ class MainActivity : AppCompatActivity() {
}
}
// Sleep timer methods
fun startSleepTimer() {
// Check if timer already running
if (sleepTimerStartTime > 0) {
Toast.makeText(this, R.string.sleep_timer_already_running, Toast.LENGTH_SHORT).show()
return
}
// Save timer state
sleepTimerStartTime = System.currentTimeMillis()
saveSleepTimerState()
// Show timer dialog
showSleepTimerDialog()
}
fun showSleepTimerDialog() {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.sleep_timer_dialog, null)
d.setTitle(R.string.sleep_timer_title)
d.setView(dialogView)
d.setCancelable(false)
val timerDisplay = dialogView.findViewById<TextView>(R.id.sleep_timer_display)
// Set up timer updates
sleepTimerHandler = Handler(mainLooper)
sleepTimerRunnable = object : Runnable {
override fun run() {
val elapsed = (System.currentTimeMillis() - sleepTimerStartTime) / 1000
val hours = elapsed / 3600
val minutes = (elapsed % 3600) / 60
val seconds = elapsed % 60
timerDisplay.text = if (hours > 0) {
String.format("%d:%02d:%02d", hours, minutes, seconds)
} else {
String.format("%02d:%02d", minutes, seconds)
}
sleepTimerHandler?.postDelayed(this, 1000)
}
}
sleepTimerHandler?.post(sleepTimerRunnable!!)
d.setPositiveButton(R.string.sleep_timer_stop) { _, _ ->
stopSleepTimer()
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
cancelSleepTimer()
dialogInterface.dismiss()
}
sleepTimerDialog = d.create()
sleepTimerDialog?.show()
}
fun stopSleepTimer() {
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
val durationMillis = System.currentTimeMillis() - sleepTimerStartTime
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
clearSleepTimerState()
logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, durationMinutes))
}
fun cancelSleepTimer() {
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
clearSleepTimerState()
}
fun askSleepDuration() {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.sleep_duration_dialog, null)
d.setTitle(R.string.sleep_duration_title)
d.setMessage(R.string.sleep_duration_description)
d.setView(dialogView)
val numberPicker = dialogView.findViewById<NumberPicker>(R.id.sleep_duration_picker)
numberPicker.minValue = 1
numberPicker.maxValue = 180 // Up to 3 hours
numberPicker.value = 30 // Default 30 minutes
numberPicker.wrapSelectorWheel = false
d.setPositiveButton(android.R.string.ok) { _, _ ->
logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, numberPicker.value))
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
dialogInterface.dismiss()
}
d.create().show()
}
fun saveSleepTimerState() {
LocalSettingsRepository(this).saveSleepTimer(sleepTimerStartTime)
}
fun clearSleepTimerState() {
sleepTimerStartTime = 0
sleepTimerDialog = null
LocalSettingsRepository(this).clearSleepTimer()
}
fun restoreSleepTimerIfNeeded() {
val startTime = LocalSettingsRepository(this).loadSleepTimer()
if (startTime > 0) {
sleepTimerStartTime = startTime
showSleepTimerDialog()
}
}
fun askToTrimLogbook() {
val d = AlertDialog.Builder(this)
d.setTitle(R.string.trim_logbook_dialog_title)
@@ -539,24 +672,32 @@ class MainActivity : AppCompatActivity() {
}, startYear, startMonth, startDay).show()
}
// Make quantity editable for breastfeeding events
// Make quantity editable for breastfeeding and sleep events
val quantityTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity)
if (event.type in listOf(
val isBreastfeeding = event.type in listOf(
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
) && event.quantity > 0) {
)
val isSleep = event.type == LunaEvent.TYPE_SLEEP
if ((isBreastfeeding || isSleep) && event.quantity > 0) {
quantityTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_edit, 0)
quantityTextView.compoundDrawableTintList = ColorStateList.valueOf(getColor(R.color.accent))
quantityTextView.setOnClickListener {
val pickerDialog = AlertDialog.Builder(this@MainActivity)
val pickerView = layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
val picker = pickerView.findViewById<NumberPicker>(R.id.breastfeeding_duration_picker)
val pickerView = if (isSleep) {
layoutInflater.inflate(R.layout.sleep_duration_dialog, null)
} else {
layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
}
val picker = pickerView.findViewById<NumberPicker>(
if (isSleep) R.id.sleep_duration_picker else R.id.breastfeeding_duration_picker
)
picker.minValue = 1
picker.maxValue = 60
picker.value = if (event.quantity > 0) event.quantity else 15
picker.maxValue = if (isSleep) 180 else 60
picker.value = if (event.quantity > 0) Math.min(event.quantity, picker.maxValue) else if (isSleep) 30 else 15
pickerDialog.setTitle(R.string.breastfeeding_duration_title)
pickerDialog.setTitle(if (isSleep) R.string.sleep_duration_title else R.string.breastfeeding_duration_title)
pickerDialog.setView(pickerView)
pickerDialog.setPositiveButton(android.R.string.ok) { _, _ ->
event.quantity = picker.value
@@ -971,6 +1112,15 @@ class MainActivity : AppCompatActivity() {
isOutsideTouchable = true
val inflater = LayoutInflater.from(anchor.context)
contentView = inflater.inflate(R.layout.more_events_popup, null)
contentView.findViewById<View>(R.id.button_sleep).setOnClickListener {
startSleepTimer()
dismiss()
}
contentView.findViewById<View>(R.id.button_sleep).setOnLongClickListener {
askSleepDuration()
dismiss()
true
}
contentView.findViewById<View>(R.id.button_medicine).setOnClickListener {
askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE))
dismiss()