6 Commits

16 changed files with 390 additions and 105 deletions

View File

@ -8,3 +8,5 @@ You have to react fast, event if it's 4 AM and you have no idea why is crying.
This app is meant to log all the relevant events (diaper change, breastfeeding, baby bottle feeding...), so you can always remember when has been done last time. It supports syncing the data between different devices (using your WebDAV server for the best privacy) so that when the baby needs attentions, the dad can step in leaving the mom sleeping peacefully. This app is meant to log all the relevant events (diaper change, breastfeeding, baby bottle feeding...), so you can always remember when has been done last time. It supports syncing the data between different devices (using your WebDAV server for the best privacy) so that when the baby needs attentions, the dad can step in leaving the mom sleeping peacefully.
Dedicated to my daughter Luna. Dedicated to my daughter Luna.
![Screenshot](fastlane/metadata/android/en-US/images/phoneScreenshots/1.png)

View File

@ -13,7 +13,7 @@ android {
minSdk = 29 minSdk = 29
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "0.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@ -4,9 +4,12 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText
import android.widget.NumberPicker import android.widget.NumberPicker
import android.widget.PopupWindow
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -15,11 +18,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.thegrizzlylabs.sardineandroid.impl.SardineException import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.SettingsActivity
import it.danieleverducci.lunatracker.adapters.LunaEventRecyclerAdapter import it.danieleverducci.lunatracker.adapters.LunaEventRecyclerAdapter
import it.danieleverducci.lunatracker.entities.Logbook import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.entities.LunaEventType
import it.danieleverducci.lunatracker.repository.FileLogbookRepository import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookLoadedListener import it.danieleverducci.lunatracker.repository.LogbookLoadedListener
@ -33,6 +34,7 @@ import org.json.JSONException
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
companion object { companion object {
val TAG = "MainActivity" val TAG = "MainActivity"
val UPDATE_EVERY_SECS: Long = 30
} }
lateinit var logbook: Logbook lateinit var logbook: Logbook
@ -68,29 +70,33 @@ class MainActivity : AppCompatActivity() {
findViewById<View>(R.id.button_scale).setOnClickListener { askWeightValue() } findViewById<View>(R.id.button_scale).setOnClickListener { askWeightValue() }
findViewById<View>(R.id.button_nipple_left).setOnClickListener { logEvent( findViewById<View>(R.id.button_nipple_left).setOnClickListener { logEvent(
LunaEvent( LunaEvent(
LunaEventType.BREASTFEEDING_LEFT_NIPPLE LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE
) )
) } ) }
findViewById<View>(R.id.button_nipple_both).setOnClickListener { logEvent( findViewById<View>(R.id.button_nipple_both).setOnClickListener { logEvent(
LunaEvent( LunaEvent(
LunaEventType.BREASTFEEDING_BOTH_NIPPLE LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE
) )
) } ) }
findViewById<View>(R.id.button_nipple_right).setOnClickListener { logEvent( findViewById<View>(R.id.button_nipple_right).setOnClickListener { logEvent(
LunaEvent( LunaEvent(
LunaEventType.BREASTFEEDING_RIGHT_NIPPLE LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
) )
) } ) }
findViewById<View>(R.id.button_change_poo).setOnClickListener { logEvent( findViewById<View>(R.id.button_change_poo).setOnClickListener { logEvent(
LunaEvent( LunaEvent(
LunaEventType.DIAPERCHANGE_POO LunaEvent.TYPE_DIAPERCHANGE_POO
) )
) } ) }
findViewById<View>(R.id.button_change_pee).setOnClickListener { logEvent( findViewById<View>(R.id.button_change_pee).setOnClickListener { logEvent(
LunaEvent( LunaEvent(
LunaEventType.DIAPERCHANGE_PEE LunaEvent.TYPE_DIAPERCHANGE_PEE
) )
) } ) }
val moreButton = findViewById<View>(R.id.button_more)
moreButton.setOnClickListener {
showOverflowPopupWindow(moreButton)
}
findViewById<View>(R.id.button_no_connection_settings).setOnClickListener({ findViewById<View>(R.id.button_no_connection_settings).setOnClickListener({
showSettings() showSettings()
}) })
@ -140,7 +146,6 @@ class MainActivity : AppCompatActivity() {
// Reload data // Reload data
loadLogbook() loadLogbook()
handler.postDelayed(updateListRunnable, 1000*30)
} }
override fun onStop() { override fun onStop() {
@ -164,7 +169,7 @@ class MainActivity : AppCompatActivity() {
numberPicker.wrapSelectorWheel = false numberPicker.wrapSelectorWheel = false
numberPicker.value = localSettings.loadBabyBottleContent() numberPicker.value = localSettings.loadBabyBottleContent()
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
logEvent(LunaEvent(LunaEventType.BABY_BOTTLE, numberPicker.value * 10)) logEvent(LunaEvent(LunaEvent.TYPE_BABY_BOTTLE, numberPicker.value * 10))
localSettings.saveBabyBottleContent(numberPicker.value) localSettings.saveBabyBottleContent(numberPicker.value)
} }
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
@ -179,11 +184,11 @@ class MainActivity : AppCompatActivity() {
d.setTitle(R.string.log_weight_dialog_title) d.setTitle(R.string.log_weight_dialog_title)
d.setMessage(R.string.log_weight_dialog_description) d.setMessage(R.string.log_weight_dialog_description)
d.setView(dialogView) d.setView(dialogView)
val weightET = dialogView.findViewById<TextView>(R.id.dialog_number_edittext) val weightET = dialogView.findViewById<EditText>(R.id.dialog_number_edittext)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val weight = weightET.text.toString().toIntOrNull() val weight = weightET.text.toString().toIntOrNull()
if (weight != null) if (weight != null)
logEvent(LunaEvent(LunaEventType.WEIGHT, weight)) logEvent(LunaEvent(LunaEvent.TYPE_WEIGHT, weight))
else else
Toast.makeText(this, R.string.toast_integer_error, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.toast_integer_error, Toast.LENGTH_SHORT).show()
} }
@ -192,10 +197,66 @@ class MainActivity : AppCompatActivity() {
alertDialog.show() alertDialog.show()
} }
fun askNotes(lunaEvent: LunaEvent) {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_notes, null)
d.setTitle(lunaEvent.getTypeDescription(this))
d.setMessage(
when (lunaEvent.type){
LunaEvent.TYPE_MEDICINE -> R.string.log_medicine_dialog_description
else -> R.string.log_notes_dialog_description
}
)
d.setView(dialogView)
val notesET = dialogView.findViewById<EditText>(R.id.notes_edittext)
val qtyET = dialogView.findViewById<EditText>(R.id.notes_qty_edittext)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val qtyStr = qtyET.text.toString()
if (qtyStr.isNotEmpty()) {
val qty = qtyStr.toIntOrNull()
if (qty == null) {
Toast.makeText(this, R.string.toast_integer_error, Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
lunaEvent.quantity = qty
}
val notes = notesET.text.toString()
lunaEvent.notes = notes
logEvent(lunaEvent)
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
val alertDialog = d.create()
alertDialog.show()
}
fun askToTrimLogbook() {
val d = AlertDialog.Builder(this)
d.setTitle(R.string.trim_logbook_dialog_title)
d.setMessage(
when (LocalSettingsRepository(this).loadDataRepository()) {
LocalSettingsRepository.DATA_REPO.WEBDAV -> R.string.trim_logbook_dialog_message_dav
else -> R.string.trim_logbook_dialog_message_local
}
)
d.setPositiveButton(R.string.trim_logbook_dialog_button_ok) { dialogInterface, i ->
logbook.trim()
saveLogbook()
}
d.setNegativeButton(R.string.trim_logbook_dialog_button_cancel) { dialogInterface, i ->
dialogInterface.dismiss()
}
val alertDialog = d.create()
alertDialog.show()
}
fun loadLogbook() { fun loadLogbook() {
if (savingEvent) if (savingEvent)
return return
// Reset time counter
handler.removeCallbacks(updateListRunnable)
handler.postDelayed(updateListRunnable, UPDATE_EVERY_SECS*1000)
// Load data // Load data
setLoading(true) setLoading(true)
logbookRepo?.loadLogbook(this, object: LogbookLoadedListener{ logbookRepo?.loadLogbook(this, object: LogbookLoadedListener{
@ -261,13 +322,33 @@ class MainActivity : AppCompatActivity() {
setLoading(true) setLoading(true)
logbook.logs.add(0, event) logbook.logs.add(0, event)
saveLogbook(event)
// Check logbook size to avoid OOM errors
if (logbook.isTooBig()) {
askToTrimLogbook()
}
}
/**
* Saves the logbook. If saving while adding an event, please specify the event so in case
* of error can be removed from the list.
*/
fun saveLogbook(lastEventAdded: LunaEvent? = null) {
logbookRepo?.saveLogbook(this, logbook, object: LogbookSavedListener{ logbookRepo?.saveLogbook(this, logbook, object: LogbookSavedListener{
override fun onLogbookSaved() { override fun onLogbookSaved() {
Log.d(TAG, "Logbook saved") Log.d(TAG, "Logbook saved")
runOnUiThread({ runOnUiThread({
setLoading(false) setLoading(false)
Toast.makeText(this@MainActivity, R.string.toast_event_added, Toast.LENGTH_SHORT).show() Toast.makeText(
this@MainActivity,
if (lastEventAdded != null)
R.string.toast_event_added
else
R.string.toast_logbook_saved,
Toast.LENGTH_SHORT
).show()
savingEvent(false) savingEvent(false)
}) })
} }
@ -276,7 +357,8 @@ class MainActivity : AppCompatActivity() {
runOnUiThread({ runOnUiThread({
setLoading(false) setLoading(false)
onRepoError(getString(R.string.settings_network_error) + error.toString()) onRepoError(getString(R.string.settings_network_error) + error.toString())
onAddError(event, error.toString()) if (lastEventAdded != null)
onAddError(lastEventAdded, error.toString())
}) })
} }
@ -290,7 +372,8 @@ class MainActivity : AppCompatActivity() {
getString(R.string.settings_webdav_error_generic) + error.toString() getString(R.string.settings_webdav_error_generic) + error.toString()
} }
) )
onAddError(event, error.toString()) if (lastEventAdded != null)
onAddError(lastEventAdded, error.toString())
}) })
} }
@ -298,7 +381,8 @@ class MainActivity : AppCompatActivity() {
runOnUiThread({ runOnUiThread({
setLoading(false) setLoading(false)
onRepoError(getString(R.string.settings_json_error) + error.toString()) onRepoError(getString(R.string.settings_json_error) + error.toString())
onAddError(event, error.toString()) if (lastEventAdded != null)
onAddError(lastEventAdded, error.toString())
}) })
} }
@ -306,7 +390,8 @@ class MainActivity : AppCompatActivity() {
runOnUiThread({ runOnUiThread({
setLoading(false) setLoading(false)
onRepoError(getString(R.string.settings_generic_error) + error.toString()) onRepoError(getString(R.string.settings_generic_error) + error.toString())
onAddError(event, error.toString()) if (lastEventAdded != null)
onAddError(lastEventAdded, error.toString())
}) })
} }
}) })
@ -342,4 +427,25 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun showOverflowPopupWindow(anchor: View) {
PopupWindow(anchor.context).apply {
isOutsideTouchable = true
val inflater = LayoutInflater.from(anchor.context)
contentView = inflater.inflate(R.layout.more_events_popup, null)
contentView.findViewById<View>(R.id.button_medicine).setOnClickListener({
askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE))
dismiss()
})
contentView.findViewById<View>(R.id.button_enema).setOnClickListener({
logEvent(LunaEvent(LunaEvent.TYPE_ENEMA))
dismiss()
})
contentView.findViewById<View>(R.id.button_custom).setOnClickListener({
Toast.makeText(anchor.context, "TODO: Implement custom events", Toast.LENGTH_SHORT).show()
dismiss()
})
}.also { popupWindow ->
popupWindow.showAsDropDown(anchor)
}
}
} }

