From 587fc5d3e3c8c573bb17090876f9d19df8460d7d Mon Sep 17 00:00:00 2001 From: Maximilian von Heyden Date: Thu, 25 Dec 2025 18:36:30 +0100 Subject: [PATCH] Add breastfeeding duration tracking and UI improvements Features: - Breastfeeding timer: Click to start, stop to save duration - Manual duration input: Long-press for NumberPicker (1-60 min) - Edit breastfeeding duration: Click on duration in event details - Day separators: Visual dividers between days in event list - German translations: Added missing strings for puke/bath events, time units, amount labels, signature settings, event details The breastfeeding timer state persists across app restarts. --- .../lunatracker/MainActivity.kt | 201 ++++++++++++++++-- .../adapters/DaySeparatorDecoration.kt | 80 +++++++ .../repository/LocalSettingsRepository.kt | 23 ++ app/src/main/java/utils/NumericUtils.kt | 4 + .../layout/breastfeeding_duration_dialog.xml | 20 ++ .../res/layout/breastfeeding_timer_dialog.xml | 35 +++ app/src/main/res/values-de/strings.xml | 38 ++++ app/src/main/res/values-fr/strings.xml | 7 + app/src/main/res/values-it/strings.xml | 7 + app/src/main/res/values/strings.xml | 8 + 10 files changed, 408 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/it/danieleverducci/lunatracker/adapters/DaySeparatorDecoration.kt create mode 100644 app/src/main/res/layout/breastfeeding_duration_dialog.xml create mode 100644 app/src/main/res/layout/breastfeeding_timer_dialog.xml diff --git a/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt index afdd5b8..e80dddb 100644 --- a/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt +++ b/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt @@ -4,6 +4,7 @@ import android.app.DatePickerDialog import android.app.TimePickerDialog import android.content.DialogInterface import android.content.Intent +import android.content.res.ColorStateList import android.os.Bundle import android.os.Handler import android.util.Log @@ -26,6 +27,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.slider.Slider import com.thegrizzlylabs.sardineandroid.impl.SardineException +import it.danieleverducci.lunatracker.adapters.DaySeparatorDecoration import it.danieleverducci.lunatracker.adapters.LunaEventRecyclerAdapter import it.danieleverducci.lunatracker.entities.Logbook import it.danieleverducci.lunatracker.entities.LunaEvent @@ -67,6 +69,13 @@ class MainActivity : AppCompatActivity() { var logbookRepo: LogbookRepository? = null var showingOverflowPopupWindow = false + // Breastfeeding timer state + var bfTimerStartTime: Long = 0 + var bfTimerType: String? = null + var bfTimerDialog: AlertDialog? = null + var bfTimerHandler: Handler? = null + var bfTimerRunnable: Runnable? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -84,21 +93,27 @@ class MainActivity : AppCompatActivity() { findViewById(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog(true) } findViewById(R.id.button_bottle).setOnClickListener { askBabyBottleContent() } findViewById(R.id.button_food).setOnClickListener { askNotes(LunaEvent(LunaEvent.TYPE_FOOD)) } - findViewById(R.id.button_nipple_left).setOnClickListener { logEvent( - LunaEvent( - LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE - ) - ) } - findViewById(R.id.button_nipple_both).setOnClickListener { logEvent( - LunaEvent( - LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE - ) - ) } - findViewById(R.id.button_nipple_right).setOnClickListener { logEvent( - LunaEvent( - LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE - ) - ) } + findViewById(R.id.button_nipple_left).setOnClickListener { + startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE) + } + findViewById(R.id.button_nipple_left).setOnLongClickListener { + askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE) + true + } + findViewById(R.id.button_nipple_both).setOnClickListener { + startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE) + } + findViewById(R.id.button_nipple_both).setOnLongClickListener { + askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE) + true + } + findViewById(R.id.button_nipple_right).setOnClickListener { + startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE) + } + findViewById(R.id.button_nipple_right).setOnLongClickListener { + askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE) + true + } findViewById(R.id.button_change_poo).setOnClickListener { logEvent( LunaEvent( LunaEvent.TYPE_DIAPERCHANGE_POO @@ -136,6 +151,12 @@ class MainActivity : AppCompatActivity() { } } recyclerView.adapter = adapter + + // Tages-Trenner hinzufügen + while (recyclerView.itemDecorationCount > 0) { + recyclerView.removeItemDecorationAt(0) + } + recyclerView.addItemDecoration(DaySeparatorDecoration(this, items)) } fun showSettings() { @@ -180,6 +201,9 @@ class MainActivity : AppCompatActivity() { // Update list dates recyclerView.adapter?.notifyDataSetChanged() + // Check for ongoing breastfeeding timer + restoreBreastfeedingTimerIfNeeded() + if (logbook != null) { // Already running: reload data for currently selected logbook loadLogbook(logbook!!.name) @@ -192,6 +216,10 @@ class MainActivity : AppCompatActivity() { override fun onStop() { handler.removeCallbacks(updateListRunnable) + // Clean up breastfeeding timer UI (state is preserved in SharedPreferences) + bfTimerRunnable?.let { bfTimerHandler?.removeCallbacks(it) } + bfTimerDialog?.dismiss() + super.onStop() } @@ -311,6 +339,119 @@ class MainActivity : AppCompatActivity() { alertDialog.show() } + fun startBreastfeedingTimer(eventType: String) { + // Check if timer already running + if (bfTimerType != null) { + Toast.makeText(this, R.string.breastfeeding_timer_already_running, Toast.LENGTH_SHORT).show() + return + } + + // Save timer state + bfTimerStartTime = System.currentTimeMillis() + bfTimerType = eventType + saveBreastfeedingTimerState() + + // Show timer dialog + showBreastfeedingTimerDialog(eventType) + } + + fun showBreastfeedingTimerDialog(eventType: String) { + val d = AlertDialog.Builder(this) + val dialogView = layoutInflater.inflate(R.layout.breastfeeding_timer_dialog, null) + d.setTitle(R.string.breastfeeding_timer_title) + d.setView(dialogView) + d.setCancelable(false) + + val timerDisplay = dialogView.findViewById(R.id.breastfeeding_timer_display) + val sideEmoji = dialogView.findViewById(R.id.breastfeeding_side_emoji) + sideEmoji.text = LunaEvent(eventType).getTypeEmoji(this) + + // Set up timer updates + bfTimerHandler = Handler(mainLooper) + bfTimerRunnable = object : Runnable { + override fun run() { + val elapsed = (System.currentTimeMillis() - bfTimerStartTime) / 1000 + val minutes = elapsed / 60 + val seconds = elapsed % 60 + timerDisplay.text = String.format("%02d:%02d", minutes, seconds) + bfTimerHandler?.postDelayed(this, 1000) + } + } + bfTimerHandler?.post(bfTimerRunnable!!) + + d.setPositiveButton(R.string.breastfeeding_timer_stop) { _, _ -> + stopBreastfeedingTimer() + } + d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ -> + cancelBreastfeedingTimer() + dialogInterface.dismiss() + } + + bfTimerDialog = d.create() + bfTimerDialog?.show() + } + + fun stopBreastfeedingTimer() { + bfTimerHandler?.removeCallbacks(bfTimerRunnable!!) + + val durationMillis = System.currentTimeMillis() - bfTimerStartTime + val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute + + val eventType = bfTimerType + clearBreastfeedingTimerState() + + if (eventType != null) { + logEvent(LunaEvent(eventType, durationMinutes)) + } + } + + fun cancelBreastfeedingTimer() { + bfTimerHandler?.removeCallbacks(bfTimerRunnable!!) + clearBreastfeedingTimerState() + } + + fun askBreastfeedingDuration(eventType: String) { + val d = AlertDialog.Builder(this) + val dialogView = layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null) + d.setTitle(R.string.breastfeeding_duration_title) + d.setMessage(R.string.breastfeeding_duration_description) + d.setView(dialogView) + + val numberPicker = dialogView.findViewById(R.id.breastfeeding_duration_picker) + numberPicker.minValue = 1 + numberPicker.maxValue = 60 + numberPicker.value = 15 // Default 15 minutes + numberPicker.wrapSelectorWheel = false + + d.setPositiveButton(android.R.string.ok) { _, _ -> + logEvent(LunaEvent(eventType, numberPicker.value)) + } + d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ -> + dialogInterface.dismiss() + } + d.create().show() + } + + fun saveBreastfeedingTimerState() { + LocalSettingsRepository(this).saveBreastfeedingTimer(bfTimerStartTime, bfTimerType ?: "") + } + + fun clearBreastfeedingTimerState() { + bfTimerStartTime = 0 + bfTimerType = null + bfTimerDialog = null + LocalSettingsRepository(this).clearBreastfeedingTimer() + } + + fun restoreBreastfeedingTimerIfNeeded() { + val timerState = LocalSettingsRepository(this).loadBreastfeedingTimer() + if (timerState != null && timerState.first > 0 && timerState.second.isNotEmpty()) { + bfTimerStartTime = timerState.first + bfTimerType = timerState.second + showBreastfeedingTimerDialog(timerState.second) + } + } + fun askToTrimLogbook() { val d = AlertDialog.Builder(this) d.setTitle(R.string.trim_logbook_dialog_title) @@ -398,6 +539,36 @@ class MainActivity : AppCompatActivity() { }, startYear, startMonth, startDay).show() } + // Make quantity editable for breastfeeding events + val quantityTextView = dialogView.findViewById(R.id.dialog_event_detail_type_quantity) + if (event.type in listOf( + LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE, + LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE, + LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE + ) && 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(R.id.breastfeeding_duration_picker) + picker.minValue = 1 + picker.maxValue = 60 + picker.value = if (event.quantity > 0) event.quantity else 15 + + pickerDialog.setTitle(R.string.breastfeeding_duration_title) + pickerDialog.setView(pickerView) + pickerDialog.setPositiveButton(android.R.string.ok) { _, _ -> + event.quantity = picker.value + quantityTextView.text = NumericUtils(this@MainActivity).formatEventQuantity(event) + recyclerView.adapter?.notifyDataSetChanged() + saveLogbook() + } + pickerDialog.setNegativeButton(android.R.string.cancel, null) + pickerDialog.show() + } + } + d.setView(dialogView) d.setPositiveButton(R.string.dialog_event_detail_close_button) { dialogInterface, i -> dialogInterface.dismiss() } d.setNeutralButton(R.string.dialog_event_detail_delete_button) { dialogInterface, i -> deleteEvent(event) } diff --git a/app/src/main/java/it/danieleverducci/lunatracker/adapters/DaySeparatorDecoration.kt b/app/src/main/java/it/danieleverducci/lunatracker/adapters/DaySeparatorDecoration.kt new file mode 100644 index 0000000..428b622 --- /dev/null +++ b/app/src/main/java/it/danieleverducci/lunatracker/adapters/DaySeparatorDecoration.kt @@ -0,0 +1,80 @@ +package it.danieleverducci.lunatracker.adapters + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.text.format.DateFormat +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import it.danieleverducci.lunatracker.R +import it.danieleverducci.lunatracker.entities.LunaEvent +import java.util.Calendar +import java.util.Date + +class DaySeparatorDecoration( + private val context: Context, + private val items: List +) : RecyclerView.ItemDecoration() { + + private val textPaint = Paint().apply { + color = context.getColor(R.color.grey) + textSize = 32f + textAlign = Paint.Align.CENTER + isAntiAlias = true + } + + private val linePaint = Paint().apply { + color = context.getColor(R.color.grey) + strokeWidth = 1f + isAntiAlias = true + } + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + val position = parent.getChildAdapterPosition(view) + if (shouldShowHeader(position)) { + outRect.top = 48 + } + } + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + for (i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + val position = parent.getChildAdapterPosition(child) + + if (shouldShowHeader(position)) { + val dateText = formatDate(items[position].time) + val y = child.top - 16f + + // Linie links + canvas.drawLine(20f, y, parent.width / 2f - 80f, y, linePaint) + // Datum in der Mitte + canvas.drawText(dateText, parent.width / 2f, y + 10f, textPaint) + // Linie rechts + canvas.drawLine(parent.width / 2f + 80f, y, parent.width - 20f, y, linePaint) + } + } + } + + private fun shouldShowHeader(position: Int): Boolean { + if (position <= 0 || position >= items.size) return false + + val currentDay = getDay(items[position].time) + val previousDay = getDay(items[position - 1].time) + return currentDay != previousDay + } + + private fun getDay(timestamp: Long): Long { + val cal = Calendar.getInstance() + cal.timeInMillis = timestamp * 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 + } + + private fun formatDate(timestamp: Long): String { + return DateFormat.getDateFormat(context).format(Date(timestamp * 1000)) + } +} diff --git a/app/src/main/java/it/danieleverducci/lunatracker/repository/LocalSettingsRepository.kt b/app/src/main/java/it/danieleverducci/lunatracker/repository/LocalSettingsRepository.kt index 1462820..196b64e 100644 --- a/app/src/main/java/it/danieleverducci/lunatracker/repository/LocalSettingsRepository.kt +++ b/app/src/main/java/it/danieleverducci/lunatracker/repository/LocalSettingsRepository.kt @@ -15,6 +15,8 @@ class LocalSettingsRepository(val context: Context) { const val SHARED_PREFS_DAV_PASS = "webdav_password" const val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding" const val SHARED_PREFS_SIGNATURE = "signature" + const val SHARED_PREFS_BF_TIMER_START = "bf_timer_start" + const val SHARED_PREFS_BF_TIMER_TYPE = "bf_timer_type" } enum class DATA_REPO {LOCAL_FILE, WEBDAV} val sharedPreferences: SharedPreferences @@ -84,4 +86,25 @@ class LocalSettingsRepository(val context: Context) { return null return arrayOf(url, user, pass) } + + fun saveBreastfeedingTimer(startTime: Long, eventType: String) { + sharedPreferences.edit { + putLong(SHARED_PREFS_BF_TIMER_START, startTime) + putString(SHARED_PREFS_BF_TIMER_TYPE, eventType) + } + } + + fun loadBreastfeedingTimer(): Pair? { + val startTime = sharedPreferences.getLong(SHARED_PREFS_BF_TIMER_START, 0) + val eventType = sharedPreferences.getString(SHARED_PREFS_BF_TIMER_TYPE, null) + if (startTime == 0L || eventType == null) return null + return Pair(startTime, eventType) + } + + fun clearBreastfeedingTimer() { + sharedPreferences.edit { + remove(SHARED_PREFS_BF_TIMER_START) + remove(SHARED_PREFS_BF_TIMER_TYPE) + } + } } \ No newline at end of file diff --git a/app/src/main/java/utils/NumericUtils.kt b/app/src/main/java/utils/NumericUtils.kt index 73b95bc..6e7e5cb 100644 --- a/app/src/main/java/utils/NumericUtils.kt +++ b/app/src/main/java/utils/NumericUtils.kt @@ -79,6 +79,10 @@ class NumericUtils (val context: Context) { LunaEvent.TYPE_WEIGHT -> measurement_unit_weight_base LunaEvent.TYPE_MEDICINE -> measurement_unit_weight_tiny LunaEvent.TYPE_TEMPERATURE -> measurement_unit_temperature_base + LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE, + LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE, + LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE -> + context.getString(R.string.measurement_unit_time_minutes) else -> "" } ) diff --git a/app/src/main/res/layout/breastfeeding_duration_dialog.xml b/app/src/main/res/layout/breastfeeding_duration_dialog.xml new file mode 100644 index 0000000..afebce7 --- /dev/null +++ b/app/src/main/res/layout/breastfeeding_duration_dialog.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/layout/breastfeeding_timer_dialog.xml b/app/src/main/res/layout/breastfeeding_timer_dialog.xml new file mode 100644 index 0000000..6984587 --- /dev/null +++ b/app/src/main/res/layout/breastfeeding_timer_dialog.xml @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 49ed835..01adda6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -94,4 +94,42 @@ 👶 Mein erstes Logbuch Neues Logbuch erstellt: + Stillen läuft + Stopp + Tippe Stopp wenn fertig + Es läuft bereits eine Stillsitzung + Stilldauer + Dauer in Minuten eingeben + + + Spucken + Menge auswählen + Spucken + Baden + 🤮 Spucken + 🛁 Baden + + + Sek. + Sek. + Tag + Tage + Jahr + Jahre + + + Wenig + Normal + Viel + + + Signatur + Füge jedem Event eine Signatur hinzu, die andere sehen können. Nützlich wenn mehrere Personen Events hinzufügen. + Verstecke die Stillbuttons wenn sie nicht benötigt werden. + + + Menge + Notizen + von %s + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3d783db..b603e52 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -93,4 +93,11 @@ 👶 Mon premier carnet de bord Journal ajouté: + Allaitement en cours + Arrêter + Appuyez sur Arrêter quand terminé + Une session d\'allaitement est déjà en cours + Durée d\'allaitement + Entrez la durée en minutes + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e6adaa2..d94bccc 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -93,4 +93,11 @@ 👶 Il mio primo diario Creato nuovo diario: + Allattamento in corso + Stop + Premi Stop quando hai finito + Una sessione di allattamento è già in corso + Durata allattamento + Inserisci la durata in minuti + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c612ca8..acb0826 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -148,4 +148,12 @@ 👶 My first logbook New logbook created: + Breastfeeding in progress + Stop + Tap Stop when finished + A breastfeeding session is already in progress + Breastfeeding duration + Enter the duration in minutes + min + \ No newline at end of file