6 Commits

Author SHA1 Message Date
016dbf330b MainActivity: generate dynamic menu from last two weeks 2026-02-19 11:07:57 +01:00
9b0b489f99 LunaEvent: rework sleep event
Make the UI more flexible and
slightly easier to understand.
2026-02-19 11:07:57 +01:00
5874723e6f MainActivity: rename datepicker 2026-02-19 11:07:57 +01:00
a9c00d2836 layout: replace menu icon with utf8 character
The three dot menu icosn looks odd when stretched
due to the dynamic menu feature. Thus replace it
with the hamburger menu character that looks better
when scaled.
2026-02-19 11:07:57 +01:00
b084f40f39 MainActivity: sort events before saving
Also replace notifyItemInserted since it does not
call the adapter to redraw the row striping when
a new event is added.
2026-02-19 11:07:57 +01:00
3f1afe6c77 StatisticsActivity: rework all statistics
Improve the overall code.
2026-02-19 11:07:53 +01:00
7 changed files with 96 additions and 100 deletions

View File

@@ -162,55 +162,60 @@ class MainActivity : AppCompatActivity() {
compareBy({ -1 * (eventTypeStats[it] ?: 0) }, { it.ordinal }) compareBy({ -1 * (eventTypeStats[it] ?: 0) }, { it.ordinal })
).filter { it != LunaEvent.Type.UNKNOWN } ).filter { it != LunaEvent.Type.UNKNOWN }
fun setupMenu(maxButtonCount: Int, sortedEventTypes: List<LunaEvent.Type>): Int {
val row1 = findViewById<View>(R.id.linear_layout_row1)
val row1Button1 = findViewById<TextView>(R.id.button1_row1)
val row1Button2 = findViewById<TextView>(R.id.button2_row1)
val row2 = findViewById<View>(R.id.linear_layout_row2)
val row2Button1 = findViewById<TextView>(R.id.button1_row2)
val row2Button2 = findViewById<TextView>(R.id.button2_row2)
val row2Button3 = findViewById<TextView>(R.id.button3_row2)
val row3 = findViewById<View>(R.id.linear_layout_row3)
val row3Button1 = findViewById<TextView>(R.id.button1_row3)
val row3Button2 = findViewById<TextView>(R.id.button2_row3)
// hide all rows/buttons (except row 3)
for (view in listOf(row1, row1Button1, row1Button2,
row2, row2Button1, row2Button2, row2Button3,
row3, row3Button1, row3Button2)) {
view.visibility = View.GONE
}
row3.visibility = View.VISIBLE
var showCounter = 0
fun show(vararg tvs: TextView) {
for (tv in tvs) {
val type = sortedEventTypes[showCounter]
tv.text = LunaEvent.getHeaderEmoji(applicationContext, type)
tv.setOnClickListener { showCreateDialog(type) }
tv.visibility = View.VISIBLE
// show parent row
(tv.parent as View).visibility = View.VISIBLE
showCounter += 1
}
}
when (maxButtonCount) {
0 -> { } // ignore - show empty row3
1 -> show(row3Button1)
2 -> show(row3Button1, row3Button2)
3 -> show(row1Button1, row3Button1)
4, 5, 6 -> show(row1Button1, row1Button2, row3Button1, row3Button2)
else -> show(row1Button1, row1Button2, row2Button1, row2Button2, row2Button3, row3Button1, row3Button2)
}
return showCounter
}
val usedEventCount = eventTypeStats.count { it.value > 0 } val usedEventCount = eventTypeStats.count { it.value > 0 }
val maxButtonCount = if (dynamicMenu) { usedEventCount } else { 7 } val maxButtonCount = if (dynamicMenu) { usedEventCount } else { 7 }
val eventsShown = setupMenu(maxButtonCount, eventTypesSorted)
val row1 = findViewById<View>(R.id.linear_layout_row1)
val row1Button1 = findViewById<TextView>(R.id.button1_row1)
val row1Button2 = findViewById<TextView>(R.id.button2_row1)
val row2 = findViewById<View>(R.id.linear_layout_row2)
val row2Button1 = findViewById<TextView>(R.id.button1_row2)
val row2Button2 = findViewById<TextView>(R.id.button2_row2)
val row2Button3 = findViewById<TextView>(R.id.button3_row2)
val row3 = findViewById<View>(R.id.linear_layout_row3)
val row3Button1 = findViewById<TextView>(R.id.button1_row3)
val row3Button2 = findViewById<TextView>(R.id.button2_row3)
// hide all rows/buttons (except row 3)
for (view in listOf(row1, row1Button1, row1Button2,
row2, row2Button1, row2Button2, row2Button3,
row3, row3Button1, row3Button2)) {
view.visibility = View.GONE
}
row3.visibility = View.VISIBLE
var showCounter = 0
fun show(vararg tvs: TextView) {
for (tv in tvs) {
val type = eventTypesSorted[showCounter]
tv.text = LunaEvent.getHeaderEmoji(applicationContext, type)
tv.setOnClickListener { showCreateDialog(type) }
tv.visibility = View.VISIBLE
// show parent row
(tv.parent as View).visibility = View.VISIBLE
showCounter += 1
}
}
when (maxButtonCount) {
0 -> { } // ignore - show empty row3
1 -> show(row3Button1)
2 -> show(row3Button1, row3Button2)
3 -> show(row1Button1, row3Button1)
4, 5, 6 -> show(row1Button1, row1Button2, row3Button1, row3Button2)
else -> show(row1Button1, row1Button2, row2Button1, row2Button2, row2Button3, row3Button1, row3Button2)
}
// store left over events for popup menu // store left over events for popup menu
currentPopupItems = eventTypesSorted.subList(showCounter, eventTypesSorted.size) currentPopupItems = eventTypesSorted.subList(eventsShown, eventTypesSorted.size)
} }
override fun onStart() { override fun onStart() {
@@ -272,9 +277,6 @@ class MainActivity : AppCompatActivity() {
numberPicker.wrapSelectorWheel = false numberPicker.wrapSelectorWheel = false
numberPicker.value = event.quantity / 10 numberPicker.value = event.quantity / 10
val numberPickerUnit = dialogView.findViewById<TextView>(R.id.dialog_number_picker_unit)
numberPickerUnit.text = NumericUtils(this).measurement_unit_liquid_base
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker) val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV) val pickedTime = dateTimePicker(event.time, dateTV)
@@ -421,11 +423,11 @@ class MainActivity : AppCompatActivity() {
return dateTime return dateTime
} }
fun addDurationEvent(event: LunaEvent) { fun addSleepEvent(event: LunaEvent) {
askDurationEvent(event, true) { saveEvent(event) } askSleepValue(event, true) { saveEvent(event) }
} }
fun askDurationEvent(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) { fun askSleepValue(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_duration, null) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_duration, null)
d.setTitle(event.getDialogTitle(this)) d.setTitle(event.getDialogTitle(this))
@@ -437,7 +439,7 @@ class MainActivity : AppCompatActivity() {
val dateDelimiter = dialogView.findViewById<TextView>(R.id.dialog_date_range_delimiter) val dateDelimiter = dialogView.findViewById<TextView>(R.id.dialog_date_range_delimiter)
val durationButtons = dialogView.findViewById<LinearLayout>(R.id.duration_buttons) val durationButtons = dialogView.findViewById<LinearLayout>(R.id.duration_buttons)
val durationNowButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_now) val durationNowButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_now)
val durationClearButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_clear) val durationAsleepButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_asleep)
val durationMinus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_minus5) val durationMinus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_minus5)
val durationPlus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_plus5) val durationPlus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_plus5)
@@ -445,8 +447,8 @@ class MainActivity : AppCompatActivity() {
val invalidDurationTextColor = ContextCompat.getColor(this, R.color.danger) val invalidDurationTextColor = ContextCompat.getColor(this, R.color.danger)
// in seconds // in seconds
var durationStart = event.getStartTime() var sleepBegin = event.time
var durationEnd = event.getEndTime() var sleepEnd = event.time + event.quantity
fun isValidTime(timeUnix: Long): Boolean { fun isValidTime(timeUnix: Long): Boolean {
val now = System.currentTimeMillis() / 1000 val now = System.currentTimeMillis() / 1000
@@ -457,43 +459,46 @@ class MainActivity : AppCompatActivity() {
return (timeBeginUnix <= timeEndUnix) && (timeEndUnix - timeBeginUnix) < (24 * 60 * 60) return (timeBeginUnix <= timeEndUnix) && (timeEndUnix - timeBeginUnix) < (24 * 60 * 60)
} }
// prevent printing of seconds
fun adjustToMinute(unixTime: Long): Long {
return unixTime - (unixTime % 60)
}
fun updateFields() { fun updateFields() {
datePickerBegin.text = DateUtils.formatDateTime(durationStart) datePickerBegin.text = DateUtils.formatDateTime(sleepBegin)
datePickerEnd.text = DateUtils.formatDateTime(durationEnd) datePickerEnd.text = DateUtils.formatDateTime(sleepEnd)
durationTextView.setTextColor(currentDurationTextColor) durationTextView.setTextColor(currentDurationTextColor)
val duration = durationEnd - durationStart val duration = sleepEnd - sleepBegin
if (duration == 0L) { if (duration == 0L) {
// event is ongoing // baby is sleeping
durationTextView.text = "💤" durationTextView.text = "💤"
dateDelimiter.visibility = View.GONE
datePickerEnd.visibility = View.GONE
} else { } else {
durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration) durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration)
if (!isValidTimeSpan(durationStart, durationEnd)) { if (!isValidTimeSpan(sleepBegin, sleepEnd)) {
durationTextView.setTextColor(invalidDurationTextColor) durationTextView.setTextColor(invalidDurationTextColor)
} }
dateDelimiter.visibility = View.VISIBLE
datePickerEnd.visibility = View.VISIBLE
} }
datePickerBegin.setTextColor(if (isValidTime(durationStart)) { currentDurationTextColor } else { invalidDurationTextColor }) datePickerBegin.setTextColor(if (isValidTime(sleepBegin)) { currentDurationTextColor } else { invalidDurationTextColor })
datePickerEnd.setTextColor(if (isValidTime(durationEnd)) { currentDurationTextColor } else { invalidDurationTextColor }) datePickerEnd.setTextColor(if (isValidTime(sleepEnd)) { currentDurationTextColor } else { invalidDurationTextColor })
} }
val pickedDateTimeBegin = dateTimePicker(event.time, datePickerBegin) { pickedTime: Long -> val pickedDateTimeBegin = dateTimePicker(event.time, datePickerBegin) { time: Long ->
durationStart = pickedTime sleepBegin = adjustToMinute(time)
if (datePickerEnd.visibility == View.GONE) {
durationEnd = pickedTime
}
updateFields() updateFields()
} }
val pickedDateTimeEnd = dateTimePicker(event.time + event.quantity, datePickerEnd) { pickedTime: Long -> val pickedDateTimeEnd = dateTimePicker(event.time + event.quantity, datePickerEnd) { time: Long ->
durationEnd = pickedTime sleepEnd = adjustToMinute(time)
updateFields() updateFields()
} }
sleepBegin = adjustToMinute(pickedDateTimeBegin.time.time / 1000)
sleepEnd = adjustToMinute(pickedDateTimeEnd.time.time / 1000)
updateFields()
if (showTime) { if (showTime) {
dateDelimiter.visibility = View.GONE dateDelimiter.visibility = View.GONE
datePickerEnd.visibility = View.GONE datePickerEnd.visibility = View.GONE
@@ -508,35 +513,31 @@ class MainActivity : AppCompatActivity() {
d.setMessage(event.getDialogMessage(this)) d.setMessage(event.getDialogMessage(this))
} }
durationStart = pickedDateTimeBegin.time.time / 1000
durationEnd = pickedDateTimeEnd.time.time / 1000
updateFields()
durationMinus5Button.setOnClickListener { durationMinus5Button.setOnClickListener {
durationEnd = (durationEnd - 300).coerceAtLeast(durationStart) sleepEnd = (sleepEnd - 300).coerceAtLeast(sleepBegin)
updateFields() updateFields()
} }
durationPlus5Button.setOnClickListener { durationPlus5Button.setOnClickListener {
durationEnd = (durationEnd + 300).coerceAtLeast(durationStart) sleepEnd = (sleepEnd + 300).coerceAtLeast(sleepBegin)
updateFields() updateFields()
} }
durationClearButton.setOnClickListener { durationAsleepButton.setOnClickListener {
durationEnd = durationStart sleepEnd = sleepBegin
updateFields() updateFields()
} }
durationNowButton.setOnClickListener { durationNowButton.setOnClickListener {
durationEnd = System.currentTimeMillis() / 1000 val now = System.currentTimeMillis() / 1000
sleepEnd = adjustToMinute(now)
updateFields() updateFields()
} }
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
if (isValidTime(durationStart) && isValidTime(durationEnd) && isValidTimeSpan(durationStart, durationEnd)) { if (isValidTime(sleepBegin) && isValidTime(sleepEnd) && isValidTimeSpan(sleepBegin, sleepEnd)) {
event.time = durationStart event.time = sleepBegin
event.quantity = (durationEnd - durationStart).toInt() event.quantity = (sleepEnd - sleepBegin).toInt()
onPositive() onPositive()
} else { } else {
Toast.makeText(this, R.string.toast_date_error, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.toast_date_error, Toast.LENGTH_SHORT).show()
@@ -882,7 +883,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.PUKE -> askAmountValue(event, false, updateValues) LunaEvent.Type.PUKE -> askAmountValue(event, false, updateValues)
LunaEvent.Type.TEMPERATURE -> askTemperatureValue(event, false, updateValues) LunaEvent.Type.TEMPERATURE -> askTemperatureValue(event, false, updateValues)
LunaEvent.Type.NOTE -> askNotes(event, false, updateValues) LunaEvent.Type.NOTE -> askNotes(event, false, updateValues)
LunaEvent.Type.SLEEP -> askDurationEvent(event, false, updateValues) LunaEvent.Type.SLEEP -> askSleepValue(event, false, updateValues)
else -> { else -> {
Log.w(TAG, "Unexpected type: ${event.type}") Log.w(TAG, "Unexpected type: ${event.type}")
} }
@@ -1309,7 +1310,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.FOOD -> addNoteEvent(event) LunaEvent.Type.FOOD -> addNoteEvent(event)
LunaEvent.Type.PUKE -> addAmountEvent(event) LunaEvent.Type.PUKE -> addAmountEvent(event)
LunaEvent.Type.BATH -> addPlainEvent(event) LunaEvent.Type.BATH -> addPlainEvent(event)
LunaEvent.Type.SLEEP -> addDurationEvent(event) LunaEvent.Type.SLEEP -> addSleepEvent(event)
LunaEvent.Type.UNKNOWN -> {} // ignore LunaEvent.Type.UNKNOWN -> {} // ignore
} }
} }

