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
21 changed files with 1241 additions and 396 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)
@@ -60,4 +65,5 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.mpandroidchart.vv310)
} }

View File

@@ -30,6 +30,10 @@
android:name=".SettingsActivity" android:name=".SettingsActivity"
android:label="@string/settings_title" android:label="@string/settings_title"
android:theme="@style/Theme.LunaTracker"/> android:theme="@style/Theme.LunaTracker"/>
<activity
android:name=".StatisticsActivity"
android:label="@string/statistics_title"
android:theme="@style/Theme.LunaTracker"/>
</application> </application>
</manifest> </manifest>

View File

@@ -6,6 +6,7 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.text.Editable
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -14,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
@@ -50,6 +52,9 @@ class MainActivity : AppCompatActivity() {
const val TAG = "MainActivity" const val TAG = "MainActivity"
const val UPDATE_EVERY_SECS: Long = 30 const val UPDATE_EVERY_SECS: Long = 30
const val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false const val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false
// list of all events
var allEvents = arrayListOf<LunaEvent>()
var currentPopupItems = listOf<LunaEvent.Type>()
} }
var logbook: Logbook? = null var logbook: Logbook? = null
@@ -81,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)
@@ -121,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()
}
} }
} }
@@ -145,7 +136,84 @@ class MainActivity : AppCompatActivity() {
if (logbook == null) if (logbook == null)
Log.w(TAG, "showLogbook(): logbook is null!") Log.w(TAG, "showLogbook(): logbook is null!")
setListAdapter(logbook?.logs ?: arrayListOf()) allEvents = logbook?.logs ?: arrayListOf()
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() {
@@ -168,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()
@@ -192,10 +254,6 @@ class MainActivity : AppCompatActivity() {
super.onStop() super.onStop()
} }
fun getAllEvents(): ArrayList<LunaEvent> {
return logbook?.logs ?: arrayListOf()
}
fun addBabyBottleEvent(event: LunaEvent) { fun addBabyBottleEvent(event: LunaEvent) {
setToPreviousQuantity(event) setToPreviousQuantity(event)
askBabyBottleContent(event, true) { askBabyBottleContent(event, true) {
@@ -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) {
@@ -363,7 +421,7 @@ class MainActivity : AppCompatActivity() {
} }
fun saveEvent(event: LunaEvent) { fun saveEvent(event: LunaEvent) {
if (!getAllEvents().contains(event)) { if (!allEvents.contains(event)) {
// new event // new event
logEvent(event) logEvent(event)
} }
@@ -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)
@@ -557,7 +614,7 @@ class MainActivity : AppCompatActivity() {
val nextTextView = dialogView.findViewById<TextView>(R.id.notes_template_next) val nextTextView = dialogView.findViewById<TextView>(R.id.notes_template_next)
val prevTextView = dialogView.findViewById<TextView>(R.id.notes_template_prev) val prevTextView = dialogView.findViewById<TextView>(R.id.notes_template_prev)
val templates = getAllEvents().filter { it.type == event.type }.distinctBy { it.notes.trim() }.sortedBy { it.time } val templates = allEvents.filter { it.type == event.type }.distinctBy { it.notes }.sortedBy { it.time }
fun updateContent(current: LunaEvent) { fun updateContent(current: LunaEvent) {
val prevEvent = getPreviousSameEvent(current, templates) val prevEvent = getPreviousSameEvent(current, templates)
@@ -612,7 +669,7 @@ class MainActivity : AppCompatActivity() {
updateContent(event) updateContent(event)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val notes = notesET.text.toString() val notes = notesET.text.toString().trim()
if (useQuantity) { if (useQuantity) {
val quantity = qtyET.text.toString().toIntOrNull() val quantity = qtyET.text.toString().toIntOrNull()
@@ -663,7 +720,7 @@ class MainActivity : AppCompatActivity() {
} }
fun setToPreviousQuantity(event: LunaEvent) { fun setToPreviousQuantity(event: LunaEvent) {
val prev = getPreviousSameEvent(event, getAllEvents()) val prev = getPreviousSameEvent(event, allEvents)
if (prev != null) { if (prev != null) {
event.quantity = prev.quantity event.quantity = prev.quantity
} }
@@ -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}")
}
} }
} }
@@ -808,13 +871,11 @@ class MainActivity : AppCompatActivity() {
signatureTextEdit.visibility = View.VISIBLE signatureTextEdit.visibility = View.VISIBLE
} }
val allEvents = getAllEvents()
// create link to prevent event of the same type // create link to prevent event of the same type
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 {
@@ -829,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 {
@@ -1000,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}")
} }
@@ -1185,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
@@ -1193,41 +1277,31 @@ 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)
contentView.findViewById<View>(R.id.button_statistics).setOnClickListener {
if (logbook != null && !pauseLogbookUpdate) {
val i = Intent(applicationContext, StatisticsActivity::class.java)
i.putExtra("LOOGBOOK_NAME", logbook!!.name)
startActivity(i)
} else {
Toast.makeText(applicationContext, "No logbook selected!", Toast.LENGTH_SHORT).show()
}
dismiss() dismiss()
} }
contentView.findViewById<View>(R.id.button_enema).setOnClickListener {
addPlainEvent(LunaEvent(LunaEvent.TYPE_ENEMA)) val linearLayout = contentView.findViewById<LinearLayout>(R.id.layout_list)
dismiss()
} // Add buttons to create other events
contentView.findViewById<View>(R.id.button_note).setOnClickListener { for (type in currentPopupItems) {
addNoteEvent(LunaEvent(LunaEvent.TYPE_NOTE)) val view = layoutInflater.inflate(R.layout.more_events_popup_item, linearLayout, false)
dismiss() val textView = view.findViewById<TextView>(R.id.tv)
} textView.text = LunaEvent.getPopupItemTitle(applicationContext, type)
contentView.findViewById<View>(R.id.button_temperature).setOnClickListener { textView.setOnClickListener {
addTemperatureEvent(LunaEvent(LunaEvent.TYPE_TEMPERATURE)) showCreateDialog(type)
dismiss() dismiss()
} }
contentView.findViewById<View>(R.id.button_puke).setOnClickListener { linearLayout.addView(textView)
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()
} }
}.also { popupWindow -> }.also { popupWindow ->
popupWindow.setOnDismissListener({ popupWindow.setOnDismissListener({

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

@@ -0,0 +1,646 @@
package it.danieleverducci.lunatracker
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.formatter.ValueFormatter
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet
import it.danieleverducci.lunatracker.entities.LunaEvent
import utils.NumericUtils
import java.util.Calendar
import java.util.Date
import kotlin.math.max
import kotlin.math.min
import androidx.core.graphics.toColorInt
class StatisticsActivity : AppCompatActivity() {
lateinit var barChart: BarChart
lateinit var noDataTextView: TextView
lateinit var graphTypeSpinner: Spinner
//lateinit var dataTypeSelection: 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
var graphTypeSelection = GraphType.SLEEP_SUM
var timeRangeSelection = RangeType.DAY
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_statistics)
val logbookName = intent.getStringExtra("LOOGBOOK_NAME")
if (logbookName == null) {
finish()
return
}
noDataTextView = findViewById(R.id.no_data)
barChart = findViewById(R.id.bar_chart)
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)
graphTypeSpinner = findViewById(R.id.type_selection)
//dataTypeSelection = findViewById(R.id.data_selection)
timeRangeSpinner = findViewById(R.id.time_selection)
setupSpinner("SLEEP_SUM",
R.id.type_selection,
R.array.StatisticsTypeLabels,
R.array.StatisticsTypeValues,
object : SpinnerItemSelected {
override fun call(newValue: String?) {
newValue ?: return
graphTypeSelection = when (newValue) {
"BOTTLE_EVENTS" -> GraphType.BOTTLE_EVENTS
"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,
R.id.data_selection,
R.array.StatisticsDataLabels,
R.array.StatisticsDataValues,
object : SpinnerItemSelected {
override fun call(newValue: String?) {
if (newValue != null) {
dataTypeSelectionValue = newValue
//Log.d("event", "new value: $newValue")
updateGraph()
}
}
}
)
*/
setupSpinner("DAY",
R.id.time_selection,
R.array.StatisticsTimeLabels,
R.array.StatisticsTimeValues,
object : SpinnerItemSelected {
override fun call(newValue: String?) {
newValue ?: return
timeRangeSelection = when (newValue) {
"DAY" -> RangeType.DAY
"WEEK" -> RangeType.WEEK
"MONTH" -> RangeType.MONTH
else -> {
Log.e(TAG, "Invalid time range selection: $newValue")
return
}
}
//Log.d("event", "new value: $newValue")
updateGraph()
}
}
)
updateGraph()
}
fun showSleepBarGraph(events: List<LunaEvent>, unixToSpan: (Long) -> Int, spanToUnix: (Int) -> Long) {
fun getEndTime(event: LunaEvent): Long {
if (event.quantity == 0) {
// sleep is still ongoing
val dateTime = Calendar.getInstance()
return (dateTime.time.time / 1000) - event.time
} else {
return event.quantity.toLong()
}
}
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>()
fun countEvent(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, 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, "")
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)
barChart.legend.isEnabled = false
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 NumericUtils(applicationContext).formatEventQuantity(LunaEvent.TYPE_SLEEP, value.toInt())
}
})
barChart.xAxis.valueFormatter = object: ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return labels.getOrElse(value.toInt(), {"?"})
}
}
//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()
}
}
}
})
barChart.xAxis.valueFormatter = object: ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return labels.getOrElse(value.toInt(), {"?"})
}
}
data.setValueTextSize(12f)
barChart.setData(data)
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 {
fun call(newValue: String?)
}
private fun setupSpinner(
currentValue: String,
spinnerId: Int,
arrayId: Int,
arrayValuesId: Int,
callback: SpinnerItemSelected
) {
val arrayValues = resources.getStringArray(arrayValuesId)
val spinner = findViewById<Spinner>(spinnerId)
val spinnerAdapter =
ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item)
spinner.adapter = spinnerAdapter
spinner.setSelection(arrayValues.indexOf(currentValue))
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
var check = 0
override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
if (pos >= arrayValues.size) {
Toast.makeText(
this@StatisticsActivity,
"pos out of bounds: $arrayValues", Toast.LENGTH_SHORT
).show()
return
}
if (check++ > 0) {
callback.call(arrayValues[pos])
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// ignore
}
}
}
companion object {
const val TAG = "StatisticsActivity"
fun unixToMonths(seconds: Long): Int {
val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR)
val months = dateTime.get(Calendar.MONTH)
return 12 * years + months
}
fun monthsToUnix(months: Int): Long {
val dateTime = Calendar.getInstance()
dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, months / 12)
dateTime.set(Calendar.MONTH, months % 12)
dateTime.set(Calendar.HOUR, 0)
dateTime.set(Calendar.MINUTE, 0)
dateTime.set(Calendar.SECOND, 0)
return dateTime.time.time / 1000
}
fun unixToWeeks(seconds: Long): Int {
val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR)
val weeks = dateTime.get(Calendar.WEEK_OF_YEAR)
return 52 * years + weeks
}
fun weeksToUnix(weeks: Int): Long {
val dateTime = Calendar.getInstance()
dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, weeks / 52)
dateTime.set(Calendar.WEEK_OF_YEAR, weeks % 52)
dateTime.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
dateTime.set(Calendar.HOUR, 0)
dateTime.set(Calendar.MINUTE, 0)
dateTime.set(Calendar.SECOND, 0)
return dateTime.time.time / 1000
}
fun unixToDays(seconds: Long): Int {
val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR)
val days = dateTime.get(Calendar.DAY_OF_YEAR)
return 365 * years + days
}
// convert from days to Date
fun daysToUnix(days: Int): Long {
val dateTime = Calendar.getInstance()
dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, days / 365)
dateTime.set(Calendar.DAY_OF_YEAR, days % 365)
dateTime.set(Calendar.HOUR, 0)
dateTime.set(Calendar.MINUTE, 0)
dateTime.set(Calendar.SECOND, 0)
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.time) 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,81 +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 getTypeEmoji(context: Context): String { fun getHeaderEmoji(context: Context): String {
return context.getString( return getHeaderEmoji(context, type)
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 getDialogTitle(context: Context): String {
return context.getString( return getDialogTitle(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? { 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.log_bottle_dialog_description
TYPE_MEDICINE -> R.string.log_medicine_dialog_description fun getDialogMessage(context: Context): String {
TYPE_TEMPERATURE -> R.string.log_temperature_dialog_description return getDialogMessage(context, type)
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 {
@@ -169,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

@@ -63,51 +63,54 @@ class NumericUtils (val context: Context) {
} }
fun formatEventQuantity(event: LunaEvent): String { fun formatEventQuantity(event: LunaEvent): String {
return formatEventQuantity(event.type, event.quantity)
}
fun formatEventQuantity(type: LunaEvent.Type, quantity: Int): String {
val formatted = StringBuilder() val formatted = StringBuilder()
if (event.quantity > 0) { if (quantity > 0) {
formatted.append(when (event.type) { formatted.append(when (type) {
LunaEvent.TYPE_TEMPERATURE -> LunaEvent.Type.TEMPERATURE ->
(event.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(event.quantity) { return array.getOrElse(quantity) {
Log.e("NumericUtils", "Invalid index ${event.quantity}") Log.e("NumericUtils", "Invalid index $quantity")
return "" return ""
} }
} }
LunaEvent.TYPE_SLEEP -> formatTimeDuration(context, event.quantity.toLong()) LunaEvent.Type.SLEEP -> formatTimeDuration(context, quantity.toLong())
else -> else -> quantity
event.quantity
}) })
formatted.append(" ") formatted.append(" ")
formatted.append( formatted.append(
when (event.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 (event.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

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<com.github.mikephil.charting.charts.HorizontalBarChart
android:id="@+id/bar_chart"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/no_data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:gravity="center"
android:text="No Data"/>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"
android:orientation="horizontal">
<Spinner
android:id="@+id/type_selection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1" />
<!--
<Spinner
android:id="@+id/data_selection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"/>
-->
<Spinner
android:id="@+id/time_selection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"/>
</LinearLayout>
</LinearLayout>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@@ -6,99 +6,21 @@
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">
<TextView <TextView
android:id="@+id/button_medicine" 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: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="@string/overflow_event_medicine"/> android:text="📊 Statistics"/>
<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"/>
<!-- Other buttons are inserted dynamically -->
</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

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:gravity="center"
android:padding="5dip" />

View File

@@ -6,4 +6,45 @@
<item>@string/amount_normal</item> <item>@string/amount_normal</item>
<item>@string/amount_plenty</item> <item>@string/amount_plenty</item>
</string-array> </string-array>
<string-array name="StatisticsTypeLabels">
<item>BOTTLE_EVENTS</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 name="StatisticsTypeValues">
<item>BOTTLE_EVENTS</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 name="StatisticsDataLabels">
<item>Event</item>
<item>Amount</item>
</string-array>
<string-array name="StatisticsDataValues">
<item>EVENT</item>
<item>AMOUNT</item>
</string-array>
-->
<string-array name="StatisticsTimeLabels">
<item>Day</item>
<item>Week</item>
<item>Month</item>
</string-array>
<string-array name="StatisticsTimeValues">
<item>DAY</item>
<item>WEEK</item>
<item>MONTH</item>
</string-array>
</resources> </resources>

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>
@@ -77,6 +88,10 @@
<string name="no_connection_go_to_settings">Settings</string> <string name="no_connection_go_to_settings">Settings</string>
<string name="no_connection_retry">Retry</string> <string name="no_connection_retry">Retry</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>
@@ -95,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

@@ -9,8 +9,11 @@ lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0" activityCompose = "1.8.0"
composeBom = "2024.04.01" composeBom = "2024.04.01"
appcompat = "1.7.0" appcompat = "1.7.0"
mpandroidchart = "v4.2.2"
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" }
@@ -30,6 +33,9 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
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-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" }

View File

@@ -16,7 +16,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven(url = "https://jitpack.io") maven(url = uri("https://jitpack.io"))
} }
} }