View File

@ -2,14 +2,12 @@ package it.danieleverducci.lunatracker.adapters
import android.content.Context import android.content.Context
import android.text.format.DateFormat import android.text.format.DateFormat
import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import it.danieleverducci.lunatracker.entities.LunaEvent import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.entities.LunaEventType
import it.danieleverducci.lunatracker.R import it.danieleverducci.lunatracker.R
import java.util.Date import java.util.Date
@ -35,30 +33,8 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
position: Int position: Int
) { ) {
val item = items.get(position) val item = items.get(position)
holder.type.text = context.getString( holder.type.text = item.getTypeEmoji(context)
when (item.type) { holder.description.text = item.getTypeDescription(context)
LunaEventType.BABY_BOTTLE -> R.string.event_bottle_type
LunaEventType.WEIGHT -> R.string.event_scale_type
LunaEventType.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_type
LunaEventType.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_type
LunaEventType.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_type
LunaEventType.DIAPERCHANGE_POO -> R.string.event_diaperchange_poo_type
LunaEventType.DIAPERCHANGE_PEE -> R.string.event_diaperchange_pee_type
else -> R.string.event_unknown_type
}
)
holder.description.text = context.getString(
when (item.type) {
LunaEventType.BABY_BOTTLE -> R.string.event_bottle_desc
LunaEventType.WEIGHT -> R.string.event_scale_desc
LunaEventType.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_desc
LunaEventType.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_desc
LunaEventType.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_desc
LunaEventType.DIAPERCHANGE_POO -> R.string.event_diaperchange_poo_desc
LunaEventType.DIAPERCHANGE_PEE -> R.string.event_diaperchange_pee_desc
else -> R.string.event_unknown_desc
}
)
holder.quantity.text = if ((item.quantity ?: 0) > 0) item.quantity.toString() else "" holder.quantity.text = if ((item.quantity ?: 0) > 0) item.quantity.toString() else ""
holder.time.text = formatTimeAgo(context, item.time) holder.time.text = formatTimeAgo(context, item.time)
} }

