7 Commits

Author SHA1 Message Date
a4b5ae7cd0 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-01 23:00:52 +01:00
bed3350113 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-01 23:00:52 +01:00
758f37b510 StatisticsActivity: rework all statistics
Improve the overall code.
2026-02-01 23:00:48 +01:00
c8c678b294 NumericUtils: remove possible trailing whitespace 2026-01-23 06:46:11 +01:00
45e798ac3b MainActivity: do not switch logbook on reload 2026-01-23 06:46:11 +01:00
a06264091b LunaEvent: reorganize event text getters
Use method names that better reflect
the use of the returned text.
2026-01-23 06:46:11 +01:00
1e82c94d83 MainAcitivty: add dynamic header setting
The setting allows to build the menu and
popup list to be populated by the frequency
of events that has been created.
This also makes the 'no breastfeeding' setting irrelevant.
2026-01-23 06:46:11 +01:00
9 changed files with 384 additions and 290 deletions

View File

@@ -148,23 +148,19 @@ class MainActivity : AppCompatActivity() {
val eventTypeStats = mutableMapOf<LunaEvent.Type, Int>()
if (dynamicMenu) {
// populate frequency map from all events of the last two weeks
val lastWeekTime = (System.currentTimeMillis() / 1000) - (14 * 24 * 60 * 60)
allEvents.forEach {
if (it.time > lastWeekTime) {
val sampleSize = 100
// populate frequency map from first 100 events
allEvents.take(sampleSize.coerceAtMost(allEvents.size)).forEach {
eventTypeStats[it.type] = 1 + (eventTypeStats[it.type] ?: 0)
}
}
}
// sort all event types by frequency and ordinal
// sort all event types by frequency or ordinal
val eventTypesSorted = LunaEvent.Type.entries.toList().sortedWith(
compareBy({ -1 * (eventTypeStats[it] ?: 0) }, { it.ordinal })
).filter { it != LunaEvent.Type.UNKNOWN }
val usedEventCount = eventTypeStats.count { it.value > 0 }
val maxButtonCount = if (dynamicMenu) { usedEventCount } else { 7 }
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)
@@ -190,7 +186,7 @@ class MainActivity : AppCompatActivity() {
fun show(vararg tvs: TextView) {
for (tv in tvs) {
val type = eventTypesSorted[showCounter]
val type = sortedEventTypes[showCounter]
tv.text = LunaEvent.getHeaderEmoji(applicationContext, type)
tv.setOnClickListener { showCreateDialog(type) }
tv.visibility = View.VISIBLE
@@ -209,8 +205,15 @@ class MainActivity : AppCompatActivity() {
else -> show(row1Button1, row1Button2, row2Button1, row2Button2, row2Button3, row3Button1, row3Button2)
}
return showCounter
}
val usedEventCount = eventTypeStats.count { it.value > 0 }
val maxButtonCount = if (dynamicMenu) { usedEventCount } else { 7 }
val eventsShown = setupMenu(maxButtonCount, eventTypesSorted)
// store left over events for popup menu
currentPopupItems = eventTypesSorted.subList(showCounter, eventTypesSorted.size)
currentPopupItems = eventTypesSorted.subList(eventsShown, eventTypesSorted.size)
}
override fun onStart() {
@@ -272,11 +275,8 @@ class MainActivity : AppCompatActivity() {
numberPicker.wrapSelectorWheel = false
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 pickedTime = dateTimePicker(event.time, dateTV)
val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
@@ -314,7 +314,7 @@ class MainActivity : AppCompatActivity() {
weightET.setText(event.quantity.toString())
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV)
val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
@@ -365,7 +365,7 @@ class MainActivity : AppCompatActivity() {
}
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV)
val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
}
@@ -389,8 +389,7 @@ class MainActivity : AppCompatActivity() {
alertDialog.show()
}
// Pick a date/time.
fun dateTimePicker(time: Long, dateTextView: TextView, onChange: (Long) -> Unit = {}): Calendar {
fun datePickerHelper(time: Long, dateTextView: TextView, onChange: (Long) -> Unit = {}): Calendar {
dateTextView.text = DateUtils.formatDateTime(time)
val dateTime = Calendar.getInstance()
@@ -421,122 +420,86 @@ class MainActivity : AppCompatActivity() {
return dateTime
}
fun addDurationEvent(event: LunaEvent) {
askDurationEvent(event, true) { saveEvent(event) }
fun addSleepEvent(event: LunaEvent) {
askSleepValue(event, true) { saveEvent(event) }
}
fun askDurationEvent(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) {
fun askSleepValue(event: LunaEvent, hideDurationButtons: Boolean, onPositive: () -> Unit) {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_duration, null)
d.setTitle(event.getDialogTitle(this))
d.setMessage(event.getDialogMessage(this))
d.setView(dialogView)
val durationTextView = dialogView.findViewById<TextView>(R.id.dialog_date_duration)
val datePickerBegin = dialogView.findViewById<TextView>(R.id.dialog_date_picker_begin)
val datePickerEnd = dialogView.findViewById<TextView>(R.id.dialog_date_picker_end)
val dateDelimiter = dialogView.findViewById<TextView>(R.id.dialog_date_range_delimiter)
val datePicker = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val durationButtons = dialogView.findViewById<LinearLayout>(R.id.duration_buttons)
val durationNowButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_now)
val durationClearButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_clear)
val durationMinus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_minus5)
val durationPlus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_plus5)
val currentDurationTextColor = durationTextView.currentTextColor
val invalidDurationTextColor = ContextCompat.getColor(this, R.color.danger)
// in seconds
var durationStart = event.getStartTime()
var durationEnd = event.getEndTime()
var duration = event.quantity
fun isValidTime(timeUnix: Long): Boolean {
fun isValidTime(timeSeconds: Long, durationSeconds: Int): Boolean {
val now = System.currentTimeMillis() / 1000
return timeUnix in 1..now
return (timeSeconds + durationSeconds) <= now && durationSeconds < (24 * 60 * 60)
}
fun isValidTimeSpan(timeBeginUnix: Long, timeEndUnix: Long): Boolean {
return (timeBeginUnix <= timeEndUnix) && (timeEndUnix - timeBeginUnix) < (24 * 60 * 60)
}
fun updateFields() {
datePickerBegin.text = DateUtils.formatDateTime(durationStart)
datePickerEnd.text = DateUtils.formatDateTime(durationEnd)
val onDateChange = { time: Long ->
durationTextView.setTextColor(currentDurationTextColor)
val duration = durationEnd - durationStart
if (duration == 0L) {
// event is ongoing
if (duration == 0) {
// baby is sleeping
durationTextView.text = "💤"
dateDelimiter.visibility = View.GONE
datePickerEnd.visibility = View.GONE
} else {
durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration)
if (!isValidTimeSpan(durationStart, durationEnd)) {
durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration.toLong())
if (!isValidTime(time, duration)) {
durationTextView.setTextColor(invalidDurationTextColor)
}
dateDelimiter.visibility = View.VISIBLE
datePickerEnd.visibility = View.VISIBLE
}
}
datePickerBegin.setTextColor(if (isValidTime(durationStart)) { currentDurationTextColor } else { invalidDurationTextColor })
datePickerEnd.setTextColor(if (isValidTime(durationEnd)) { currentDurationTextColor } else { invalidDurationTextColor })
}
val pickedDateTime = datePickerHelper(event.time, datePicker, onDateChange)
val pickedDateTimeBegin = dateTimePicker(event.time, datePickerBegin) { pickedTime: Long ->
durationStart = pickedTime
if (datePickerEnd.visibility == View.GONE) {
durationEnd = pickedTime
}
updateFields()
}
onDateChange(pickedDateTime.time.time / 1000)
val pickedDateTimeEnd = dateTimePicker(event.time + event.quantity, datePickerEnd) { pickedTime: Long ->
durationEnd = pickedTime
updateFields()
}
if (showTime) {
dateDelimiter.visibility = View.GONE
datePickerEnd.visibility = View.GONE
durationTextView.visibility = View.GONE
if (hideDurationButtons) {
durationButtons.visibility = View.GONE
//d.setMessage("")
d.setMessage(getString(R.string.log_sleep_dialog_description_start))
} else {
dateDelimiter.visibility = View.VISIBLE
datePickerEnd.visibility = View.VISIBLE
durationTextView.visibility = View.VISIBLE
durationButtons.visibility = View.VISIBLE
d.setMessage(event.getDialogMessage(this))
fun adjust(minutes: Int) {
duration += minutes * 60
if (duration < 0) {
duration = 0
}
onDateChange(pickedDateTime.time.time / 1000)
}
durationStart = pickedDateTimeBegin.time.time / 1000
durationEnd = pickedDateTimeEnd.time.time / 1000
updateFields()
durationMinus5Button.setOnClickListener {
durationEnd = (durationEnd - 300).coerceAtLeast(durationStart)
updateFields()
}
durationPlus5Button.setOnClickListener {
durationEnd = (durationEnd + 300).coerceAtLeast(durationStart)
updateFields()
}
durationClearButton.setOnClickListener {
durationEnd = durationStart
updateFields()
}
durationMinus5Button.setOnClickListener { adjust(-5) }
durationPlus5Button.setOnClickListener { adjust(5) }
durationNowButton.setOnClickListener {
durationEnd = System.currentTimeMillis() / 1000
updateFields()
val now = System.currentTimeMillis() / 1000
val start = pickedDateTime.time.time / 1000
if (now > start) {
duration = (now - start).toInt()
duration -= duration % 60 // prevent printing of seconds
onDateChange(pickedDateTime.time.time / 1000)
}
}
}
d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
if (isValidTime(durationStart) && isValidTime(durationEnd) && isValidTimeSpan(durationStart, durationEnd)) {
event.time = durationStart
event.quantity = (durationEnd - durationStart).toInt()
val time = pickedDateTime.time.time / 1000
if (isValidTime(time, duration)) {
event.time = time
event.quantity = duration
onPositive()
} else {
Toast.makeText(this, R.string.toast_date_error, Toast.LENGTH_SHORT).show()
@@ -574,7 +537,7 @@ class MainActivity : AppCompatActivity() {
spinner.setSelection(event.quantity.coerceIn(0, spinner.count - 1))
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV)
val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
}
@@ -607,7 +570,7 @@ class MainActivity : AppCompatActivity() {
d.setView(dialogView)
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedDateTime = dateTimePicker(event.time, dateTV)
val pickedDateTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
}
@@ -642,7 +605,7 @@ class MainActivity : AppCompatActivity() {
val qtyET = dialogView.findViewById<EditText>(R.id.notes_qty_edittext)
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV)
val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) {
dateTV.visibility = View.GONE
@@ -854,7 +817,7 @@ class MainActivity : AppCompatActivity() {
quantityTextView.text = NumericUtils(this).formatEventQuantity(event)
notesTextView.text = event.notes
if (event.type == LunaEvent.Type.SLEEP && event.quantity > 0) {
dateEndTextView.text = DateUtils.formatDateTime(event.getEndTime())
dateEndTextView.text = DateUtils.formatDateTime(event.time + event.quantity)
dateEndTextView.visibility = View.VISIBLE
} else {
dateEndTextView.visibility = View.GONE
@@ -868,7 +831,7 @@ class MainActivity : AppCompatActivity() {
}
updateValues()
dateTimePicker(event.time, dateTextView, { newTime: Long ->
datePickerHelper(event.time, dateTextView, { newTime: Long ->
event.time = newTime
updateValues()
})
@@ -882,7 +845,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.PUKE -> askAmountValue(event, false, updateValues)
LunaEvent.Type.TEMPERATURE -> askTemperatureValue(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 -> {
Log.w(TAG, "Unexpected type: ${event.type}")
}
@@ -912,7 +875,7 @@ class MainActivity : AppCompatActivity() {
val previousEvent = getPreviousSameEvent(event, allEvents)
if (previousEvent != null) {
val emoji = previousEvent.getHeaderEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, event.getStartTime() - previousEvent.getEndTime())
val time = DateUtils.formatTimeDuration(applicationContext, event.time - previousEvent.time)
previousTextView.text = String.format("⬅️ %s %s", emoji, time)
previousTextView.setOnClickListener {
alertDialog.cancel()
@@ -927,7 +890,7 @@ class MainActivity : AppCompatActivity() {
val nextEvent = getNextSameEvent(event, allEvents)
if (nextEvent != null) {
val emoji = nextEvent.getHeaderEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.getStartTime() - event.getEndTime())
val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.time - event.time)
nextTextView.text = String.format("%s %s ➡️", time, emoji)
nextTextView.setOnClickListener {
alertDialog.cancel()
@@ -1309,7 +1272,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.FOOD -> addNoteEvent(event)
LunaEvent.Type.PUKE -> addAmountEvent(event)
LunaEvent.Type.BATH -> addPlainEvent(event)
LunaEvent.Type.SLEEP -> addDurationEvent(event)
LunaEvent.Type.SLEEP -> addSleepEvent(event)
LunaEvent.Type.UNKNOWN -> {} // ignore
}
}

View File

@@ -1,6 +1,5 @@
package it.danieleverducci.lunatracker
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.Log
@@ -12,7 +11,6 @@ import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.toColorInt
import com.github.mikephil.charting.animation.ChartAnimator
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.components.YAxis
import com.github.mikephil.charting.data.BarData
@@ -21,10 +19,8 @@ import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.formatter.ValueFormatter
import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet
import com.github.mikephil.charting.listener.OnChartValueSelectedListener
import com.github.mikephil.charting.renderer.HorizontalBarChartRenderer
import com.github.mikephil.charting.utils.ViewPortHandler
import it.danieleverducci.lunatracker.entities.LunaEvent
import utils.DateUtils
import utils.NumericUtils
@@ -32,6 +28,7 @@ import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@@ -51,7 +48,8 @@ class StatisticsActivity : AppCompatActivity() {
BOTTLE_SUM,
SLEEP_SUM,
SLEEP_EVENTS,
SLEEP_PATTERN
SLEEP_PATTERN,
MEDICINE_EVENTS
}
enum class TimeRange {
@@ -141,6 +139,124 @@ class StatisticsActivity : AppCompatActivity() {
}
}
fun resetBarChart() {
barChart.fitScreen()
barChart.data?.clearValues()
barChart.xAxis.valueFormatter = null
barChart.notifyDataSetChanged()
barChart.clear()
barChart.invalidate()
/*
barChart.setBackgroundColor(Color.WHITE)
//barChart.description.text = logbookName
barChart.setDrawValueAboveBar(false)
barChart.axisLeft.setAxisMinimum(0F)
barChart.axisLeft.setDrawGridLines(false)
barChart.axisLeft.setDrawLabels(false)
barChart.axisRight.setDrawGridLines(false)
barChart.axisRight.setDrawLabels(false)
//barChart.xAxis.setDrawGridLines(true)
barChart.xAxis.setDrawLabels(true)
barChart.xAxis.setDrawAxisLine(false)
barChart.xAxis.setCenterAxisLabels(true)
barChart.setScaleEnabled(false)
//barChart.isScaleXEnabled = false
//barChart.isScaleYEnabled = true
*/
// for debugging
Log.d(TAG, "resetBarChart; barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}, barChart.visibleXRange: ${barChart.visibleXRange}, barChart.xAxis.isCenterAxisLabelsEnabled: ${barChart.xAxis.isCenterAxisLabelsEnabled}, barChart.isAutoScaleMinMaxEnabled: ${barChart.isAutoScaleMinMaxEnabled}, barChart.isScaleXEnabled: ${barChart.isScaleXEnabled}, barChart.isScaleYEnabled: ${barChart.isScaleYEnabled}")
}
fun showMedicineBarGraph(state: GraphState) {
val values = HashMap<String, ArrayList<BarEntry>>()
val days = state.endSpan - state.startSpan + 1
for (event in state.events) {
val index = unixToSpan(event.time) - state.startSpan
val key = event.notes.trim().lowercase()
val array = values.getOrPut(key) {
// create initial array with 0
ArrayList(List(days) { BarEntry(it.toFloat(), 0F) })
}
array[index].y += 1F
}
Log.d(TAG, "values.size: ${values.size}, days: $days")
for ((key, value) in values) {
Log.d(TAG, "key: $key, value.size: ${value.size} ,value: ${value.joinToString { it.y.toLong().toString() }}")
}
// make sure legend names are not too long
fun shorten(notes: String): String {
return if (notes.length > 16) {
notes.take(13) + "..."
} else {
notes
}
}
fun chooseColor(notes: String): Int {
return (abs(notes.hashCode()) * 16777215) or (0xFF shl 24)
}
val sets = arrayListOf<IBarDataSet>()
for ((key, array) in values.entries) {
val description = shorten(key)
Log.d(TAG, "key: $key")
val barDataSet = BarDataSet(array, description)
barDataSet.color = chooseColor(key)
sets.add(barDataSet)
}
val data = BarData(sets)
//data.groupBars(0F, 0.2F, 0.1F);
//data.setValueTextSize(12f)
//data.barWidth = 1F
//data.groupBars(0F, 1F, 1F)
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return if (value == 0F) {
""
} else {
value.toInt().toString()
}
}
})
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) {
Log.d(TAG, "onValueSelected ${e == null} ${h == null}")
if (e == null || h == null) {
return
}
val index = e.x.toInt()
if (index !in 0..values.size) {
return
}
Log.d(TAG, "index: $index")
}
override fun onNothingSelected() {}
})
data.setValueTextSize(12f)
barChart.setData(data)
barChart.legend.isEnabled = true
val valueCount = min(days, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.invalidate()
}
data class SleepRange(val start: Long, var end: Long)
fun toSleepRanges(events: List<LunaEvent>): ArrayList<SleepRange> {
@@ -181,11 +297,15 @@ class StatisticsActivity : AppCompatActivity() {
val values = ArrayList<BarEntry>()
val stack = ArrayList(List(state.endSpan - state.startSpan + 1) { IntArray(24 * 60 * 60 / SLEEP_PATTERN_GRANULARITY) })
Log.d(TAG, "stack.size: ${stack.size}, array.size: ${stack[0].size}, dayCounter.daysWithData.size: ${state.dayCounter.daysWithData.size}")
fun stackValuePattern(index: Int, spanBegin: Long, spanEnd: Long, begin: Long, end: Long) {
val beginDays = unixToDays(begin)
val endDays = unixToDays(end)
var mid = begin
//Log.d(TAG, "stackValuePattern: ${beginDays}..${endDays}")
for (i in beginDays..endDays) {
// i is the days/weeks/months since unix epoch
val dayBegin = daysToUnix(i)
@@ -198,7 +318,8 @@ class StatisticsActivity : AppCompatActivity() {
assert(sleepBegin <= sleepEnd)
val iBegin = (sleepBegin - dayBegin) / SLEEP_PATTERN_GRANULARITY
val iEnd = iBegin + (sleepEnd - sleepBegin) / SLEEP_PATTERN_GRANULARITY
for (j in iBegin..<iEnd) {
//Log.d(TAG, "index: $index, iBegin: $iBegin, iEnd: $iEnd, dayBegin: ${Date(dayBegin * 1000)}, dayEnd: ${Date(dayEnd * 1000)}, sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}")
for (j in iBegin..iEnd) {
stack[index][j.toInt()] += 1
}
}
@@ -215,14 +336,17 @@ class StatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix)
var mid = startUnix
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})")
for (i in begIndex..endIndex) {
// i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}")
val sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan
val duration = sleepEnd - sleepBegin
//Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}")
state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
@@ -249,6 +373,7 @@ class StatisticsActivity : AppCompatActivity() {
for ((index, dayArray) in stack.withIndex()) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
//Log.d(TAG, "index: $index: daysWithData: $daysWithData, dayArray: ${dayArray.joinToString { it.toString() }}")
val vals = ArrayList<Float>()
var prevIndex = -1 // time slot index
@@ -270,10 +395,14 @@ class StatisticsActivity : AppCompatActivity() {
allColors.add(mapColor(prevValue, daysWithData))
}
assert(values.size == index)
values.add(BarEntry(values.size.toFloat(), vals.toFloatArray()))
//Log.d(TAG, "Range $index, vals: ${vals.joinToString { it.toInt().toString() }}") //, allColors: ${allColors.joinToString { it.toString() }}")
Log.d(TAG, "index: ${index.toFloat()}")
values.add(BarEntry(index.toFloat(), vals.toFloatArray()))
}
//Log.d(TAG, "daysWithData: ${state.dayCounter.daysWithData.joinToString()}")
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) {
if (e == null || h == null) {
@@ -297,6 +426,7 @@ class StatisticsActivity : AppCompatActivity() {
val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + index)
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, x: ${e.x.toInt()}, dayStartUnix: ${Date(dayStartUnix * 1000)}")
val startSeconds =
SLEEP_PATTERN_GRANULARITY * value.yVals.sliceArray(0..<h.stackIndex)
.fold(0) { acc, y -> acc + y.toInt() }
@@ -309,7 +439,10 @@ class StatisticsActivity : AppCompatActivity() {
format.format((dayStartUnix + startSeconds) * 1000).toString()
val endTimeString =
format.format((dayStartUnix + endSeconds) * 1000).toString()
val durationString = DateUtils.formatTimeDuration(applicationContext, durationSeconds.toLong())
val durationString = NumericUtils(applicationContext).formatEventQuantity(
LunaEvent.Type.SLEEP,
durationSeconds
)
val daysWithData =
stack[e.x.toInt()][startSeconds / SLEEP_PATTERN_GRANULARITY]
@@ -338,22 +471,49 @@ class StatisticsActivity : AppCompatActivity() {
})
val set1 = BarDataSet(values, "")
val data = BarData(set1)
set1.colors = allColors
set1.setDrawValues(false) // usually too many values
set1.isHighlightEnabled = true
set1.setDrawIcons(false)
val data = BarData(set1)
data.setValueTextSize(12f)
//barChart.legend.isEnabled = false
/*
val valueCount = min(values.size, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
*/
Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}, barChart.visibleXRange: ${barChart.visibleXRange}, barChart.xAxis.isCenterAxisLabelsEnabled: ${barChart.xAxis.isCenterAxisLabelsEnabled}, barChart.isAutoScaleMinMaxEnabled: ${barChart.isAutoScaleMinMaxEnabled}, barChart.isScaleXEnabled: ${barChart.isScaleXEnabled}, barChart.isScaleYEnabled: ${barChart.isScaleYEnabled}")
//barChart.minimumWidth
//barChart.isAutoScaleMinMaxEnabled = true
//barChart.setScaleEnabled(true)
//barChart.fitScreen()
//debugBarValues(values)
//barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
barChart.setData(data)
Log.d(TAG, "xChartMax: ${barChart.xChartMax}")
barChart.centerViewTo(barChart.xChartMax, 0F, YAxis.AxisDependency.RIGHT)
// does not work quite right yet
barChart.legend.isEnabled = false
val valueCount = min(values.size, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate()
//barChart.moveViewToX(77F) //values.lastOrNull()!!.x)
}
// Sleep pattern bars that do not use time slots.
@@ -383,8 +543,7 @@ class StatisticsActivity : AppCompatActivity() {
// update value
val newYVals = appendToFloatArray(yVals, awakeDuration.toFloat(), sleepDuration.toFloat())
assert(index == x.toInt())
values[index] = BarEntry(index.toFloat(), newYVals)
values[index] = BarEntry(x, newYVals)
}
for (range in ranges) {
@@ -396,14 +555,17 @@ class StatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix)
var mid = startUnix
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})")
for (i in begIndex..endIndex) {
// i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}")
val sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan
val duration = sleepEnd - sleepBegin
//Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}")
state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
@@ -412,13 +574,16 @@ class StatisticsActivity : AppCompatActivity() {
}
}
// awake phase color is transparent
val set1 = BarDataSet(values, "")
val data = BarData(set1)
// awake phase color is transparent
set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt())
set1.setDrawValues(false) // usually too many values
set1.setDrawIcons(false)
set1.isHighlightEnabled = true
//barChart.xAxis.setCenterAxisLabels(true)
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) {
if (e == null || h == null) {
@@ -441,7 +606,7 @@ class StatisticsActivity : AppCompatActivity() {
}
val duration = value.yVals[h.stackIndex].toInt()
val durationString = DateUtils.formatTimeDuration(applicationContext, duration.toLong())
val durationString = NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, duration)
val offsetUnix = spanToUnix(state.startSpan + e.x.toInt()) // start of the time span (day/week/month)
val startUnix = offsetUnix + value.yVals.sliceArray(0..<h.stackIndex).fold(0) { acc, y -> acc + y.toInt() }
@@ -457,36 +622,32 @@ class StatisticsActivity : AppCompatActivity() {
override fun onNothingSelected() {}
})
val data = BarData(set1)
data.setValueTextSize(12f)
val valueCount = min(values.size, 24)
Log.d(TAG, "showSleepPatternBarGraphDaily: values.size: ${values.size}, barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}")
set1.setDrawIcons(false)
//barChart.legend.isEnabled = false
//val valueCount = min(values.size, 24)
//barChart.setVisibleXRangeMaximum(valueCount.toFloat())
//barChart.xAxis.setLabelCount(valueCount)
data.setValueTextSize(12f)
barChart.setData(data)
Log.d(TAG, "showSleepPatternBarGraphDaily: new barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}")
barChart.legend.isEnabled = false
val valueCount = min(values.size, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate()
}
// Make sure the value on the bar is not out of screen.
class CustomHorizontalBarChartRenderer(chart: BarDataProvider, animator: ChartAnimator, viewPortHandler: ViewPortHandler): HorizontalBarChartRenderer(chart, animator, viewPortHandler) {
override fun drawValue(
c: Canvas,
valueText: String,
x: Float,
y: Float,
color: Int
) {
mValuePaint.setColor(color)
c.drawText(valueText, x.coerceAtLeast(60F), y, mValuePaint)
}
}
fun showSleepBarGraph(state: GraphState) {
val ranges = toSleepRanges(state.events)
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) })
//Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}")
for (range in ranges) {
// a sleep event can span to another day
@@ -497,14 +658,18 @@ class StatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix)
var mid = startUnix
//Log.d(TAG, "beginIndex: $begIndex, endIndex: $endIndex, startUnix: ${Date(startUnix * 1000)} ($startUnix), endUnix: ${Date(endUnix * 1000)} ($endUnix)")
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})")
for (i in begIndex..endIndex) {
// i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "i: $i, mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}")
val sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan
val duration = sleepEnd - sleepBegin
//Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}")
state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
@@ -531,22 +696,19 @@ class StatisticsActivity : AppCompatActivity() {
}
val set1 = BarDataSet(values, "")
val data = BarData(set1)
set1.setDrawValues(true)
set1.setDrawIcons(false)
set1.isHighlightEnabled = false
val data = BarData(set1)
data.setValueTextSize(12f)
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return when (graphTypeSelection) {
GraphType.SLEEP_EVENTS -> {
prefix + value.toInt().toString()
}
GraphType.SLEEP_SUM -> {
prefix + DateUtils.formatTimeDuration(applicationContext, value.toLong())
prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, value.toInt())
}
else -> {
Log.e(TAG, "unhandled graphTypeSelection $graphTypeSelection")
@@ -556,14 +718,16 @@ class StatisticsActivity : AppCompatActivity() {
}
})
val valueCount = min(values.size, 24)
barChart.renderer = CustomHorizontalBarChartRenderer(barChart, barChart.animator, barChart.viewPortHandler)
set1.setDrawIcons(false)
data.setValueTextSize(12f)
barChart.setData(data)
barChart.legend.isEnabled = false
val valueCount = min(values.size, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate()
}
@@ -575,6 +739,7 @@ class StatisticsActivity : AppCompatActivity() {
state.dayCounter.setDaysWithData(event.time, event.time)
values[index].x += values.size.toFloat()
if (graphTypeSelection == GraphType.BOTTLE_EVENTS) {
values[index].y += 1F
} else if (graphTypeSelection == GraphType.BOTTLE_SUM) {
@@ -587,6 +752,7 @@ class StatisticsActivity : AppCompatActivity() {
for (index in values.indices) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
//Log.d(TAG, "index: $index, daysWithData: $daysWithData")
if (daysWithData == 0) {
assert(values[index].y == 0F)
} else {
@@ -595,14 +761,21 @@ class StatisticsActivity : AppCompatActivity() {
}
val set1 = BarDataSet(values, "")
set1.setDrawValues(true)
set1.isHighlightEnabled = false
//barChart.axisLeft.isSLEEP_PATTERN_GRANULARITYEnabled = true
//barChart.axisLeft.setSLEEP_PATTERN_GRANULARITY(0.8F)
val data = BarData(set1)
data.setValueTextSize(12f)
//data.barWidth = 0.3F // 0.85 default // ratio of barWidth to totalWidth.
//Log.d(TAG, "data.barWidth: ${data.barWidth}")
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
//Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}")
return when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS -> {
prefix + value.toInt().toString()
@@ -618,14 +791,17 @@ class StatisticsActivity : AppCompatActivity() {
}
})
val valueCount = min(values.size, 24)
barChart.renderer = CustomHorizontalBarChartRenderer(barChart, barChart.animator, barChart.viewPortHandler)
data.setValueTextSize(12f)
barChart.setData(data)
//barChart.legend.isEnabled = false
val valueCount = min(values.size, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate()
//barChart.moveViewToX(values.lastOrNull()!!.x)
}
class DayCounter(val startDays: Int, val stopDays: Int) {
@@ -636,8 +812,10 @@ class StatisticsActivity : AppCompatActivity() {
fun countDaysWithData(beginUnix: Long, endUnix: Long): Int {
val beginDays = unixToDays(beginUnix)
val endDays = unixToDays(endUnix)
//Log.d(TAG, "countDaysWithData: beginDays: $beginDays, endDays: $endDays, ${Date(beginUnix * 1000)} - ${Date(endUnix * 1000)}")
var count = 0
for (i in (beginDays - startDays)..<(endDays - startDays)) {
//Log.d(TAG, "countDaysWithData: i: $i, size: ${daysWithData.size}")
count += if (daysWithData[i]) { 1 } else { 0 }
}
return count
@@ -656,22 +834,8 @@ class StatisticsActivity : AppCompatActivity() {
data class GraphState(val events: List<LunaEvent>, val dayCounter: DayCounter, val startUnix: Long, val endUnix: Long, val startSpan: Int, val endSpan: Int)
fun showGraph() {
barChart.fitScreen()
barChart.data?.clearValues()
barChart.xAxis.valueFormatter = null
barChart.notifyDataSetChanged()
barChart.clear()
barChart.invalidate()
val type = when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM -> LunaEvent.Type.BABY_BOTTLE
GraphType.SLEEP_EVENTS,
GraphType.SLEEP_SUM,
GraphType.SLEEP_PATTERN -> LunaEvent.Type.SLEEP
}
// wrapper for common graph setup
fun prepareGraph(type: LunaEvent.Type, callback: (GraphState) -> Unit) {
val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time }
if (events.isEmpty()) {
@@ -728,20 +892,33 @@ class StatisticsActivity : AppCompatActivity() {
}
}
val state = GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan)
//Log.d(TAG, "startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}")
callback(GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan))
}
fun showGraph() {
//Log.d(TAG, "showGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection")
// test
resetBarChart()
when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM -> showBottleBarGraph(state)
GraphType.BOTTLE_SUM -> prepareGraph(LunaEvent.Type.BABY_BOTTLE) { state -> showBottleBarGraph(state) }
GraphType.SLEEP_EVENTS,
GraphType.SLEEP_SUM -> showSleepBarGraph(state)
GraphType.SLEEP_PATTERN -> if (timeRangeSelection == TimeRange.DAY) {
GraphType.SLEEP_SUM -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepBarGraph(state) }
GraphType.SLEEP_PATTERN -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepPatternBarGraphSlotted(state) }
/* if (timeRangeSelection == TimeRange.DAY) {
// specialized pattern bar for daily view (optional)
showSleepPatternBarGraphDaily(state)
} else {
showSleepPatternBarGraphSlotted(state)
}
}
*/
GraphType.MEDICINE_EVENTS -> prepareGraph(LunaEvent.Type.MEDICINE) { state -> showMedicineBarGraph(state) }
}
}
private interface SpinnerItemSelected {
@@ -760,6 +937,8 @@ class StatisticsActivity : AppCompatActivity() {
val spinnerAdapter =
ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item)
//Log.d(TAG, "spinner ${arrayValues.indexOf(currentValue)} (${arrayValues.joinToString { it }})")
spinner.adapter = spinnerAdapter
spinner.setSelection(arrayValues.indexOf(currentValue))
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {

View File

@@ -58,7 +58,12 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
LunaEvent.Type.NOTE -> item.notes
else -> item.getRowItemTitle(context)
}
holder.time.text = DateUtils.formatTimeAgo(context, item.getEndTime())
val endTime = if (item.type == LunaEvent.Type.SLEEP) {
item.quantity + item.time
} else {
item.time
}
holder.time.text = DateUtils.formatTimeAgo(context, endTime)
var quantityText = numericUtils.formatEventQuantity(item)
// if the event is weight, show difference with the last one

View File

@@ -115,18 +115,6 @@ class LunaEvent: Comparable<LunaEvent> {
return getDialogMessage(context, type)
}
fun getStartTime(): Long {
return time
}
fun getEndTime(): Long {
return if (type == Type.SLEEP) {
time + quantity
} else {
time
}
}
fun toJson(): JSONObject {
return jo
}
@@ -174,7 +162,7 @@ class LunaEvent: Comparable<LunaEvent> {
Type.DIAPERCHANGE_PEE,
Type.PUKE -> R.string.log_amount_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
}
)

View File

@@ -9,15 +9,10 @@ import java.util.Date
class DateUtils {
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.
*/
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
val years = (seconds / (365 * 24 * 60 * 60F)).toLong()
@@ -61,11 +56,11 @@ class DateUtils {
return builder.toString()
}
if (years != 0L) {
if (years > 0) {
return format(years, days, R.string.year_ago, R.string.years_ago, R.string.day_ago, R.string.days_ago)
} else if (days != 0L) {
} else if (days > 0) {
return format(days, hours, R.string.day_ago, R.string.days_ago, R.string.hour_ago, R.string.hours_ago)
} else if (hours != 0L) {
} else if (hours > 0) {
return format(hours, minutes, R.string.hour_ago, R.string.hours_ago, R.string.minute_ago, R.string.minutes_ago)
} else {
return format(minutes, seconds, R.string.minute_ago, R.string.minute_ago, R.string.second_ago, R.string.seconds_ago)

View File

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

View File

@@ -11,14 +11,13 @@
android:id="@+id/dialog_date_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="20sp"
android:text="💤"/>
<LinearLayout
android:id="@+id/duration_buttons"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="10dp"
@@ -29,61 +28,29 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/dialog_duration_button_minus_5"/>
android:text="-5"/>
<Button
android:id="@+id/dialog_date_duration_now"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/dialog_duration_button_now"/>
<Button
android:id="@+id/dialog_date_duration_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/dialog_duration_button_clear"/>
android:text="@string/now"/>
<Button
android:id="@+id/dialog_date_duration_plus5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/dialog_duration_button_plus_5"/>
android:text="+5"/>
</LinearLayout>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:gravity="center"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/dialog_date_picker_begin"
android:id="@+id/dialog_date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"/>
<TextView
android:id="@+id/dialog_date_range_delimiter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"
android:text=""/>
<TextView
android:id="@+id/dialog_date_picker_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"/>
</LinearLayout>
android:layout_gravity="center"
android:layout_marginTop="20dp"/>
</LinearLayout>

View File

@@ -13,6 +13,7 @@
<item>@string/statistics_sleep_sum</item>
<item>@string/statistics_sleep_events</item>
<item>@string/statistics_sleep_pattern</item>
<item>@string/statistics_medicine_events</item>
</string-array>
<string-array name="StatisticsTypeValues">
@@ -21,6 +22,7 @@
<item>SLEEP_SUM</item>
<item>SLEEP_EVENTS</item>
<item>SLEEP_PATTERN</item>
<item>MEDICINE_EVENTS</item>
</string-array>
<string-array name="StatisticsTimeLabels">

View File

@@ -130,7 +130,8 @@
<string name="log_temperature_dialog_description">Select the temperature:</string>
<string name="log_unknown_dialog_description"></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="log_sleep_dialog_description_start">Start sleep cycle:</string>
<string name="measurement_unit_liquid_base_metric" translatable="false">ml</string>
<string name="measurement_unit_weight_base_metric" translatable="false">g</string>
@@ -160,11 +161,6 @@
<string name="dialog_event_detail_notes">Notes</string>
<string name="dialog_event_detail_signature">by %s</string>
<string name="dialog_duration_button_clear">Clear</string>
<string name="dialog_duration_button_minus_5">-5 min</string>
<string name="dialog_duration_button_now">Now</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_logbookname">👶 Logbook name</string>
<string name="dialog_add_logbook_message">Write a name to identify this logbook. This name will appear on top of the screen and, if you use WebDAV, will be in the save file name as well.</string>