View File

@@ -672,6 +672,7 @@ class StatisticsActivity : AppCompatActivity() {
GraphType.SLEEP_PATTERN -> LunaEvent.Type.SLEEP GraphType.SLEEP_PATTERN -> LunaEvent.Type.SLEEP
} }
val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time } val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time }
if (events.isEmpty()) { if (events.isEmpty()) {

View File

@@ -174,7 +174,7 @@ class LunaEvent: Comparable<LunaEvent> {
Type.DIAPERCHANGE_PEE, Type.DIAPERCHANGE_PEE,
Type.PUKE -> R.string.log_amount_dialog_description Type.PUKE -> R.string.log_amount_dialog_description
Type.WEIGHT -> R.string.log_weight_dialog_description Type.WEIGHT -> R.string.log_weight_dialog_description
Type.SLEEP -> R.string.log_duration_dialog_description Type.SLEEP -> R.string.log_sleep_dialog_description
else -> R.string.log_unknown_dialog_description else -> R.string.log_unknown_dialog_description
} }
) )

View File

@@ -9,15 +9,10 @@ import java.util.Date
class DateUtils { class DateUtils {
companion object { companion object {
/** /**
* Format time duration in seconds as e.g. "2 hours, 1 min", rounded to minutes. * Format time duration in seconds as e.g. "2 hours, 1 min".
* Used for the duration to the next/previous event in the event details dialog. * Used for the duration to the next/previous event in the event details dialog.
*/ */
fun formatTimeDuration(context: Context, secondsDiff: Long): String { fun formatTimeDuration(context: Context, secondsDiff: Long): String {
val adjusted = (secondsDiff + 30) - (secondsDiff + 30) % 60
return formatTimeDurationExact(context, adjusted)
}
fun formatTimeDurationExact(context: Context, secondsDiff: Long): String {
var seconds = secondsDiff var seconds = secondsDiff
val years = (seconds / (365 * 24 * 60 * 60F)).toLong() val years = (seconds / (365 * 24 * 60 * 60F)).toLong()

View File

@@ -18,7 +18,6 @@
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/dialog_number_picker_unit"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"

View File

@@ -39,11 +39,11 @@
android:text="@string/dialog_duration_button_now"/> android:text="@string/dialog_duration_button_now"/>
<Button <Button
android:id="@+id/dialog_date_duration_clear" android:id="@+id/dialog_date_duration_asleep"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/dialog_duration_button_clear"/> android:text="@string/dialog_duration_button_asleep"/>
<Button <Button
android:id="@+id/dialog_date_duration_plus5" android:id="@+id/dialog_date_duration_plus5"

View File

@@ -130,7 +130,7 @@
<string name="log_temperature_dialog_description">Select the temperature:</string> <string name="log_temperature_dialog_description">Select the temperature:</string>
<string name="log_unknown_dialog_description"></string> <string name="log_unknown_dialog_description"></string>
<string name="log_weight_dialog_description">Insert the weight:</string> <string name="log_weight_dialog_description">Insert the weight:</string>
<string name="log_duration_dialog_description">Set duration:</string> <string name="log_sleep_dialog_description">Set sleep duration:</string>
<string name="measurement_unit_liquid_base_metric" translatable="false">ml</string> <string name="measurement_unit_liquid_base_metric" translatable="false">ml</string>
<string name="measurement_unit_weight_base_metric" translatable="false">g</string> <string name="measurement_unit_weight_base_metric" translatable="false">g</string>
@@ -160,9 +160,9 @@
<string name="dialog_event_detail_notes">Notes</string> <string name="dialog_event_detail_notes">Notes</string>
<string name="dialog_event_detail_signature">by %s</string> <string name="dialog_event_detail_signature">by %s</string>
<string name="dialog_duration_button_clear">Clear</string> <string name="dialog_duration_button_asleep">asleep</string>
<string name="dialog_duration_button_minus_5">-5 min</string> <string name="dialog_duration_button_minus_5">-5 min</string>
<string name="dialog_duration_button_now">Now</string> <string name="dialog_duration_button_now">now</string>
<string name="dialog_duration_button_plus_5">+5 min</string> <string name="dialog_duration_button_plus_5">+5 min</string>
<string name="dialog_add_logbook_title">Add logbook</string> <string name="dialog_add_logbook_title">Add logbook</string>