View File

@ -1,5 +1,19 @@
package it.danieleverducci.lunatracker.entities package it.danieleverducci.lunatracker.entities
class Logbook { class Logbook {
companion object {
val MAX_SAFE_LOGBOOK_SIZE = 30000
}
val logs = ArrayList<LunaEvent>() val logs = ArrayList<LunaEvent>()
fun isTooBig(): Boolean {
return logs.size > MAX_SAFE_LOGBOOK_SIZE
}
/**
* Halves the logbook to avoid the file being too big
*/
fun trim() {
logs.subList(MAX_SAFE_LOGBOOK_SIZE/2, logs.size).clear()
}
} }

View File

@ -1,68 +1,114 @@
package it.danieleverducci.lunatracker.entities package it.danieleverducci.lunatracker.entities
import android.content.Context
import it.danieleverducci.lunatracker.R
import org.json.JSONObject import org.json.JSONObject
import java.util.Date import java.util.Date
class LunaEvent( /**
val type: LunaEventType, * Represents a logged event.
val quantity: Int? = null, * It encloses, but doesn't parse entirely, a jsonObject. This was done to
){ * allow expandability and backwards compatibility (if a field is added in a
var time: Long // In unix time (seconds since 1970) * release, it is simply ignored by previous ones).
*/
class LunaEvent {
init { companion object {
time = System.currentTimeMillis() / 1000 val TYPE_BABY_BOTTLE = "BABY_BOTTLE"
val TYPE_WEIGHT = "WEIGHT"
val TYPE_BREASTFEEDING_LEFT_NIPPLE = "BREASTFEEDING_LEFT_NIPPLE"
val TYPE_BREASTFEEDING_BOTH_NIPPLE = "BREASTFEEDING_BOTH_NIPPLE"
val TYPE_BREASTFEEDING_RIGHT_NIPPLE = "BREASTFEEDING_RIGHT_NIPPLE"
val TYPE_DIAPERCHANGE_POO = "DIAPERCHANGE_POO"
val TYPE_DIAPERCHANGE_PEE = "DIAPERCHANGE_PEE"
val TYPE_MEDICINE = "MEDICINE"
val TYPE_ENEMA = "ENEMA"
val TYPE_CUSTOM = "CUSTOM"
} }
override fun toString(): String { private val jo: JSONObject
return "${type.toString()} qty: $quantity time: ${Date(time * 1000)}"
var time: Long // In unix time (seconds since 1970)
get() = jo.getLong("time")
set(value) {
jo.put("time", value)
}
var type: String
get(): String = jo.getString("type")
set(value) {
jo.put("type", value)
}
var quantity: Int
get() = jo.optInt("quantity")
set(value) {
if (value > 0)
jo.put("quantity", value)
}
var notes: String
get(): String = jo.getString("notes")
set(value) {
jo.put("notes", value)
}
constructor(jo: JSONObject) {
this.jo = jo
// A minimal LunaEvent should have at least time and type
if (!jo.has("time") || !jo.has("type"))
throw IllegalArgumentException("JSONObject is not a LunaEvent")
}
constructor(type: String) {
this.jo = JSONObject()
this.time = System.currentTimeMillis() / 1000
this.type = type
}
constructor(type: String, quantity: Int) {
this.jo = JSONObject()
this.time = System.currentTimeMillis() / 1000
this.type = type
this.quantity = quantity
}
fun getTypeEmoji(context: Context): String {
return context.getString(
when (type) {
TYPE_BABY_BOTTLE -> R.string.event_bottle_type
TYPE_WEIGHT -> R.string.event_scale_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
else -> R.string.event_unknown_type
}
)
}
fun getTypeDescription(context: Context): String {
return context.getString(
when (type) {
TYPE_BABY_BOTTLE -> R.string.event_bottle_desc
TYPE_WEIGHT -> R.string.event_scale_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
else -> R.string.event_unknown_desc
}
)
} }
fun toJson(): JSONObject { fun toJson(): JSONObject {
val jo = JSONObject()
val type = when (type) {
LunaEventType.BABY_BOTTLE -> "BABY_BOTTLE"
LunaEventType.WEIGHT -> "SCALE"
LunaEventType.BREASTFEEDING_LEFT_NIPPLE -> "BREASTFEEDING_LEFT_NIPPLE"
LunaEventType.BREASTFEEDING_BOTH_NIPPLE -> "BREASTFEEDING_BOTH_NIPPLE"
LunaEventType.BREASTFEEDING_RIGHT_NIPPLE -> "BREASTFEEDING_RIGHT_NIPPLE"
LunaEventType.DIAPERCHANGE_POO -> "DIAPERCHANGE_POO"
LunaEventType.DIAPERCHANGE_PEE -> "DIAPERCHANGE_PEE"
else -> "UNKNOWN"
}
jo.put("type", type)
jo.put("quantity", quantity)
jo.put("time", time)
return jo return jo
} }
companion object { override fun toString(): String {
fun fromJson(j: JSONObject): LunaEvent { return "${type} qty: $quantity time: ${Date(time * 1000)}"
val type = when (j.getString("type")) {
"BABY_BOTTLE" -> LunaEventType.BABY_BOTTLE
"SCALE" -> LunaEventType.WEIGHT
"BREASTFEEDING_LEFT_NIPPLE" -> LunaEventType.BREASTFEEDING_LEFT_NIPPLE
"BREASTFEEDING_BOTH_NIPPLE" -> LunaEventType.BREASTFEEDING_BOTH_NIPPLE
"BREASTFEEDING_RIGHT_NIPPLE" -> LunaEventType.BREASTFEEDING_RIGHT_NIPPLE
"DIAPERCHANGE_POO" -> LunaEventType.DIAPERCHANGE_POO
"DIAPERCHANGE_PEE" -> LunaEventType.DIAPERCHANGE_PEE
else -> LunaEventType.UNKNOWN
}
val quantity = j.optInt("quantity")
val time = j.getLong("time")
val evt = LunaEvent(type, quantity)
evt.time = time
return evt
}
} }
}
enum class LunaEventType {
BABY_BOTTLE,
WEIGHT,
BREASTFEEDING_LEFT_NIPPLE,
BREASTFEEDING_BOTH_NIPPLE,
BREASTFEEDING_RIGHT_NIPPLE,
DIAPERCHANGE_POO,
DIAPERCHANGE_PEE,
UNKNOWN
} }

View File

@ -5,6 +5,7 @@ import it.danieleverducci.lunatracker.entities.Logbook
import android.util.Log import android.util.Log
import it.danieleverducci.lunatracker.entities.LunaEvent import it.danieleverducci.lunatracker.entities.LunaEvent
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -31,9 +32,13 @@ class FileLogbookRepository: LogbookRepository {
val json = FileInputStream(file).bufferedReader().use { it.readText() } val json = FileInputStream(file).bufferedReader().use { it.readText() }
val ja = JSONArray(json) val ja = JSONArray(json)
for (i in 0 until ja.length()) { for (i in 0 until ja.length()) {
val jo = ja.getJSONObject(i) try {
val evt = LunaEvent.fromJson(jo) val evt: LunaEvent = LunaEvent(ja.getJSONObject(i))
logbook.logs.add(evt) logbook.logs.add(evt)
} catch (e: IllegalArgumentException) {
// Mangled JSON?
throw JSONException(e.toString())
}
} }
return logbook return logbook
} }

View File

@ -59,10 +59,15 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
val ja = JSONArray(json) val ja = JSONArray(json)
val logbook = Logbook() val logbook = Logbook()
for (i in 0 until ja.length()) { for (i in 0 until ja.length()) {
val jo = ja.getJSONObject(i) try {
val evt = LunaEvent.fromJson(jo) val evt: LunaEvent = LunaEvent(ja.getJSONObject(i))
logbook.logs.add(evt) logbook.logs.add(evt)
} catch (e: IllegalArgumentException) {
// Mangled JSON?
throw JSONException(e.toString())
}
} }
Log.d(TAG, "Loaded ${logbook.logs.size} events into logbook")
return logbook return logbook
} }

View File

@ -3,6 +3,8 @@
<stroke <stroke
android:width="2dp" android:width="2dp"
android:color="@color/accent" /> android:color="@color/accent" />
<solid
android:color="@color/cardview_dark_background"/>
<corners android:radius="15dp" /> <corners android:radius="15dp" />
<padding <padding
android:bottom="5dp" android:bottom="5dp"

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -108,7 +108,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="10dp" android:layout_margin="10dp"
android:layout_weight="1" android:layout_weight="2"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:textSize="30dp" android:textSize="30dp"
@ -119,12 +119,23 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="10dp" android:layout_margin="10dp"
android:layout_weight="1" android:layout_weight="2"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:textSize="30dp" android:textSize="30dp"
android:text="🚼 💧"/> android:text="🚼 💧"/>
<ImageView
android:id="@+id/button_more"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:src="@drawable/ic_more"
app:tint="@android:color/darker_gray"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">
<EditText
android:id="@+id/notes_qty_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="1"
android:inputType="number"
android:hint="@string/log_notes_dialog_qty_hint"/>
<EditText
android:id="@+id/notes_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:lines="10"/>
</LinearLayout>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
android:background="@color/transparent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/button_medicine"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
android:background="@drawable/button_background"
android:text="@string/overflow_event_medicine"/>
<TextView
android:id="@+id/button_enema"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/button_background"
android:text="@string/overflow_event_enema"/>
<LinearLayout
android:id="@+id/overflow_event_custom_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
<TextView
android:id="@+id/button_custom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/button_background"
android:text="@string/overflow_event_custom"/>
</LinearLayout>
</ScrollView>

View File

@ -17,8 +17,14 @@
<string name="event_breastfeeding_right_type">🤱 →</string> <string name="event_breastfeeding_right_type">🤱 →</string>
<string name="event_diaperchange_poo_type">🚼 💩</string> <string name="event_diaperchange_poo_type">🚼 💩</string>
<string name="event_diaperchange_pee_type">🚼 💧</string> <string name="event_diaperchange_pee_type">🚼 💧</string>
<string name="event_medicine_type">💊</string>
<string name="event_enema_type">🪠</string>
<string name="event_unknown_type">-</string> <string name="event_unknown_type">-</string>
<string name="overflow_event_medicine">💊 Medicina</string>
<string name="overflow_event_enema">🪠 Clistere</string>
<string name="overflow_event_custom"> Aggiungi evento personalizzato</string>
<string name="event_bottle_desc">Biberon</string> <string name="event_bottle_desc">Biberon</string>
<string name="event_scale_desc">Pesata</string> <string name="event_scale_desc">Pesata</string>
<string name="event_breastfeeding_left_desc">Allatt. al seno (sx)</string> <string name="event_breastfeeding_left_desc">Allatt. al seno (sx)</string>
@ -26,9 +32,12 @@
<string name="event_breastfeeding_right_desc">Allatt. al seno (dx)</string> <string name="event_breastfeeding_right_desc">Allatt. al seno (dx)</string>
<string name="event_diaperchange_poo_desc">Cambio (con cacca)</string> <string name="event_diaperchange_poo_desc">Cambio (con cacca)</string>
<string name="event_diaperchange_pee_desc">Cambio (con pipì)</string> <string name="event_diaperchange_pee_desc">Cambio (con pipì)</string>
<string name="event_medicine_desc">Medicina</string>
<string name="event_enema_desc">Clistere</string>
<string name="event_unknown_desc"></string> <string name="event_unknown_desc"></string>
<string name="toast_event_added">Evento aggiunto</string> <string name="toast_event_added">Evento aggiunto</string>
<string name="toast_logbook_saved">Diario salvato</string>
<string name="toast_event_add_error">Impossibile aggiungere l\'evento</string> <string name="toast_event_add_error">Impossibile aggiungere l\'evento</string>
<string name="toast_integer_error">Valore non valido. Inserire un numero intero.</string> <string name="toast_integer_error">Valore non valido. Inserire un numero intero.</string>
@ -60,4 +69,15 @@
<string name="settings_webdav_creation_ok">Connessione al server WebDAV avvenuta con successo</string> <string name="settings_webdav_creation_ok">Connessione al server WebDAV avvenuta con successo</string>
<string name="settings_json_error">Sul server esiste un salvataggio, ma è corrotto o illeggibile. Cancellare il file </string> <string name="settings_json_error">Sul server esiste un salvataggio, ma è corrotto o illeggibile. Cancellare il file </string>
<string name="settings_generic_error">Si è verificato un errore: </string> <string name="settings_generic_error">Si è verificato un errore: </string>
<string name="trim_logbook_dialog_title">Il tuo diario è bello grande!</string>
<string name="trim_logbook_dialog_message_local">Il file del tuo diario sta crescendo molto. Ti suggeriamo di cancellare gli eventi più vecchi per evitare problemi di memoria.</string>
<string name="trim_logbook_dialog_message_dav">Il file del tuo diario sta crescendo molto. Ti suggeriamo di cancellare gli eventi più vecchi per evitare problemi di memoria. Se vuoi conservare tutto lo storico, fai un backup del file "lunatracker_logbook.json" sul tuo server WebDAV o rinominalo per mantenerlo così e cominciare un nuovo diario.</string>
<string name="trim_logbook_dialog_button_ok">Cancella i più vecchi</string>
<string name="trim_logbook_dialog_button_cancel">Ricordamelo dopo</string>
<string name="log_notes_dialog_description">Note:</string>
<string name="log_medicine_dialog_description">Nome della medicina, quantità, formato, note…:</string>
<string name="log_notes_dialog_qty_hint">Quantità (numero intero), o lascia vuoto</string>
</resources> </resources>

View File

@ -4,5 +4,6 @@
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="translucent">#c000</color> <color name="translucent">#c000</color>
<color name="transparent">#0000</color>
<color name="grey">#ccc</color> <color name="grey">#ccc</color>
</resources> </resources>

View File

@ -17,7 +17,9 @@
<string name="event_breastfeeding_right_type">🤱 →</string> <string name="event_breastfeeding_right_type">🤱 →</string>
<string name="event_diaperchange_poo_type">🚼 💩</string> <string name="event_diaperchange_poo_type">🚼 💩</string>
<string name="event_diaperchange_pee_type">🚼 💧</string> <string name="event_diaperchange_pee_type">🚼 💧</string>
<string name="event_unknown_type">-</string> <string name="event_medicine_type">💊</string>
<string name="event_enema_type">🪠</string>
<string name="event_unknown_type">\?</string>
<string name="event_bottle_desc">Baby bottle</string> <string name="event_bottle_desc">Baby bottle</string>
<string name="event_scale_desc">Weight</string> <string name="event_scale_desc">Weight</string>
@ -26,9 +28,16 @@
<string name="event_breastfeeding_right_desc">Breastfeeding (right)</string> <string name="event_breastfeeding_right_desc">Breastfeeding (right)</string>
<string name="event_diaperchange_poo_desc">Diaper chg (poo)</string> <string name="event_diaperchange_poo_desc">Diaper chg (poo)</string>
<string name="event_diaperchange_pee_desc">Diaper chg (pee)</string> <string name="event_diaperchange_pee_desc">Diaper chg (pee)</string>
<string name="event_medicine_desc">Medicine</string>
<string name="event_enema_desc">Enema</string>
<string name="event_unknown_desc"></string> <string name="event_unknown_desc"></string>
<string name="overflow_event_medicine">💊 Medicine</string>
<string name="overflow_event_enema">🪠 Enema</string>
<string name="overflow_event_custom"> Add custom event</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_event_add_error">Unable to log the event</string> <string name="toast_event_add_error">Unable to log the event</string>
<string name="toast_integer_error">Invalid value. Insert an integer.</string> <string name="toast_integer_error">Invalid value. Insert an integer.</string>
@ -48,7 +57,7 @@
<string name="settings_storage_local">On device</string> <string name="settings_storage_local">On device</string>
<string name="settings_storage_local_desc">Most privacy-friendly solution: data doesn\'t leave your device</string> <string name="settings_storage_local_desc">Most privacy-friendly solution: data doesn\'t leave your device</string>
<string name="settings_storage_dav">On a WebDAV server</string> <string name="settings_storage_dav">On a WebDAV server</string>
<string name="settings_storage_dav_desc">You can use any WebDAV service (i.e. Nextcloud, Owncloud, Dropbox, Box...) to save the data. In this way you can syncronize it between more devices, i.e. the dad\'s, the mom\'s, the grandma\'s... You will need the WebDAV url, you can find it in your provider\'s documentation (i.e. in Nextcloud web is in the Files app settings)</string> <string name="settings_storage_dav_desc">You can use any WebDAV service (i.e. Nextcloud, Owncloud, Dropbox, Box) to save the data. In this way you can syncronize it between more devices, i.e. the dad\'s, the mom\'s, the grandma\'s You will need the WebDAV url, you can find it in your provider\'s documentation (i.e. in Nextcloud web is in the Files app settings)</string>
<string name="settings_storage_dav_url">WebDAV url</string> <string name="settings_storage_dav_url">WebDAV url</string>
<string name="settings_storage_dav_url_hint">https://</string> <string name="settings_storage_dav_url_hint">https://</string>
<string name="settings_storage_dav_user">Username</string> <string name="settings_storage_dav_user">Username</string>
@ -60,4 +69,15 @@
<string name="settings_webdav_creation_ok">Successfully connected with WebDAV server</string> <string name="settings_webdav_creation_ok">Successfully connected with WebDAV server</string>
<string name="settings_json_error">There\'s a save file on the server, but is corrupted or unreadable. Please delete it </string> <string name="settings_json_error">There\'s a save file on the server, but is corrupted or unreadable. Please delete it </string>
<string name="settings_generic_error">Error: </string> <string name="settings_generic_error">Error: </string>
<string name="trim_logbook_dialog_title">Your logbook is pretty big!</string>
<string name="trim_logbook_dialog_message_local">Your logbook file is growing a lot. We suggest trimming the oldest events to avoid crashes.</string>
<string name="trim_logbook_dialog_message_dav">Your logbook file is growing a lot. We suggest trimming the oldest events to avoid crashes. If you want to preserve all the events, please backup the "lunatracker_logbook.json" file on the WebDAV server or rename it to start a new logbook keeping the old one.</string>
<string name="trim_logbook_dialog_button_ok">Trim it now</string>
<string name="trim_logbook_dialog_button_cancel">Remind me later</string>
<string name="log_notes_dialog_description">Notes:</string>
<string name="log_medicine_dialog_description">Medicine name, quantity, type, notes…:</string>
<string name="log_notes_dialog_qty_hint">Integer quantity, or leave blank</string>
</resources> </resources>