14 Commits

Author SHA1 Message Date
4f0416e1c1 NumericUtils: remove possible trailing whitespace 2025-12-14 19:42:08 +01:00
8c2fcaaef5 MainActivity: do not switch logbook on reload 2025-12-14 19:42:08 +01:00
01907d7c4e LunaEvent: reorganize event text getters
Use method names that better reflect
the use of the returned text.
2025-12-14 19:42:08 +01:00
3a3dc2faf4 Add dynamic header setting
This makes the 'no breastfeeding' setting irrelevant.
2025-12-14 19:42:08 +01:00
2518118759 LunaEvent: use enum class for event types
This helps to have compile errors when some
case it not handled while adding a new type.
The enum class can also be interated over
to create a complete drop down list.
2025-12-14 19:42:08 +01:00
d8b67531db MainActivity: increase bottle volume to 340ml
This is the maximum amount found in sold bottles.
2025-12-14 19:42:08 +01:00
0a424e8807 improve statistics 2025-12-14 19:42:08 +01:00
8adacd3bfe gradle: use uniform implementation directive for sardine-android 2025-12-14 19:42:08 +01:00
508c14ff6e gradle: avoid inclusion of apk signing blobs
See https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs
2025-12-14 19:42:08 +01:00
7d67a2e33b gradle: set compileSDK/targetSdk to 36 2025-12-14 19:42:08 +01:00
a0dc023941 add statistics for bottle and sleep events 2025-12-14 19:42:08 +01:00
6759956461 MainActivity: show save button if any values has changed 2025-12-14 19:42:08 +01:00
c521a3373f MainActivity: use unique templates for notes 2025-12-14 19:42:08 +01:00
1ad5f31b32 LunaEvent: add sleep event 2025-12-14 19:42:03 +01:00
17 changed files with 874 additions and 544 deletions

View File

@@ -6,12 +6,12 @@ plugins {
android { android {
namespace = "it.danieleverducci.lunatracker" namespace = "it.danieleverducci.lunatracker"
compileSdk = 34 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "it.danieleverducci.lunatracker" applicationId = "it.danieleverducci.lunatracker"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 36
versionCode = 7 versionCode = 7
versionName = "0.9" versionName = "0.9"
@@ -31,6 +31,12 @@ android {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
} }
@@ -40,7 +46,6 @@ android {
} }
dependencies { dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
@@ -51,7 +56,7 @@ dependencies {
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.recyclerview) implementation(libs.androidx.recyclerview)
implementation("com.github.thegrizzlylabs:sardine-android:v0.9") implementation(libs.sardine.android)
implementation(libs.material) implementation(libs.material)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View File

@@ -15,6 +15,7 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Button import android.widget.Button
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout
import android.widget.NumberPicker import android.widget.NumberPicker
import android.widget.PopupWindow import android.widget.PopupWindow
import android.widget.Spinner import android.widget.Spinner
@@ -53,6 +54,7 @@ class MainActivity : AppCompatActivity() {
const val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false const val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false
// list of all events // list of all events
var allEvents = arrayListOf<LunaEvent>() var allEvents = arrayListOf<LunaEvent>()
var currentPopupItems = listOf<LunaEvent.Type>()
} }
var logbook: Logbook? = null var logbook: Logbook? = null
@@ -84,31 +86,13 @@ class MainActivity : AppCompatActivity() {
recyclerView = findViewById(R.id.list_events) recyclerView = findViewById(R.id.list_events)
recyclerView.setLayoutManager(LinearLayoutManager(applicationContext)) recyclerView.setLayoutManager(LinearLayoutManager(applicationContext))
populateHeaderMenu()
// Set listeners // Set listeners
findViewById<View>(R.id.logbooks_add_button).setOnClickListener { findViewById<View>(R.id.logbooks_add_button).setOnClickListener {
showAddLogbookDialog(true) showAddLogbookDialog(true)
} }
findViewById<View>(R.id.button_bottle).setOnClickListener {
addBabyBottleEvent(LunaEvent(LunaEvent.TYPE_BABY_BOTTLE))
}
findViewById<View>(R.id.button_food).setOnClickListener {
addNoteEvent(LunaEvent(LunaEvent.TYPE_FOOD))
}
findViewById<View>(R.id.button_nipple_left).setOnClickListener {
addPlainEvent(LunaEvent(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE))
}
findViewById<View>(R.id.button_nipple_both).setOnClickListener {
addPlainEvent(LunaEvent(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE))
}
findViewById<View>(R.id.button_nipple_right).setOnClickListener {
addPlainEvent(LunaEvent(LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE))
}
findViewById<View>(R.id.button_change_poo).setOnClickListener {
addAmountEvent(LunaEvent(LunaEvent.TYPE_DIAPERCHANGE_POO))
}
findViewById<View>(R.id.button_change_pee).setOnClickListener {
addAmountEvent(LunaEvent(LunaEvent.TYPE_DIAPERCHANGE_PEE))
}
val moreButton = findViewById<View>(R.id.button_more) val moreButton = findViewById<View>(R.id.button_more)
moreButton.setOnClickListener { moreButton.setOnClickListener {
showOverflowPopupWindow(moreButton) showOverflowPopupWindow(moreButton)
@@ -124,7 +108,11 @@ class MainActivity : AppCompatActivity() {
loadLogbookList() loadLogbookList()
} }
findViewById<View>(R.id.button_sync).setOnClickListener { findViewById<View>(R.id.button_sync).setOnClickListener {
loadLogbookList() if (logbook != null) {
loadLogbook(logbook!!.name)
} else {
loadLogbookList()
}
} }
} }
@@ -150,6 +138,82 @@ class MainActivity : AppCompatActivity() {
allEvents = logbook?.logs ?: arrayListOf() allEvents = logbook?.logs ?: arrayListOf()
setListAdapter(allEvents) setListAdapter(allEvents)
populateHeaderMenu()
}
private fun populateHeaderMenu() {
val settingsRepository = LocalSettingsRepository(this)
val dynamicMenu = settingsRepository.loadDynamicMenu()
val eventTypeStats = mutableMapOf<LunaEvent.Type, Int>()
if (dynamicMenu) {
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 or ordinal
val eventTypesSorted = LunaEvent.Type.entries.toList().sortedWith(
compareBy({ -1 * (eventTypeStats[it] ?: 0) }, { it.ordinal })
).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 maxButtonCount = if (dynamicMenu) { usedEventCount } else { 7 }
val eventsShown = setupMenu(maxButtonCount, eventTypesSorted)
// store left over events for popup menu
currentPopupItems = eventTypesSorted.subList(eventsShown, eventTypesSorted.size)
} }
override fun onStart() { override fun onStart() {
@@ -172,12 +236,6 @@ class MainActivity : AppCompatActivity() {
signature = settingsRepository.loadSignature() signature = settingsRepository.loadSignature()
val noBreastfeeding = settingsRepository.loadNoBreastfeeding()
findViewById<View>(R.id.layout_nipples).visibility = when (noBreastfeeding) {
true -> View.GONE
false -> View.VISIBLE
}
// Update list dates // Update list dates
recyclerView.adapter?.notifyDataSetChanged() recyclerView.adapter?.notifyDataSetChanged()
@@ -206,14 +264,14 @@ class MainActivity : AppCompatActivity() {
fun askBabyBottleContent(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) { fun askBabyBottleContent(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_bottle, null) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_bottle, null)
d.setTitle(event.getTypeDescription(this)) d.setTitle(event.getDialogTitle(this))
d.setMessage(event.getDialogMessage(this)) d.setMessage(event.getDialogMessage(this))
d.setView(dialogView) d.setView(dialogView)
val numberPicker = dialogView.findViewById<NumberPicker>(R.id.dialog_number_picker) val numberPicker = dialogView.findViewById<NumberPicker>(R.id.dialog_number_picker)
numberPicker.minValue = 1 // "10" numberPicker.minValue = 1 // "10"
numberPicker.maxValue = 25 // "250 numberPicker.maxValue = 34 // "340
numberPicker.displayedValues = ((10..250 step 10).map { it.toString() }.toTypedArray()) numberPicker.displayedValues = ((10..340 step 10).map { it.toString() }.toTypedArray())
numberPicker.wrapSelectorWheel = false numberPicker.wrapSelectorWheel = false
numberPicker.value = event.quantity / 10 numberPicker.value = event.quantity / 10
@@ -248,7 +306,7 @@ class MainActivity : AppCompatActivity() {
// Show number picker dialog // Show number picker dialog
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_weight, null) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_weight, null)
d.setTitle(event.getTypeDescription(this)) d.setTitle(event.getDialogTitle(this))
d.setMessage(event.getDialogMessage(this)) d.setMessage(event.getDialogMessage(this))
d.setView(dialogView) d.setView(dialogView)
@@ -292,12 +350,12 @@ class MainActivity : AppCompatActivity() {
// Show number picker dialog // Show number picker dialog
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_temperature, null) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_temperature, null)
d.setTitle(event.getTypeDescription(this)) d.setTitle(event.getDialogTitle(this))
d.setMessage(event.getDialogMessage(this)) d.setMessage(event.getDialogMessage(this))
d.setView(dialogView) d.setView(dialogView)
val tempSlider = dialogView.findViewById<Slider>(R.id.dialog_temperature_value) val tempSlider = dialogView.findViewById<Slider>(R.id.dialog_temperature_value)
val range = NumericUtils(this).getValidEventQuantityRange(LunaEvent.TYPE_TEMPERATURE)!! val range = NumericUtils(this).getValidEventQuantityRange(LunaEvent.Type.TEMPERATURE)!!
tempSlider.valueFrom = range.first.toFloat() tempSlider.valueFrom = range.first.toFloat()
tempSlider.valueTo = range.second.toFloat() tempSlider.valueTo = range.second.toFloat()
tempSlider.value = if (event.quantity == 0) { tempSlider.value = if (event.quantity == 0) {
@@ -380,7 +438,7 @@ class MainActivity : AppCompatActivity() {
fun askSleepValue(event: LunaEvent, onPositive: () -> Unit) { fun askSleepValue(event: LunaEvent, 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.getTypeDescription(this)) d.setTitle(event.getDialogTitle(this))
d.setMessage(event.getDialogMessage(this)) d.setMessage(event.getDialogMessage(this))
d.setView(dialogView) d.setView(dialogView)
@@ -396,8 +454,8 @@ class MainActivity : AppCompatActivity() {
var duration = event.quantity var duration = event.quantity
fun isValidTime(timeSeconds: Long, durationSeconds: Int): Boolean { fun isValidTime(timeSeconds: Long, durationSeconds: Int): Boolean {
val now = Calendar.getInstance().time.time / 1000 val now = System.currentTimeMillis() / 1000
return (timeSeconds + durationSeconds) <= now && durationSeconds < (12 * 60 * 60) return (timeSeconds + durationSeconds) <= now && durationSeconds < (24 * 60 * 60)
} }
val onDateChange = { time: Long -> val onDateChange = { time: Long ->
@@ -430,7 +488,7 @@ class MainActivity : AppCompatActivity() {
durationPlus5Button.setOnClickListener { adjust(5) } durationPlus5Button.setOnClickListener { adjust(5) }
durationNowButton.setOnClickListener { durationNowButton.setOnClickListener {
val now = Calendar.getInstance().time.time / 1000 val now = System.currentTimeMillis() / 1000
val start = pickedDateTime.time.time / 1000 val start = pickedDateTime.time.time / 1000
if (now > start) { if (now > start) {
duration = (now - start).toInt() duration = (now - start).toInt()
@@ -441,7 +499,6 @@ class MainActivity : AppCompatActivity() {
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val time = pickedDateTime.time.time / 1000 val time = pickedDateTime.time.time / 1000
if (isValidTime(time, duration)) { if (isValidTime(time, duration)) {
event.time = time event.time = time
event.quantity = duration event.quantity = duration
@@ -467,7 +524,7 @@ class MainActivity : AppCompatActivity() {
fun askAmountValue(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) { fun askAmountValue(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_amount, null) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_amount, null)
d.setTitle(event.getTypeDescription(this)) d.setTitle(event.getDialogTitle(this))
d.setMessage(event.getDialogMessage(this)) d.setMessage(event.getDialogMessage(this))
d.setView(dialogView) d.setView(dialogView)
@@ -509,7 +566,7 @@ class MainActivity : AppCompatActivity() {
fun askDateValue(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) { fun askDateValue(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_plain, null) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_plain, null)
d.setTitle(event.getTypeDescription(this)) d.setTitle(event.getDialogTitle(this))
d.setMessage(event.getDialogMessage(this)) d.setMessage(event.getDialogMessage(this))
d.setView(dialogView) d.setView(dialogView)
@@ -538,11 +595,11 @@ class MainActivity : AppCompatActivity() {
} }
fun askNotes(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) { fun askNotes(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) {
val useQuantity = (event.type != LunaEvent.TYPE_NOTE && event.type != LunaEvent.TYPE_CUSTOM) val useQuantity = (event.type != LunaEvent.Type.NOTE)
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_notes, null) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_notes, null)
d.setTitle(event.getTypeDescription(this)) d.setTitle(event.getDialogTitle(this))
d.setMessage(event.getDialogMessage(this)) d.setMessage(event.getDialogMessage(this))
d.setView(dialogView) d.setView(dialogView)
val notesET = dialogView.findViewById<EditText>(R.id.notes_edittext) val notesET = dialogView.findViewById<EditText>(R.id.notes_edittext)
@@ -720,8 +777,8 @@ class MainActivity : AppCompatActivity() {
val quantityTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity) val quantityTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity)
val notesTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_notes) val notesTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_notes)
emojiTextView.text = event.getTypeEmoji(this) emojiTextView.text = event.getHeaderEmoji(this)
descriptionTextView.text = event.getTypeDescription(this) descriptionTextView.text = event.getDialogTitle(this)
d.setView(dialogView) d.setView(dialogView)
@@ -760,7 +817,7 @@ class MainActivity : AppCompatActivity() {
val updateValues = { val updateValues = {
quantityTextView.text = NumericUtils(this).formatEventQuantity(event) quantityTextView.text = NumericUtils(this).formatEventQuantity(event)
notesTextView.text = event.notes notesTextView.text = event.notes
if (event.type == LunaEvent.TYPE_SLEEP && event.quantity > 0) { if (event.type == LunaEvent.Type.SLEEP && event.quantity > 0) {
dateEndTextView.text = DateUtils.formatDateTime(event.time + event.quantity) dateEndTextView.text = DateUtils.formatDateTime(event.time + event.quantity)
dateEndTextView.visibility = View.VISIBLE dateEndTextView.visibility = View.VISIBLE
} else { } else {
@@ -782,22 +839,28 @@ class MainActivity : AppCompatActivity() {
quantityTextView.setOnClickListener { quantityTextView.setOnClickListener {
when (event.type) { when (event.type) {
LunaEvent.TYPE_BABY_BOTTLE -> askBabyBottleContent(event, false, updateValues) LunaEvent.Type.BABY_BOTTLE -> askBabyBottleContent(event, false, updateValues)
LunaEvent.TYPE_WEIGHT -> askWeightValue(event, false, updateValues) LunaEvent.Type.WEIGHT -> askWeightValue(event, false, updateValues)
LunaEvent.TYPE_DIAPERCHANGE_POO, LunaEvent.Type.DIAPERCHANGE_POO,
LunaEvent.TYPE_DIAPERCHANGE_PEE, LunaEvent.Type.DIAPERCHANGE_PEE,
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 -> askSleepValue(event, updateValues) LunaEvent.Type.SLEEP -> askSleepValue(event, updateValues)
else -> {
Log.w(TAG, "Unexpected type: ${event.type}")
}
} }
} }
notesTextView.setOnClickListener { notesTextView.setOnClickListener {
when (event.type) { when (event.type) {
LunaEvent.TYPE_FOOD, LunaEvent.Type.FOOD,
LunaEvent.TYPE_MEDICINE, LunaEvent.Type.MEDICINE,
LunaEvent.TYPE_NOTE -> askNotes(event, false, updateValues) LunaEvent.Type.NOTE -> askNotes(event, false, updateValues)
else -> {
Log.w(TAG, "Unexpected type: ${event.type}")
}
} }
} }
@@ -812,7 +875,7 @@ class MainActivity : AppCompatActivity() {
val previousTextView = dialogView.findViewById<TextView>(R.id.dialog_event_previous) val previousTextView = dialogView.findViewById<TextView>(R.id.dialog_event_previous)
val previousEvent = getPreviousSameEvent(event, allEvents) val previousEvent = getPreviousSameEvent(event, allEvents)
if (previousEvent != null) { if (previousEvent != null) {
val emoji = previousEvent.getTypeEmoji(applicationContext) val emoji = previousEvent.getHeaderEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, event.time - previousEvent.time) val time = DateUtils.formatTimeDuration(applicationContext, event.time - previousEvent.time)
previousTextView.text = String.format("⬅️ %s %s", emoji, time) previousTextView.text = String.format("⬅️ %s %s", emoji, time)
previousTextView.setOnClickListener { previousTextView.setOnClickListener {
@@ -827,7 +890,7 @@ class MainActivity : AppCompatActivity() {
val nextTextView = dialogView.findViewById<TextView>(R.id.dialog_event_next) val nextTextView = dialogView.findViewById<TextView>(R.id.dialog_event_next)
val nextEvent = getNextSameEvent(event, allEvents) val nextEvent = getNextSameEvent(event, allEvents)
if (nextEvent != null) { if (nextEvent != null) {
val emoji = nextEvent.getTypeEmoji(applicationContext) val emoji = nextEvent.getHeaderEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.time - event.time) val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.time - event.time)
nextTextView.text = String.format("%s %s ➡️", time, emoji) nextTextView.text = String.format("%s %s ➡️", time, emoji)
nextTextView.setOnClickListener { nextTextView.setOnClickListener {
@@ -998,7 +1061,7 @@ class MainActivity : AppCompatActivity() {
if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) { if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) {
for (e in logbook?.logs ?: listOf()) { for (e in logbook?.logs ?: listOf()) {
val em = e.getTypeEmoji(this@MainActivity) val em = e.getHeaderEmoji(this@MainActivity)
if (em == getString(R.string.event_unknown_type)) { if (em == getString(R.string.event_unknown_type)) {
Log.e(TAG, "UNKNOWN: ${e.type}") Log.e(TAG, "UNKNOWN: ${e.type}")
} }
@@ -1183,6 +1246,29 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun showCreateDialog(type: LunaEvent.Type) {
val event = LunaEvent(type)
when (type) {
LunaEvent.Type.BABY_BOTTLE -> addBabyBottleEvent(event)
LunaEvent.Type.WEIGHT -> addWeightEvent(event)
LunaEvent.Type.BREASTFEEDING_LEFT_NIPPLE -> addPlainEvent(event)
LunaEvent.Type.BREASTFEEDING_BOTH_NIPPLE -> addPlainEvent(event)
LunaEvent.Type.BREASTFEEDING_RIGHT_NIPPLE -> addPlainEvent(event)
LunaEvent.Type.DIAPERCHANGE_POO -> addAmountEvent(event)
LunaEvent.Type.DIAPERCHANGE_PEE -> addAmountEvent(event)
LunaEvent.Type.MEDICINE -> addNoteEvent(event)
LunaEvent.Type.ENEMA -> addNoteEvent(event)
LunaEvent.Type.NOTE -> addNoteEvent(event)
LunaEvent.Type.COLIC -> addPlainEvent(event)
LunaEvent.Type.TEMPERATURE -> addTemperatureEvent(event)
LunaEvent.Type.FOOD -> addNoteEvent(event)
LunaEvent.Type.PUKE -> addAmountEvent(event)
LunaEvent.Type.BATH -> addPlainEvent(event)
LunaEvent.Type.SLEEP -> addSleepEvent(event)
LunaEvent.Type.UNKNOWN -> {} // ignore
}
}
private fun showOverflowPopupWindow(anchor: View) { private fun showOverflowPopupWindow(anchor: View) {
if (showingOverflowPopupWindow) if (showingOverflowPopupWindow)
return return
@@ -1191,42 +1277,8 @@ class MainActivity : AppCompatActivity() {
isOutsideTouchable = true isOutsideTouchable = true
val inflater = LayoutInflater.from(anchor.context) val inflater = LayoutInflater.from(anchor.context)
contentView = inflater.inflate(R.layout.more_events_popup, null) contentView = inflater.inflate(R.layout.more_events_popup, null)
contentView.findViewById<View>(R.id.button_medicine).setOnClickListener {
addNoteEvent(LunaEvent(LunaEvent.TYPE_MEDICINE)) // Add statistics (hard coded)
dismiss()
}
contentView.findViewById<View>(R.id.button_enema).setOnClickListener {
addPlainEvent(LunaEvent(LunaEvent.TYPE_ENEMA))
dismiss()
}
contentView.findViewById<View>(R.id.button_note).setOnClickListener {
addNoteEvent(LunaEvent(LunaEvent.TYPE_NOTE))
dismiss()
}
contentView.findViewById<View>(R.id.button_temperature).setOnClickListener {
addTemperatureEvent(LunaEvent(LunaEvent.TYPE_TEMPERATURE))
dismiss()
}
contentView.findViewById<View>(R.id.button_puke).setOnClickListener {
addAmountEvent(LunaEvent(LunaEvent.TYPE_PUKE))
dismiss()
}
contentView.findViewById<View>(R.id.button_sleep).setOnClickListener {
addSleepEvent(LunaEvent(LunaEvent.TYPE_SLEEP))
dismiss()
}
contentView.findViewById<View>(R.id.button_colic).setOnClickListener {
addPlainEvent(LunaEvent(LunaEvent.TYPE_COLIC))
dismiss()
}
contentView.findViewById<View>(R.id.button_scale).setOnClickListener {
addWeightEvent(LunaEvent(LunaEvent.TYPE_WEIGHT))
dismiss()
}
contentView.findViewById<View>(R.id.button_bath).setOnClickListener {
addPlainEvent(LunaEvent(LunaEvent.TYPE_BATH))
dismiss()
}
contentView.findViewById<View>(R.id.button_statistics).setOnClickListener { contentView.findViewById<View>(R.id.button_statistics).setOnClickListener {
if (logbook != null && !pauseLogbookUpdate) { if (logbook != null && !pauseLogbookUpdate) {
val i = Intent(applicationContext, StatisticsActivity::class.java) val i = Intent(applicationContext, StatisticsActivity::class.java)
@@ -1237,6 +1289,20 @@ class MainActivity : AppCompatActivity() {
} }
dismiss() dismiss()
} }
val linearLayout = contentView.findViewById<LinearLayout>(R.id.layout_list)
// Add buttons to create other events
for (type in currentPopupItems) {
val view = layoutInflater.inflate(R.layout.more_events_popup_item, linearLayout, false)
val textView = view.findViewById<TextView>(R.id.tv)
textView.text = LunaEvent.getPopupItemTitle(applicationContext, type)
textView.setOnClickListener {
showCreateDialog(type)
dismiss()
}
linearLayout.addView(textView)
}
}.also { popupWindow -> }.also { popupWindow ->
popupWindow.setOnDismissListener({ popupWindow.setOnDismissListener({
Handler(mainLooper).postDelayed({ Handler(mainLooper).postDelayed({

View File

@@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.RadioButton import android.widget.RadioButton
import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -24,7 +25,7 @@ open class SettingsActivity : AppCompatActivity() {
protected lateinit var textViewWebDAVUser: TextView protected lateinit var textViewWebDAVUser: TextView
protected lateinit var textViewWebDAVPass: TextView protected lateinit var textViewWebDAVPass: TextView
protected lateinit var progressIndicator: LinearProgressIndicator protected lateinit var progressIndicator: LinearProgressIndicator
protected lateinit var switchNoBreastfeeding: SwitchMaterial protected lateinit var switchDynamicMenu: SwitchMaterial
protected lateinit var textViewSignature: EditText protected lateinit var textViewSignature: EditText
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -37,7 +38,7 @@ open class SettingsActivity : AppCompatActivity() {
textViewWebDAVUser = findViewById(R.id.settings_data_webdav_user) textViewWebDAVUser = findViewById(R.id.settings_data_webdav_user)
textViewWebDAVPass = findViewById(R.id.settings_data_webdav_pass) textViewWebDAVPass = findViewById(R.id.settings_data_webdav_pass)
progressIndicator = findViewById(R.id.progress_indicator) progressIndicator = findViewById(R.id.progress_indicator)
switchNoBreastfeeding = findViewById(R.id.switch_no_breastfeeding) switchDynamicMenu = findViewById(R.id.switch_dynamic_menu)
textViewSignature = findViewById(R.id.settings_signature) textViewSignature = findViewById(R.id.settings_signature)
findViewById<View>(R.id.settings_save).setOnClickListener({ findViewById<View>(R.id.settings_save).setOnClickListener({
@@ -54,7 +55,7 @@ open class SettingsActivity : AppCompatActivity() {
fun loadSettings() { fun loadSettings() {
val dataRepo = settingsRepository.loadDataRepository() val dataRepo = settingsRepository.loadDataRepository()
val webDavCredentials = settingsRepository.loadWebdavCredentials() val webDavCredentials = settingsRepository.loadWebdavCredentials()
val noBreastfeeding = settingsRepository.loadNoBreastfeeding() val dynamicMenu = settingsRepository.loadDynamicMenu()
val signature = settingsRepository.loadSignature() val signature = settingsRepository.loadSignature()
when (dataRepo) { when (dataRepo) {
@@ -63,7 +64,7 @@ open class SettingsActivity : AppCompatActivity() {
} }
textViewSignature.setText(signature) textViewSignature.setText(signature)
switchNoBreastfeeding.isChecked = noBreastfeeding switchDynamicMenu.isChecked = dynamicMenu
if (webDavCredentials != null) { if (webDavCredentials != null) {
textViewWebDAVUrl.text = webDavCredentials[0] textViewWebDAVUrl.text = webDavCredentials[0]
@@ -160,7 +161,7 @@ open class SettingsActivity : AppCompatActivity() {
if (radioDataWebDAV.isChecked) LocalSettingsRepository.DATA_REPO.WEBDAV if (radioDataWebDAV.isChecked) LocalSettingsRepository.DATA_REPO.WEBDAV
else LocalSettingsRepository.DATA_REPO.LOCAL_FILE else LocalSettingsRepository.DATA_REPO.LOCAL_FILE
) )
settingsRepository.saveNoBreastfeeding(switchNoBreastfeeding.isChecked) settingsRepository.saveDynamicMenu(switchDynamicMenu.isChecked)
settingsRepository.saveSignature(textViewSignature.text.toString()) settingsRepository.saveSignature(textViewSignature.text.toString())
settingsRepository.saveWebdavCredentials( settingsRepository.saveWebdavCredentials(
textViewWebDAVUrl.text.toString(), textViewWebDAVUrl.text.toString(),

View File

@@ -17,24 +17,39 @@ import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.formatter.ValueFormatter import com.github.mikephil.charting.formatter.ValueFormatter
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet import com.github.mikephil.charting.interfaces.datasets.IBarDataSet
import it.danieleverducci.lunatracker.entities.LunaEvent import it.danieleverducci.lunatracker.entities.LunaEvent
import utils.DateUtils.Companion.formatTimeDuration
import utils.NumericUtils import utils.NumericUtils
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import androidx.core.graphics.toColorInt
class StatisticsActivity : AppCompatActivity() { class StatisticsActivity : AppCompatActivity() {
lateinit var barChart: BarChart lateinit var barChart: BarChart
lateinit var noDataTextView: TextView lateinit var noDataTextView: TextView
lateinit var eventTypeSelection: Spinner lateinit var graphTypeSpinner: Spinner
lateinit var dataTypeSelection: Spinner //lateinit var dataTypeSelection: Spinner
lateinit var timeRangeSelection: Spinner lateinit var timeRangeSpinner: Spinner
enum class GraphType {
BOTTLE_EVENTS,
BOTTLE_SUM,
BOTTLE_SUM_AVERAGE,
SLEEP_SUM,
SLEEP_SUM_AVERAGE,
SLEEP_EVENTS,
SLEEP_PATTERN
}
enum class RangeType {
DAY,
WEEK,
MONTH
}
// default selection // default selection
var eventTypeSelectionValue = "BOTTLE" var graphTypeSelection = GraphType.SLEEP_SUM
var dataTypeSelectionValue = "AMOUNT" var timeRangeSelection = RangeType.DAY
var timeRangeSelectionValue = "DAY"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -65,25 +80,38 @@ class StatisticsActivity : AppCompatActivity() {
barChart.xAxis.setDrawLabels(true) barChart.xAxis.setDrawLabels(true)
barChart.xAxis.setDrawAxisLine(false) barChart.xAxis.setDrawAxisLine(false)
eventTypeSelection = findViewById(R.id.type_selection) graphTypeSpinner = findViewById(R.id.type_selection)
dataTypeSelection = findViewById(R.id.data_selection) //dataTypeSelection = findViewById(R.id.data_selection)
timeRangeSelection = findViewById(R.id.time_selection) timeRangeSpinner = findViewById(R.id.time_selection)
setupSpinner(eventTypeSelectionValue, setupSpinner("SLEEP_SUM",
R.id.type_selection, R.id.type_selection,
R.array.StatisticsTypeLabels, R.array.StatisticsTypeLabels,
R.array.StatisticsTypeValues, R.array.StatisticsTypeValues,
object : SpinnerItemSelected { object : SpinnerItemSelected {
override fun call(newValue: String?) { override fun call(newValue: String?) {
if (newValue != null) { newValue ?: return
eventTypeSelectionValue = newValue graphTypeSelection = when (newValue) {
//Log.d("event", "new value: $newValue") "BOTTLE_EVENTS" -> GraphType.BOTTLE_EVENTS
updateGraph() "BOTTLE_SUM" -> GraphType.BOTTLE_SUM
"BOTTLE_SUM_AVERAGE" -> GraphType.BOTTLE_SUM_AVERAGE
"SLEEP_SUM_AVERAGE" -> GraphType.SLEEP_SUM_AVERAGE
"SLEEP_SUM" -> GraphType.SLEEP_SUM
"SLEEP_SUM_AVERAGE" -> GraphType.SLEEP_SUM_AVERAGE
"SLEEP_EVENTS" -> GraphType.SLEEP_EVENTS
"SLEEP_PATTERN" -> GraphType.SLEEP_PATTERN
else -> {
Log.e(TAG, "Invalid graph type selection: $newValue")
return
}
} }
//Log.d("event", "new value: $newValue")
updateGraph()
} }
} }
) )
/*
setupSpinner(dataTypeSelectionValue, setupSpinner(dataTypeSelectionValue,
R.id.data_selection, R.id.data_selection,
R.array.StatisticsDataLabels, R.array.StatisticsDataLabels,
@@ -98,18 +126,25 @@ class StatisticsActivity : AppCompatActivity() {
} }
} }
) )
*/
setupSpinner(timeRangeSelectionValue, setupSpinner("DAY",
R.id.time_selection, R.id.time_selection,
R.array.StatisticsTimeLabels, R.array.StatisticsTimeLabels,
R.array.StatisticsTimeValues, R.array.StatisticsTimeValues,
object : SpinnerItemSelected { object : SpinnerItemSelected {
override fun call(newValue: String?) { override fun call(newValue: String?) {
if (newValue != null) { newValue ?: return
timeRangeSelectionValue = newValue timeRangeSelection = when (newValue) {
//Log.d("event", "new value: $newValue") "DAY" -> RangeType.DAY
updateGraph() "WEEK" -> RangeType.WEEK
"MONTH" -> RangeType.MONTH
else -> {
Log.e(TAG, "Invalid time range selection: $newValue")
return
}
} }
//Log.d("event", "new value: $newValue")
updateGraph()
} }
} }
) )
@@ -117,123 +152,206 @@ class StatisticsActivity : AppCompatActivity() {
updateGraph() updateGraph()
} }
fun updateGraph() { fun showSleepBarGraph(events: List<LunaEvent>, unixToSpan: (Long) -> Int, spanToUnix: (Int) -> Long) {
val eventType = when (eventTypeSelectionValue) { fun getEndTime(event: LunaEvent): Long {
"BOTTLE" -> LunaEvent.TYPE_BABY_BOTTLE if (event.quantity == 0) {
"SLEEP" -> LunaEvent.TYPE_SLEEP // sleep is still ongoing
else -> { val dateTime = Calendar.getInstance()
Log.e(TAG, "unhandled eventTypeSelectionValue: $eventTypeSelectionValue") return (dateTime.time.time / 1000) - event.time
return } else {
return event.quantity.toLong()
} }
} }
val allEvents = MainActivity.allEvents.filter { it.type == eventType }.sortedBy { it.time }
data class SleepRange(val start: Long, val end: Long)
val ranges = arrayListOf<SleepRange>()
// Transform events into time ranges.
// Merge overlapping times and extend
// ongoing sleep events until now.
val dateTime = Calendar.getInstance()
for (event in events) {
val endTime = if (event.quantity == 0) {
dateTime.time.time / 1000 // now
} else {
event.time + event.quantity
}
/*
// TODO: handle overlap
if (ranges.isNotEmpty()) {
val lastItem = ranges.lastItem()
if (lastItem.start)
if (lastItem.end <= event.time) {
// distinct
}
}
}
*/
ranges.add(SleepRange(event.time, endTime))
}
// unix time span of all events
val startUnix = events.minOf { it.time }
val endUnix = events.maxOf { getEndTime(it) }
// convert to days, weeks or months
val startSpan = unixToSpan(startUnix)
val endSpan = unixToSpan(endUnix)
val values = ArrayList<BarEntry>() val values = ArrayList<BarEntry>()
val labels = ArrayList<String>()
val unixToSpan = when (timeRangeSelectionValue) { fun countEvent(index: Int) {
"DAY" -> { unix: Long -> unixToDays(unix) } // create initial values
"WEEK" -> { unix: Long -> unixToWeeks(unix) } while (index >= values.size) {
"MONTH" -> { unix: Long -> unixToMonths(unix) }
else -> {
Log.e(TAG, "Invalid timeRangeSelectionValue: $timeRangeSelectionValue")
return
}
}
val spanToUnix = when (timeRangeSelectionValue) {
"DAY" -> { span: Int -> daysToUnix(span) }
"WEEK" -> { span: Int -> weeksToUnix(span) }
"MONTH" -> { span: Int -> monthsToUnix(span) }
else -> {
Log.e(TAG, "Invalid timeRangeSelectionValue: $timeRangeSelectionValue")
return
}
}
fun spanToLabel(span: Int): String {
val dateTime = Calendar.getInstance()
dateTime.time = Date(1000 * spanToUnix(span))
val year = dateTime.get(Calendar.YEAR)
val month = dateTime.get(Calendar.MONTH) + 1 // month starts at 0
val week = dateTime.get(Calendar.WEEK_OF_YEAR)
val day = dateTime.get(Calendar.DAY_OF_MONTH)
return when (timeRangeSelectionValue) {
"DAY" -> "$day/$month/$year"
"WEEK" -> "$week/$year"
"MONTH" -> "$month/$year"
else -> {
Log.e(TAG, "Invalid timeRangeSelectionValue: $timeRangeSelectionValue")
"?"
}
}
}
if (allEvents.isNotEmpty()) {
barChart.visibility = View.VISIBLE
noDataTextView.visibility = View.GONE
// unix time span of all events
val startUnix = allEvents.minOf { it.getStartTime() }
val endUnix = allEvents.maxOf { it.getEndTime() }
// convert to days, weeks or months
val startSpan = unixToSpan(startUnix)
val endSpan = unixToSpan(endUnix)
//Log.d(TAG, "startUnix: $startUnix (${Date(1000 * startUnix)}), startSpan: $startSpan (${Date(1000 * spanToUnix(startSpan))}), endUnix: $endUnix (${Date(1000 * endUnix)}), endSpan: $endSpan (${Date(1000 * spanToUnix(endSpan))})")
for (span in startSpan..endSpan) {
values.add(BarEntry(values.size.toFloat(), 0F)) values.add(BarEntry(values.size.toFloat(), 0F))
labels.add(spanToLabel(span))
} }
for (event in allEvents) { // update value
if (dataTypeSelectionValue == "AMOUNT") { values[index].y += 1F
if (eventTypeSelectionValue == "SLEEP") {
// a sleep event can span to another day
// distribute sleep time over the days
val startUnix = event.getStartTime()
val endUnix = event.getEndTime()
val begIndex = unixToSpan(startUnix)
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) {
val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, endUnix: ${Date(endUnix * 1000)}")
val beg = max(mid, spanBegin)
val end = min(endUnix, spanEnd)
val index = i - startSpan
val duration = end - beg
//Log.d(TAG, "[$index] beg: ${Date(beg * 1000)}, end: ${Date(end * 1000)}, ${formatTimeDuration(this, duration)}")
values[index].y += duration
mid = end
}
} else {
val index = unixToSpan(event.time) - startSpan
//Log.d(TAG, "[${index}] ${event.quantity}")
values[index].y += event.quantity
}
} else {
val index = unixToSpan(event.time) - startSpan
values[index].y += 1
}
}
} else {
barChart.visibility = View.GONE
noDataTextView.visibility = View.VISIBLE
} }
barChart.xAxis.setLabelCount(min(values.size, 24)) fun accumulateValue(index: Int, value: Long) {
// create initial values
while (index >= values.size) {
values.add(BarEntry(values.size.toFloat(), 0F))
}
// update value
values[index].y += value.toFloat()
}
fun stackValue(index: Int, value: Long) {
// create initial values
while (index >= values.size) {
values.add(BarEntry(values.size.toFloat(), FloatArray(0)))
}
val x = values[index].x
val yVals = values[index].yVals
// update value
val newYVals = appendToFloatArray(yVals, value.toFloat())
values[index] = BarEntry(x, newYVals)
}
// awake/sleep
fun stackValuePattern(index: Int, spanBegin: Long, spanEnd: Long, begin: Long, end: Long) {
// create initial values
while (index >= values.size) {
values.add(BarEntry(values.size.toFloat(), FloatArray(0)))
}
assert(begin in spanBegin..spanEnd)
assert(end in spanBegin..spanEnd)
assert(begin <= end)
val x = values[index].x
val yVals = values[index].yVals // alternating sleep/awake durations
val y = yVals.fold(0F) { acc, next -> acc + next }
// y value is seconds when last awake
val awakeDuration = max(begin - spanBegin - y.toLong(), 0L)
val sleepDuration = end - begin
if ((awakeDuration + sleepDuration) > (spanEnd - spanBegin)) {
Log.e(TAG, "Invalid sleep duration, exceeds day/week or month bounds => ignore value")
return
}
// update value
val newYVals = appendToFloatArray(yVals, awakeDuration.toFloat(), sleepDuration.toFloat())
values[index] = BarEntry(x, newYVals)
}
/*
fun addStack24hCap(spanDuration: Long) {
// spanDuration is usually a day, week, month in seconds
Log.d(TAG, "spanDuration: $spanDuration, ${24*60*60}")
for (i in values.indices) {
val x = values[i].x
val yVals = values[i].yVals
val y = yVals.fold(0F) { acc, next -> acc + next }
val cap = spanDuration.toFloat() - y
if (cap >= 0F) {
// Add a cap value and an 0 value to keep the number of spans even.
// This is important, since we configure two colors and they will alternate.
val newYVals = appendToFloatArray(yVals, cap, 0F)
values[i] = BarEntry(x, newYVals)
} else {
Log.e(TAG, "Invalid remaining sleep duration, exceeds day/week or month bounds => ignore")
}
}
}
*/
for (range in ranges) {
// a sleep event can span to another day
// distribute sleep time over the days
val startUnix = range.start //event.time
val endUnix = range.end //getEndTime(event)
val begIndex = unixToSpan(startUnix)
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) {
val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, endUnix: ${Date(endUnix * 1000)}")
val sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd)
val index = i - startSpan
val duration = sleepEnd - sleepBegin
//Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${formatTimeDuration(this, duration)}")
if (graphTypeSelection == GraphType.SLEEP_PATTERN) {
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
} else if (graphTypeSelection == GraphType.SLEEP_SUM) {
stackValue(index, duration)
} else if (graphTypeSelection == GraphType.SLEEP_SUM_AVERAGE) {
accumulateValue(index, duration)
} else if (graphTypeSelection == GraphType.SLEEP_EVENTS) {
countEvent(index)
} else {
Log.e(TAG, "Unexpected graph type.")
return
}
mid = sleepEnd
}
// TODO: move addStack24h here, since the spans can have different length (edge case and does not really matter)
}
//addStack24hCap(spanToUnix(1) - spanToUnix(0))
// for debugging
for (value in values) {
val y = value.yVals.fold(0F) { acc, next -> acc + next }
val yVals = value.yVals.joinToString { it.toString() }
Log.d(TAG, "value: ${value.x} $y ($yVals)")
}
// list of dates
val labels = ArrayList<String>()
for (index in values.indices) {
labels.add(spanToLabel(spanToUnix(startSpan + index)))
}
val set1 = BarDataSet(values, "") val set1 = BarDataSet(values, "")
set1.setDrawValues(true) if (graphTypeSelection == GraphType.SLEEP_PATTERN) {
// awake phase color is transparent
set1.colors = arrayListOf("#00000000".toColorInt(), ColorTemplate.rgb("#72d7f5"))
set1.setDrawValues(false) // too many values => let's disable it
} else {
set1.setDrawValues(false)
}
set1.setDrawIcons(false) set1.setDrawIcons(false)
barChart.legend.isEnabled = false
barChart.xAxis.setLabelCount(min(values.size, 24))
val dataSets = ArrayList<IBarDataSet?>() val dataSets = ArrayList<IBarDataSet?>()
dataSets.add(set1) dataSets.add(set1)
@@ -242,17 +360,99 @@ class StatisticsActivity : AppCompatActivity() {
data.setValueFormatter(object : ValueFormatter() { data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String { override fun getFormattedValue(value: Float): String {
//Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}") //Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}")
return when (dataTypeSelectionValue) { return NumericUtils(applicationContext).formatEventQuantity(LunaEvent.TYPE_SLEEP, value.toInt())
"EVENT" -> value.toInt().toString() }
"AMOUNT" -> when (eventTypeSelectionValue) { })
"BOTTLE" -> NumericUtils(applicationContext).formatEventQuantity(LunaEvent.TYPE_BABY_BOTTLE, value.toInt())
"SLEEP" -> NumericUtils(applicationContext).formatEventQuantity(LunaEvent.TYPE_SLEEP, value.toInt()) barChart.xAxis.valueFormatter = object: ValueFormatter() {
else -> { override fun getFormattedValue(value: Float): String {
Log.e(TAG, "unhandled eventTypeSelectionValue: $eventTypeSelectionValue") return labels.getOrElse(value.toInt(), {"?"})
value.toInt().toString() }
} }
} else -> {
Log.e(TAG, "unhandled dataTypeSelectionValue: $dataTypeSelectionValue") //data.setValueTextSize(12f)
barChart.setData(data)
barChart.invalidate()
}
fun showBottleBarGraph(events: List<LunaEvent>, unixToSpan: (Long) -> Int, spanToUnix: (Int) -> Long) {
// unix time span of all events
val startUnix = events.minOf { it.time }
val endUnix = events.maxOf { it.time }
// convert to days, weeks or months
val startSpan = unixToSpan(startUnix)
val endSpan = unixToSpan(endUnix)
val values = ArrayList<BarEntry>()
fun countValue(index: Int) {
// create initial values
while (index >= values.size) {
values.add(BarEntry(values.size.toFloat(), 0F))
}
// update value
values[index].y += 1F
}
fun accumulateValue(index: Int, duration: Long) {
// create initial values
while (index >= values.size) {
values.add(BarEntry(values.size.toFloat(), 0F))
}
// update value
values[index].y += duration.toFloat()
}
for (event in events) {
if (graphTypeSelection == GraphType.BOTTLE_EVENTS) {
val index = unixToSpan(event.time) - startSpan
countValue(index)
} else if (graphTypeSelection == GraphType.BOTTLE_SUM) {
val index = unixToSpan(event.time) - startSpan
//Log.d(TAG, "[${index}] ${event.quantity}")
accumulateValue(index, event.quantity.toLong())
} else {
Log.e(TAG, "unhandled graphTypeSelection")
return
}
}
// list of dates
val labels = ArrayList<String>()
for (index in values.indices) {
labels.add(spanToLabel(spanToUnix(startSpan + index)))
}
val set1 = BarDataSet(values, "")
set1.setDrawValues(true)
set1.setDrawIcons(false)
//showGraph(set1, valueLabels)
// for debugging
//val sum1 = allEvents.fold(0) { acc, event -> acc + event.quantity }
//val sum2 = values.fold(0F) { acc, item -> acc + item.y }
//Log.d(TAG, "sum1: $sum1, sum2: $sum2")
barChart.xAxis.setLabelCount(min(values.size, 24))
val dataSets = ArrayList<IBarDataSet?>()
dataSets.add(set1)
val data = BarData(dataSets)
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
//Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}")
return when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS -> value.toInt().toString()
GraphType.BOTTLE_SUM -> NumericUtils(applicationContext).formatEventQuantity(LunaEvent.TYPE_BABY_BOTTLE, value.toInt())
else -> {
Log.e(TAG, "unhandled graphTypeSelection")
value.toInt().toString() value.toInt().toString()
} }
} }
@@ -270,6 +470,64 @@ class StatisticsActivity : AppCompatActivity() {
barChart.invalidate() barChart.invalidate()
} }
fun spanToLabel(unixSeconds: Long): String {
val dateTime = Calendar.getInstance()
dateTime.time = Date(1000L * unixSeconds)
val year = dateTime.get(Calendar.YEAR)
val month = dateTime.get(Calendar.MONTH) + 1 // month starts at 0
val week = dateTime.get(Calendar.WEEK_OF_YEAR)
val day = dateTime.get(Calendar.DAY_OF_MONTH)
return when (timeRangeSelection) {
RangeType.DAY -> "$day/$month/$year"
RangeType.WEEK -> "$week/$year"
RangeType.MONTH -> "$month/$year"
}
}
fun updateGraph() {
val unixToSpan = when (timeRangeSelection) {
RangeType.DAY -> { unix: Long -> unixToDays(unix) }
RangeType.WEEK -> { unix: Long -> unixToWeeks(unix) }
RangeType.MONTH -> { unix: Long -> unixToMonths(unix) }
}
val spanToUnix = when (timeRangeSelection) {
RangeType.DAY -> { span: Int -> daysToUnix(span) }
RangeType.WEEK -> { span: Int -> weeksToUnix(span) }
RangeType.MONTH -> { span: Int -> monthsToUnix(span) }
}
val eventType = when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM,
GraphType.BOTTLE_SUM_AVERAGE -> LunaEvent.TYPE_BABY_BOTTLE
GraphType.SLEEP_SUM,
GraphType.SLEEP_SUM_AVERAGE,
GraphType.SLEEP_EVENTS,
GraphType.SLEEP_PATTERN -> LunaEvent.TYPE_SLEEP
}
val events = MainActivity.allEvents.filter { it.type == eventType }.sortedBy { it.time }
if (events.isEmpty()) {
barChart.visibility = View.GONE
noDataTextView.visibility = View.VISIBLE
} else {
barChart.visibility = View.VISIBLE
noDataTextView.visibility = View.GONE
when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM,
GraphType.BOTTLE_SUM_AVERAGE -> showBottleBarGraph(events, unixToSpan, spanToUnix)
GraphType.SLEEP_SUM,
GraphType.SLEEP_SUM_AVERAGE,
GraphType.SLEEP_EVENTS,
GraphType.SLEEP_PATTERN -> showSleepBarGraph(events, unixToSpan, spanToUnix)
}
}
}
private interface SpinnerItemSelected { private interface SpinnerItemSelected {
fun call(newValue: String?) fun call(newValue: String?)
} }
@@ -284,7 +542,7 @@ class StatisticsActivity : AppCompatActivity() {
val arrayValues = resources.getStringArray(arrayValuesId) val arrayValues = resources.getStringArray(arrayValuesId)
val spinner = findViewById<Spinner>(spinnerId) val spinner = findViewById<Spinner>(spinnerId)
val spinnerAdapter = val spinnerAdapter =
ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item) //android.R.layout.simple_spinner_item) //android.R.layout.simple_list_item_single_choice) //R.layout.spinner_item_settings) ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item)
spinner.adapter = spinnerAdapter spinner.adapter = spinnerAdapter
spinner.setSelection(arrayValues.indexOf(currentValue)) spinner.setSelection(arrayValues.indexOf(currentValue))
@@ -325,7 +583,6 @@ class StatisticsActivity : AppCompatActivity() {
dateTime.time = Date(0) dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, months / 12) dateTime.set(Calendar.YEAR, months / 12)
dateTime.set(Calendar.MONTH, months % 12) dateTime.set(Calendar.MONTH, months % 12)
//Calendar.MONTH_OF_YEAR?
dateTime.set(Calendar.HOUR, 0) dateTime.set(Calendar.HOUR, 0)
dateTime.set(Calendar.MINUTE, 0) dateTime.set(Calendar.MINUTE, 0)
dateTime.set(Calendar.SECOND, 0) dateTime.set(Calendar.SECOND, 0)
@@ -371,5 +628,19 @@ class StatisticsActivity : AppCompatActivity() {
dateTime.set(Calendar.SECOND, 0) dateTime.set(Calendar.SECOND, 0)
return dateTime.time.time / 1000 return dateTime.time.time / 1000
} }
fun appendToFloatArray(array: FloatArray, vararg values: Float): FloatArray {
// create new array
val newArray = FloatArray(array.size + values.size)
// copy old values
for (i in array.indices) {
newArray[i] = array[i]
}
// add new values
for (i in values.indices) {
newArray[array.size + i] = values[i]
}
return newArray
}
} }
} }

View File

@@ -52,18 +52,22 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
) )
holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.textColor)) holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.textColor))
// Contents // Contents
holder.type.text = item.getTypeEmoji(context) holder.type.text = item.getHeaderEmoji(context)
holder.description.text = when (item.type) { holder.description.text = when (item.type) {
LunaEvent.TYPE_MEDICINE -> item.notes LunaEvent.Type.MEDICINE -> item.notes
LunaEvent.TYPE_NOTE -> item.notes LunaEvent.Type.NOTE -> item.notes
LunaEvent.TYPE_CUSTOM -> item.notes else -> item.getRowItemTitle(context)
else -> item.getTypeDescription(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) var quantityText = numericUtils.formatEventQuantity(item)
// if the event is weight, show difference with the last one // if the event is weight, show difference with the last one
if (item.type == LunaEvent.TYPE_WEIGHT) { if (item.type == LunaEvent.Type.WEIGHT) {
val lastWeight = getPreviousWeightEvent(position) val lastWeight = getPreviousWeightEvent(position)
if (lastWeight != null) { if (lastWeight != null) {
val differenceInWeight = item.quantity - lastWeight.quantity val differenceInWeight = item.quantity - lastWeight.quantity
@@ -94,7 +98,7 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
return null return null
for (pos in startFromPosition + 1 until items.size) { for (pos in startFromPosition + 1 until items.size) {
val item = items.get(pos) val item = items.get(pos)
if (item.type != LunaEvent.TYPE_WEIGHT) if (item.type != LunaEvent.Type.WEIGHT)
continue continue
return item return item
} }

View File

@@ -12,25 +12,24 @@ import java.util.Date
* release, it is simply ignored by previous ones). * release, it is simply ignored by previous ones).
*/ */
class LunaEvent: Comparable<LunaEvent> { class LunaEvent: Comparable<LunaEvent> {
enum class Type {
companion object { BABY_BOTTLE,
const val TYPE_BABY_BOTTLE = "BABY_BOTTLE" FOOD,
const val TYPE_WEIGHT = "WEIGHT" BREASTFEEDING_LEFT_NIPPLE,
const val TYPE_BREASTFEEDING_LEFT_NIPPLE = "BREASTFEEDING_LEFT_NIPPLE" BREASTFEEDING_BOTH_NIPPLE,
const val TYPE_BREASTFEEDING_BOTH_NIPPLE = "BREASTFEEDING_BOTH_NIPPLE" BREASTFEEDING_RIGHT_NIPPLE,
const val TYPE_BREASTFEEDING_RIGHT_NIPPLE = "BREASTFEEDING_RIGHT_NIPPLE" DIAPERCHANGE_POO,
const val TYPE_DIAPERCHANGE_POO = "DIAPERCHANGE_POO" DIAPERCHANGE_PEE,
const val TYPE_DIAPERCHANGE_PEE = "DIAPERCHANGE_PEE" SLEEP,
const val TYPE_MEDICINE = "MEDICINE" WEIGHT,
const val TYPE_ENEMA = "ENEMA" MEDICINE,
const val TYPE_NOTE = "NOTE" ENEMA,
const val TYPE_CUSTOM = "CUSTOM" NOTE,
const val TYPE_COLIC = "COLIC" COLIC,
const val TYPE_TEMPERATURE = "TEMPERATURE" TEMPERATURE,
const val TYPE_FOOD = "FOOD" PUKE,
const val TYPE_PUKE = "PUKE" BATH,
const val TYPE_BATH = "BATH" UNKNOWN
const val TYPE_SLEEP = "SLEEP"
} }
private val jo: JSONObject private val jo: JSONObject
@@ -40,10 +39,16 @@ class LunaEvent: Comparable<LunaEvent> {
set(value) { set(value) {
jo.put("time", value) jo.put("time", value)
} }
var type: String var type: Type
get(): String = jo.getString("type") get(): Type {
return try {
Type.valueOf(jo.getString("type"))
} catch (_: Exception) {
Type.UNKNOWN
}
}
set(value) { set(value) {
jo.put("type", value) jo.put("type", value.name)
} }
var quantity: Int var quantity: Int
get() = jo.optInt("quantity") get() = jo.optInt("quantity")
@@ -81,93 +86,33 @@ class LunaEvent: Comparable<LunaEvent> {
this.signature = event.signature this.signature = event.signature
} }
constructor(type: String) { constructor(type: Type) {
this.jo = JSONObject() this.jo = JSONObject()
this.time = System.currentTimeMillis() / 1000 this.time = System.currentTimeMillis() / 1000
this.type = type this.type = type
} }
constructor(type: String, quantity: Int) { constructor(type: Type, quantity: Int) {
this.jo = JSONObject() this.jo = JSONObject()
this.time = System.currentTimeMillis() / 1000 this.time = System.currentTimeMillis() / 1000
this.type = type this.type = type
this.quantity = quantity this.quantity = quantity
} }
fun getStartTime(): Long { fun getHeaderEmoji(context: Context): String {
return time return getHeaderEmoji(context, type)
} }
fun getEndTime(): Long { fun getDialogTitle(context: Context): String {
return if (type == TYPE_SLEEP) { return getDialogTitle(context, type)
time + quantity
} else {
time
}
} }
fun getTypeEmoji(context: Context): String { fun getRowItemTitle(context: Context): String {
return context.getString( return getPopupItemTitle(context, type).split(" ", limit = 2).last() // remove emoji
when (type) {
TYPE_BABY_BOTTLE -> R.string.event_bottle_type
TYPE_WEIGHT -> R.string.event_weight_type
TYPE_BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_type
TYPE_BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_type
TYPE_BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_type
TYPE_DIAPERCHANGE_POO -> R.string.event_diaperchange_poo_type
TYPE_DIAPERCHANGE_PEE -> R.string.event_diaperchange_pee_type
TYPE_MEDICINE -> R.string.event_medicine_type
TYPE_ENEMA -> R.string.event_enema_type
TYPE_NOTE -> R.string.event_note_type
TYPE_TEMPERATURE -> R.string.event_temperature_type
TYPE_COLIC -> R.string.event_colic_type
TYPE_FOOD -> R.string.event_food_type
TYPE_PUKE -> R.string.event_puke_type
TYPE_BATH -> R.string.event_bath_type
TYPE_SLEEP -> R.string.event_sleep_type
else -> R.string.event_unknown_type
}
)
} }
fun getTypeDescription(context: Context): String { fun getDialogMessage(context: Context): String {
return context.getString( return getDialogMessage(context, type)
when (type) {
TYPE_BABY_BOTTLE -> R.string.event_bottle_desc
TYPE_WEIGHT -> R.string.event_weight_desc
TYPE_BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_desc
TYPE_BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_desc
TYPE_BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_desc
TYPE_DIAPERCHANGE_POO -> R.string.event_diaperchange_poo_desc
TYPE_DIAPERCHANGE_PEE -> R.string.event_diaperchange_pee_desc
TYPE_MEDICINE -> R.string.event_medicine_desc
TYPE_ENEMA -> R.string.event_enema_desc
TYPE_NOTE -> R.string.event_note_desc
TYPE_TEMPERATURE -> R.string.event_temperature_desc
TYPE_COLIC -> R.string.event_colic_desc
TYPE_FOOD -> R.string.event_food_desc
TYPE_PUKE -> R.string.event_puke_desc
TYPE_BATH -> R.string.event_bath_desc
TYPE_SLEEP -> R.string.event_sleep_desc
else -> R.string.event_unknown_desc
}
)
}
fun getDialogMessage(context: Context): String? {
return context.getString(
when(type) {
TYPE_BABY_BOTTLE -> R.string.log_bottle_dialog_description
TYPE_MEDICINE -> R.string.log_medicine_dialog_description
TYPE_TEMPERATURE -> R.string.log_temperature_dialog_description
TYPE_DIAPERCHANGE_POO,
TYPE_DIAPERCHANGE_PEE,
TYPE_PUKE -> R.string.log_amount_dialog_description
TYPE_WEIGHT -> R.string.log_weight_dialog_description
TYPE_SLEEP -> R.string.log_sleep_dialog_description
else -> R.string.log_unknown_dialog_description
}
)
} }
fun toJson(): JSONObject { fun toJson(): JSONObject {
@@ -181,4 +126,95 @@ class LunaEvent: Comparable<LunaEvent> {
override fun compareTo(other: LunaEvent): Int { override fun compareTo(other: LunaEvent): Int {
return (this.time - other.time).toInt() return (this.time - other.time).toInt()
} }
}
companion object {
fun getHeaderEmoji(context: Context, type: Type): String {
return context.getString(
when (type) {
Type.BABY_BOTTLE -> R.string.event_bottle_type
Type.WEIGHT -> R.string.event_weight_type
Type.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_type
Type.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_type
Type.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_type
Type.DIAPERCHANGE_POO -> R.string.event_diaperchange_poo_type
Type.DIAPERCHANGE_PEE -> R.string.event_diaperchange_pee_type
Type.MEDICINE -> R.string.event_medicine_type
Type.ENEMA -> R.string.event_enema_type
Type.NOTE -> R.string.event_note_type
Type.TEMPERATURE -> R.string.event_temperature_type
Type.COLIC -> R.string.event_colic_type
Type.FOOD -> R.string.event_food_type
Type.PUKE -> R.string.event_puke_type
Type.BATH -> R.string.event_bath_type
Type.SLEEP -> R.string.event_sleep_type
Type.UNKNOWN -> R.string.event_unknown_type
}
)
}
fun getDialogMessage(context: Context, type: Type): String {
return context.getString(
when (type) {
Type.BABY_BOTTLE -> R.string.log_bottle_dialog_description
Type.MEDICINE -> R.string.log_medicine_dialog_description
Type.TEMPERATURE -> R.string.log_temperature_dialog_description
Type.DIAPERCHANGE_POO,
Type.DIAPERCHANGE_PEE,
Type.PUKE -> R.string.log_amount_dialog_description
Type.WEIGHT -> R.string.log_weight_dialog_description
Type.SLEEP -> R.string.log_sleep_dialog_description
else -> R.string.log_unknown_dialog_description
}
)
}
fun getDialogTitle(context: Context, type: Type): String {
return context.getString(
when (type) {
Type.BABY_BOTTLE -> R.string.event_bottle_desc
Type.WEIGHT -> R.string.event_weight_desc
Type.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_desc
Type.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_desc
Type.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_desc
Type.DIAPERCHANGE_POO -> R.string.event_diaperchange_poo_desc
Type.DIAPERCHANGE_PEE -> R.string.event_diaperchange_pee_desc
Type.MEDICINE -> R.string.event_medicine_desc
Type.ENEMA -> R.string.event_enema_desc
Type.NOTE -> R.string.event_note_desc
Type.TEMPERATURE -> R.string.event_temperature_desc
Type.COLIC -> R.string.event_colic_desc
Type.FOOD -> R.string.event_food_desc
Type.PUKE -> R.string.event_puke_desc
Type.BATH -> R.string.event_bath_desc
Type.SLEEP -> R.string.event_sleep_desc
Type.UNKNOWN -> R.string.event_unknown_desc
}
)
}
// Entries for for popup list
fun getPopupItemTitle(context: Context, type: Type): String {
return context.getString(
when (type) {
Type.BABY_BOTTLE -> R.string.event_type_item_bottle
Type.WEIGHT -> R.string.event_type_item_weight
Type.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_type_item_breastfeeding_left
Type.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_type_item_breastfeeding_both
Type.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_type_item_breastfeeding_right
Type.DIAPERCHANGE_POO -> R.string.event_type_item_diaperchange_poo
Type.DIAPERCHANGE_PEE -> R.string.event_type_item_diaperchange_pee
Type.MEDICINE -> R.string.event_type_item_medicine
Type.ENEMA -> R.string.event_type_item_enema
Type.NOTE -> R.string.event_type_item_note
Type.TEMPERATURE -> R.string.event_type_item_temperature
Type.COLIC -> R.string.event_type_item_colic
Type.FOOD -> R.string.event_type_item_food
Type.PUKE -> R.string.event_type_item_puke
Type.BATH -> R.string.event_type_item_bath
Type.SLEEP -> R.string.event_type_item_sleep
Type.UNKNOWN -> R.string.event_type_item_unknown
}
)
}
}
}

View File

@@ -13,7 +13,7 @@ class LocalSettingsRepository(val context: Context) {
const val SHARED_PREFS_DAV_URL = "webdav_url" const val SHARED_PREFS_DAV_URL = "webdav_url"
const val SHARED_PREFS_DAV_USER = "webdav_user" const val SHARED_PREFS_DAV_USER = "webdav_user"
const val SHARED_PREFS_DAV_PASS = "webdav_password" const val SHARED_PREFS_DAV_PASS = "webdav_password"
const val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding" const val SHARED_PREFS_DYNAMIC_MENU = "dynamic_menu"
const val SHARED_PREFS_SIGNATURE = "signature" const val SHARED_PREFS_SIGNATURE = "signature"
} }
enum class DATA_REPO {LOCAL_FILE, WEBDAV} enum class DATA_REPO {LOCAL_FILE, WEBDAV}
@@ -31,12 +31,12 @@ class LocalSettingsRepository(val context: Context) {
return sharedPreferences.getString(SHARED_PREFS_SIGNATURE, "") ?: "" return sharedPreferences.getString(SHARED_PREFS_SIGNATURE, "") ?: ""
} }
fun saveNoBreastfeeding(content: Boolean) { fun saveDynamicMenu(content: Boolean) {
sharedPreferences.edit { putBoolean(SHARED_PREFS_NO_BREASTFEEDING, content) } sharedPreferences.edit { putBoolean(SHARED_PREFS_DYNAMIC_MENU, content) }
} }
fun loadNoBreastfeeding(): Boolean { fun loadDynamicMenu(): Boolean {
return sharedPreferences.getBoolean(SHARED_PREFS_NO_BREASTFEEDING, false) return sharedPreferences.getBoolean(SHARED_PREFS_DYNAMIC_MENU, false)
} }
fun saveDataRepository(repo: DATA_REPO) { fun saveDataRepository(repo: DATA_REPO) {

View File

@@ -66,51 +66,51 @@ class NumericUtils (val context: Context) {
return formatEventQuantity(event.type, event.quantity) return formatEventQuantity(event.type, event.quantity)
} }
fun formatEventQuantity(type: String, quantity: Int): String { fun formatEventQuantity(type: LunaEvent.Type, quantity: Int): String {
val formatted = StringBuilder() val formatted = StringBuilder()
if (quantity > 0) { if (quantity > 0) {
formatted.append(when (type) { formatted.append(when (type) {
LunaEvent.TYPE_TEMPERATURE -> LunaEvent.Type.TEMPERATURE ->
(quantity / 10.0f).toString() (quantity / 10.0f).toString()
LunaEvent.TYPE_DIAPERCHANGE_POO, LunaEvent.Type.DIAPERCHANGE_POO,
LunaEvent.TYPE_DIAPERCHANGE_PEE, LunaEvent.Type.DIAPERCHANGE_PEE,
LunaEvent.TYPE_PUKE -> { LunaEvent.Type.PUKE -> {
val array = context.resources.getStringArray(R.array.AmountLabels) val array = context.resources.getStringArray(R.array.AmountLabels)
return array.getOrElse(quantity) { return array.getOrElse(quantity) {
Log.e("NumericUtils", "Invalid index $quantity") Log.e("NumericUtils", "Invalid index $quantity")
return "" return ""
} }
} }
LunaEvent.TYPE_SLEEP -> formatTimeDuration(context, quantity.toLong()) LunaEvent.Type.SLEEP -> formatTimeDuration(context, quantity.toLong())
else -> quantity else -> quantity
}) })
formatted.append(" ") formatted.append(" ")
formatted.append( formatted.append(
when (type) { when (type) {
LunaEvent.TYPE_BABY_BOTTLE -> measurement_unit_liquid_base LunaEvent.Type.BABY_BOTTLE -> measurement_unit_liquid_base
LunaEvent.TYPE_WEIGHT -> measurement_unit_weight_base LunaEvent.Type.WEIGHT -> measurement_unit_weight_base
LunaEvent.TYPE_MEDICINE -> measurement_unit_weight_tiny LunaEvent.Type.MEDICINE -> measurement_unit_weight_tiny
LunaEvent.TYPE_TEMPERATURE -> measurement_unit_temperature_base LunaEvent.Type.TEMPERATURE -> measurement_unit_temperature_base
else -> "" else -> ""
} }
) )
} else { } else {
formatted.append(when (type) { formatted.append(when (type) {
LunaEvent.TYPE_SLEEP -> "💤" // baby is sleeping LunaEvent.Type.SLEEP -> "💤" // baby is sleeping
else -> "" else -> ""
}) })
} }
return formatted.toString() return formatted.toString().trim()
} }
/** /**
* Returns a valid quantity range for the event type. * Returns a valid quantity range for the event type.
* @return min, max, normal * @return min, max, normal
*/ */
fun getValidEventQuantityRange(lunaEventType: String): Triple<Int, Int, Int>? { fun getValidEventQuantityRange(lunaEventType: LunaEvent.Type): Triple<Int, Int, Int>? {
return when (lunaEventType) { return when (lunaEventType) {
LunaEvent.TYPE_TEMPERATURE -> { LunaEvent.Type.TEMPERATURE -> {
if (isMetricSystem()) if (isMetricSystem())
Triple( Triple(
context.resources.getInteger(R.integer.human_body_temp_min_metric), context.resources.getInteger(R.integer.human_body_temp_min_metric),

View File

@@ -86,15 +86,16 @@
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:id="@+id/linear_layout_row1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
android:id="@+id/button_bottle" android:id="@+id/button1_row1"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="2" android:layout_weight="1"
android:layout_margin="5dp" android:layout_margin="5dp"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
@@ -102,7 +103,7 @@
android:text="@string/event_bottle_type"/> android:text="@string/event_bottle_type"/>
<TextView <TextView
android:id="@+id/button_food" android:id="@+id/button2_row1"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
@@ -115,77 +116,73 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:id="@+id/linear_layout_row2"
android:layout_height="wrap_content"
android:id="@+id/layout_nipples">
<TextView
android:id="@+id/button_nipple_left"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🤱⬅️"/>
<TextView
android:id="@+id/button_nipple_both"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🤱↔️"/>
<TextView
android:id="@+id/button_nipple_right"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🤱➡️️"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/button_change_poo" android:id="@+id/button1_row2"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="5dp" android:layout_margin="5dp"
android:layout_weight="2" android:layout_weight="1"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:textSize="30sp" android:textSize="30sp"/>
android:text="🚼 💩"/>
<TextView <TextView
android:id="@+id/button_change_pee" android:id="@+id/button2_row2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"/>
<TextView
android:id="@+id/button3_row2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/linear_layout_row3"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/button1_row3"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="5dp" android:layout_margin="5dp"
android:layout_weight="2" android:layout_weight="2"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:textSize="30sp" android:textSize="30sp"/>
android:text="🚼 💧"/>
<TextView
android:id="@+id/button2_row3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="2"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"/>
<ImageView <ImageView
android:id="@+id/button_more" android:id="@+id/button_more"
android:layout_width="0dp" android:layout_width="60dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_margin="5dp" android:layout_margin="5dp"
android:layout_weight="1" android:layout_weight="0"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:src="@drawable/ic_more" android:src="@drawable/ic_more"

View File

@@ -137,7 +137,7 @@
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="30dp" android:layout_marginStart="20dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:text="@string/settings_signature_desc"/> android:text="@string/settings_signature_desc"/>
@@ -149,13 +149,13 @@
<TextView <TextView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:textStyle="bold" android:textStyle="bold"
android:text="@string/settings_no_breastfeeding" /> android:text="@string/settings_dynamic_menu" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_no_breastfeeding" android:id="@+id/switch_dynamic_menu"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
@@ -166,9 +166,9 @@
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="30dp" android:layout_marginStart="20dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:text="@string/settings_no_breastfeeding_desc"/> android:text="@string/settings_dynamic_menu_desc"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -29,25 +29,27 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="0" android:layout_weight="0"
android:layout_marginTop="10dp"
android:orientation="horizontal"> android:orientation="horizontal">
<Spinner <Spinner
android:id="@+id/type_selection" android:id="@+id/type_selection"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1" /> android:layout_weight="1" />
<!--
<Spinner <Spinner
android:id="@+id/data_selection" android:id="@+id/data_selection"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"/> android:layout_weight="1"/>
-->
<Spinner <Spinner
android:id="@+id/time_selection" android:id="@+id/time_selection"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"/> android:layout_weight="1"/>
</LinearLayout> </LinearLayout>

View File

@@ -6,6 +6,7 @@
android:background="@color/transparent"> android:background="@color/transparent">
<LinearLayout <LinearLayout
android:id="@+id/layout_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
@@ -14,102 +15,12 @@
android:id="@+id/button_statistics" android:id="@+id/button_statistics"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp" android:padding="10dp"
android:background="@drawable/dropdown_list_item_background" android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText" style="@style/OverflowMenuText"
android:text="📊 Statistics"/> android:text="📊 Statistics"/>
<TextView <!-- Other buttons are inserted dynamically -->
android:id="@+id/button_medicine"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_medicine"/>
<TextView
android:id="@+id/button_note"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_note"/>
<TextView
android:id="@+id/button_temperature"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_temperature"/>
<TextView
android:id="@+id/button_puke"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_puke"/>
<TextView
android:id="@+id/button_sleep"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_sleep"/>
<TextView
android:id="@+id/button_colic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_colic"/>
<TextView
android:id="@+id/button_scale"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_weight"/>
<TextView
android:id="@+id/button_bath"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_bath"/>
<TextView
android:id="@+id/button_enema"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_enema"/>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="Item Template"/>

View File

@@ -8,15 +8,23 @@
</string-array> </string-array>
<string-array name="StatisticsTypeLabels"> <string-array name="StatisticsTypeLabels">
<item>Bottle</item> <item>BOTTLE_EVENTS</item>
<item>Sleep</item> <item>BOTTLE_SUM</item>
<item>BOTTLE_SUM_AVERAGE</item>
<item>SLEEP_SUM_AVERAGE</item>
<item>SLEEP_EVENTS</item>
<item>SLEEP_PATTERN</item>
</string-array> </string-array>
<string-array name="StatisticsTypeValues"> <string-array name="StatisticsTypeValues">
<item>BOTTLE</item> <item>BOTTLE_EVENTS</item>
<item>SLEEP</item> <item>BOTTLE_SUM</item>
<item>BOTTLE_SUM_AVERAGE</item>
<item>SLEEP_SUM_AVERAGE</item>
<item>SLEEP_EVENTS</item>
<item>SLEEP_PATTERN</item>
</string-array> </string-array>
<!--
<string-array name="StatisticsDataLabels"> <string-array name="StatisticsDataLabels">
<item>Event</item> <item>Event</item>
<item>Amount</item> <item>Amount</item>
@@ -26,7 +34,7 @@
<item>EVENT</item> <item>EVENT</item>
<item>AMOUNT</item> <item>AMOUNT</item>
</string-array> </string-array>
-->
<string-array name="StatisticsTimeLabels"> <string-array name="StatisticsTimeLabels">
<item>Day</item> <item>Day</item>
<item>Week</item> <item>Week</item>

View File

@@ -3,12 +3,13 @@
<string name="title">🌜 LunaTracker 🌛</string> <string name="title">🌜 LunaTracker 🌛</string>
<string name="logbook">Logged events</string> <string name="logbook">Logged events</string>
<!-- menu header items -->
<string name="event_bottle_type" translatable="false">🍼</string> <string name="event_bottle_type" translatable="false">🍼</string>
<string name="event_food_type" translatable="false">🥣</string> <string name="event_food_type" translatable="false">🥣</string>
<string name="event_weight_type" translatable="false">⚖️</string> <string name="event_weight_type" translatable="false">⚖️</string>
<string name="event_breastfeeding_left_type" translatable="false">🤱</string> <string name="event_breastfeeding_left_type" translatable="false">🤱⬅️</string>
<string name="event_breastfeeding_both_type" translatable="false">🤱 </string> <string name="event_breastfeeding_both_type" translatable="false">🤱↔</string>
<string name="event_breastfeeding_right_type" translatable="false">🤱</string> <string name="event_breastfeeding_right_type" translatable="false">🤱➡️️</string>
<string name="event_diaperchange_poo_type" translatable="false">🚼 💩</string> <string name="event_diaperchange_poo_type" translatable="false">🚼 💩</string>
<string name="event_diaperchange_pee_type" translatable="false">🚼 💧</string> <string name="event_diaperchange_pee_type" translatable="false">🚼 💧</string>
<string name="event_medicine_type" translatable="false">💊</string> <string name="event_medicine_type" translatable="false">💊</string>
@@ -19,16 +20,36 @@
<string name="event_puke_type" translatable="false">🤮</string> <string name="event_puke_type" translatable="false">🤮</string>
<string name="event_bath_type" translatable="false">🛁</string> <string name="event_bath_type" translatable="false">🛁</string>
<string name="event_sleep_type" translatable="false">💤</string> <string name="event_sleep_type" translatable="false">💤</string>
<string name="event_unknown_type" translatable="false">\?</string> <string name="event_unknown_type" translatable="false"></string>
<string name="event_bottle_desc">Baby bottle</string> <!-- dropdown menu item -->
<string name="event_type_item_bottle">🍼 Bottle</string>
<string name="event_type_item_food">🥣 Food</string>
<string name="event_type_item_weight">⚖️ Weight</string>
<string name="event_type_item_breastfeeding_left">🤱⬅️ Nursing</string>
<string name="event_type_item_breastfeeding_both">🤱↔️ Nursing</string>
<string name="event_type_item_breastfeeding_right">🤱➡️️ Nursing</string>
<string name="event_type_item_diaperchange_poo">🚼💩 Diaper</string>
<string name="event_type_item_diaperchange_pee">🚼💧 Diaper</string>
<string name="event_type_item_medicine">💊 Medicine</string>
<string name="event_type_item_enema">🪠 Enema</string>
<string name="event_type_item_note">📝 Note</string>
<string name="event_type_item_temperature">🌡️ Temperature</string>
<string name="event_type_item_colic">💨 Colic</string>
<string name="event_type_item_puke">🤮 Puke</string>
<string name="event_type_item_sleep">💤 Sleep</string>
<string name="event_type_item_bath">🛁 Bath</string>
<string name="event_type_item_unknown">❓ Unknown</string>
<!-- dialog titles -->
<string name="event_bottle_desc">Milk Bottle</string>
<string name="event_food_desc">Food</string> <string name="event_food_desc">Food</string>
<string name="event_weight_desc">Weight</string> <string name="event_weight_desc">Weight</string>
<string name="event_breastfeeding_left_desc">Breastfeeding (left)</string> <string name="event_breastfeeding_left_desc">Nursing (left)</string>
<string name="event_breastfeeding_both_desc">Breastfeeding</string> <string name="event_breastfeeding_both_desc">Nursing (both)</string>
<string name="event_breastfeeding_right_desc">Breastfeeding (right)</string> <string name="event_breastfeeding_right_desc">Nursing (right)</string>
<string name="event_diaperchange_poo_desc">Diaper chg (poo)</string> <string name="event_diaperchange_poo_desc">Diaper Change (poo)</string>
<string name="event_diaperchange_pee_desc">Diaper chg (pee)</string> <string name="event_diaperchange_pee_desc">Diaper Change (pee)</string>
<string name="event_medicine_desc">Medicine</string> <string name="event_medicine_desc">Medicine</string>
<string name="event_enema_desc">Enema</string> <string name="event_enema_desc">Enema</string>
<string name="event_note_desc">Note</string> <string name="event_note_desc">Note</string>
@@ -37,17 +58,7 @@
<string name="event_puke_desc">Puke</string> <string name="event_puke_desc">Puke</string>
<string name="event_bath_desc">Bath</string> <string name="event_bath_desc">Bath</string>
<string name="event_sleep_desc">Sleep</string> <string name="event_sleep_desc">Sleep</string>
<string name="event_unknown_desc"></string> <string name="event_unknown_desc">Unknown</string>
<string name="overflow_event_weight">⚖️ Weight</string>
<string name="overflow_event_medicine">💊 Medicine</string>
<string name="overflow_event_enema">🪠 Enema</string>
<string name="overflow_event_note">📝 Note</string>
<string name="overflow_event_temperature">🌡️ Temperature</string>
<string name="overflow_event_colic">💨 Gaseous colic</string>
<string name="overflow_event_puke">🤮 Puke</string>
<string name="overflow_event_sleep">💤 Sleep</string>
<string name="overflow_event_bath">🛁 Bath</string>
<string name="toast_event_added">Event logged</string> <string name="toast_event_added">Event logged</string>
<string name="toast_logbook_saved">Logbook saved</string> <string name="toast_logbook_saved">Logbook saved</string>
@@ -79,6 +90,8 @@
<string name="statistics_title">Statistics</string> <string name="statistics_title">Statistics</string>
<string name="settings_dynamic_menu">Dynamic Menu</string>
<string name="settings_dynamic_menu_desc">Populate the header menu with the most used events.</string>
<string name="settings_title">Settings</string> <string name="settings_title">Settings</string>
<string name="settings_signature">Signature</string> <string name="settings_signature">Signature</string>
<string name="settings_signature_desc">Attach a signature to each event you create and for others to see. Useful if multiple people add events.</string> <string name="settings_signature_desc">Attach a signature to each event you create and for others to see. Useful if multiple people add events.</string>
@@ -97,8 +110,6 @@
<string name="settings_webdav_error_generic">Error while trying to access WebDAV:</string> <string name="settings_webdav_error_generic">Error while trying to access WebDAV:</string>
<string name="settings_webdav_creation_error_generic">Unable to save a file on the WebDAV server:</string> <string name="settings_webdav_creation_error_generic">Unable to save a file on the WebDAV server:</string>
<string name="settings_webdav_creation_ok">Successfully connected with the WebDAV server</string> <string name="settings_webdav_creation_ok">Successfully connected with the WebDAV server</string>
<string name="settings_no_breastfeeding">No Breastfeeding</string>
<string name="settings_no_breastfeeding_desc">Hide the Breastfeeding buttons for when they are not needed.</string>
<string name="settings_json_error">There\'s a save file on the server, but it is corrupted or unreadable. Please delete it </string> <string name="settings_json_error">There\'s a save file on the server, but it is corrupted or unreadable. Please delete it </string>
<string name="settings_generic_error">Error: </string> <string name="settings_generic_error">Error: </string>
<string name="settings_webdav_upload_error">Error while uploading local logbook %1$s to webdav: %2$s</string> <string name="settings_webdav_upload_error">Error while uploading local logbook %1$s to webdav: %2$s</string>

View File

@@ -4,6 +4,11 @@
<style name="Theme.LunaTracker" parent="Theme.AppCompat.NoActionBar"> <style name="Theme.LunaTracker" parent="Theme.AppCompat.NoActionBar">
<item name="colorAccent">@color/accent</item> <item name="colorAccent">@color/accent</item>
<item name="android:textColor">@color/textColor</item> <item name="android:textColor">@color/textColor</item>
<!-- make the screen not overlap with the system bars -->
<item name="android:fitsSystemWindows">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style> </style>
<style name="OverflowMenuText"> <style name="OverflowMenuText">

View File

@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.12.0" agp = "8.13.0"
kotlin = "2.0.0" kotlin = "2.0.0"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"
@@ -13,6 +13,7 @@ mpandroidchart = "v4.2.2"
mpandroidchartVersion = "v3.1.0" mpandroidchartVersion = "v3.1.0"
recyclerview = "1.3.2" recyclerview = "1.3.2"
material = "1.12.0" material = "1.12.0"
sardineAndroid = "v0.9"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -34,6 +35,7 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
material = { group = "com.google.android.material", name = "material", version.ref = "material" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }
mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchart" } mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchart" }
mpandroidchart-vv310 = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchartVersion" } mpandroidchart-vv310 = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchartVersion" }
sardine-android = { module = "com.github.thegrizzlylabs:sardine-android", version.ref = "sardineAndroid" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }