12 Commits

Author SHA1 Message Date
587fc5d3e3 Add breastfeeding duration tracking and UI improvements
Features:
- Breastfeeding timer: Click to start, stop to save duration
- Manual duration input: Long-press for NumberPicker (1-60 min)
- Edit breastfeeding duration: Click on duration in event details
- Day separators: Visual dividers between days in event list
- German translations: Added missing strings for puke/bath events,
  time units, amount labels, signature settings, event details

The breastfeeding timer state persists across app restarts.
2026-01-08 09:32:30 +01:00
193e21ce25 Merge pull request 'Add puke and bath events, add signature setting' (#16) from mwarning/luna-tracker:master into develop
Reviewed-on: #16
2025-11-05 07:56:01 +01:00
7f67c758c9 activity_setting: fine tune layout style 2025-10-26 20:38:26 +01:00
dfa64d71a8 add signature setting
For multiple users it helps to
keep track about who did what.
2025-10-26 20:38:26 +01:00
b7180068f3 DateUtils: move event details formatting to DateUtils
Also do not display seconds, because it is not
meaningful and is not selected in date picker.
2025-10-26 20:38:22 +01:00
36b848b95e add bath event type 2025-10-26 13:54:16 +01:00
a1bde917f8 add no-breastfeeding help text 2025-10-26 13:54:16 +01:00
4f4ff5ed21 more_events_popup: move enema to bottom and adjust padding
Enemas are usually are rare thing. Let's
move it to the bottom. Also adjust padding
to have more space to display all items.
2025-10-26 13:54:16 +01:00
453d838470 add puke event 2025-10-26 13:54:04 +01:00
34aa092722 NumericUtils: provide fallback for LocaleData.getMeasurementSystem
LocaleData.getMeasurementSystem is available at API level 28
but the app supports API level 21.
2025-09-29 03:34:08 +02:00
961e7b90e7 small code cleanup
No code behavior has been changed.
2025-09-29 03:33:30 +02:00
be77c7fb22 Added "get on F-Droid" button to readme 2025-09-21 09:28:02 +02:00
21 changed files with 710 additions and 108 deletions

View File

@@ -1,5 +1,7 @@
# 🌜 LunaTracker 🌛 # 🌜 LunaTracker 🌛
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/it/packages/it.danieleverducci.lunatracker/)
LunaTracker is a newborn baby tracking app. LunaTracker is a newborn baby tracking app.
Parenting can be tough. You get home from the hospital, exhausted, with this little fragile unknown thingy that has no user manual and a single way to let you know something's not ok: crying. Parenting can be tough. You get home from the hospital, exhausted, with this little fragile unknown thingy that has no user manual and a single way to let you know something's not ok: crying.

View File

@@ -4,6 +4,7 @@ import android.app.DatePickerDialog
import android.app.TimePickerDialog import android.app.TimePickerDialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.util.Log import android.util.Log
@@ -26,6 +27,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import com.thegrizzlylabs.sardineandroid.impl.SardineException import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.adapters.DaySeparatorDecoration
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
@@ -41,15 +43,14 @@ import okio.IOException
import org.json.JSONException import org.json.JSONException
import utils.DateUtils import utils.DateUtils
import utils.NumericUtils import utils.NumericUtils
import java.text.DateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
companion object { companion object {
val TAG = "MainActivity" const val TAG = "MainActivity"
val UPDATE_EVERY_SECS: Long = 30 const val UPDATE_EVERY_SECS: Long = 30
val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false const val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false
} }
var logbook: Logbook? = null var logbook: Logbook? = null
@@ -58,6 +59,7 @@ class MainActivity : AppCompatActivity() {
lateinit var buttonsContainer: ViewGroup lateinit var buttonsContainer: ViewGroup
lateinit var recyclerView: RecyclerView lateinit var recyclerView: RecyclerView
lateinit var handler: Handler lateinit var handler: Handler
var signature = ""
var savingEvent = false var savingEvent = false
val updateListRunnable: Runnable = Runnable { val updateListRunnable: Runnable = Runnable {
if (logbook != null && !pauseLogbookUpdate) if (logbook != null && !pauseLogbookUpdate)
@@ -67,6 +69,13 @@ class MainActivity : AppCompatActivity() {
var logbookRepo: LogbookRepository? = null var logbookRepo: LogbookRepository? = null
var showingOverflowPopupWindow = false var showingOverflowPopupWindow = false
// Breastfeeding timer state
var bfTimerStartTime: Long = 0
var bfTimerType: String? = null
var bfTimerDialog: AlertDialog? = null
var bfTimerHandler: Handler? = null
var bfTimerRunnable: Runnable? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -84,21 +93,27 @@ class MainActivity : AppCompatActivity() {
findViewById<View>(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog(true) } findViewById<View>(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog(true) }
findViewById<View>(R.id.button_bottle).setOnClickListener { askBabyBottleContent() } findViewById<View>(R.id.button_bottle).setOnClickListener { askBabyBottleContent() }
findViewById<View>(R.id.button_food).setOnClickListener { askNotes(LunaEvent(LunaEvent.TYPE_FOOD)) } findViewById<View>(R.id.button_food).setOnClickListener { askNotes(LunaEvent(LunaEvent.TYPE_FOOD)) }
findViewById<View>(R.id.button_nipple_left).setOnClickListener { logEvent( findViewById<View>(R.id.button_nipple_left).setOnClickListener {
LunaEvent( startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE)
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE }
) findViewById<View>(R.id.button_nipple_left).setOnLongClickListener {
) } askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE)
findViewById<View>(R.id.button_nipple_both).setOnClickListener { logEvent( true
LunaEvent( }
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE findViewById<View>(R.id.button_nipple_both).setOnClickListener {
) startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE)
) } }
findViewById<View>(R.id.button_nipple_right).setOnClickListener { logEvent( findViewById<View>(R.id.button_nipple_both).setOnLongClickListener {
LunaEvent( askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE)
LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE true
) }
) } findViewById<View>(R.id.button_nipple_right).setOnClickListener {
startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE)
}
findViewById<View>(R.id.button_nipple_right).setOnLongClickListener {
askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE)
true
}
findViewById<View>(R.id.button_change_poo).setOnClickListener { logEvent( findViewById<View>(R.id.button_change_poo).setOnClickListener { logEvent(
LunaEvent( LunaEvent(
LunaEvent.TYPE_DIAPERCHANGE_POO LunaEvent.TYPE_DIAPERCHANGE_POO
@@ -113,29 +128,35 @@ class MainActivity : AppCompatActivity() {
moreButton.setOnClickListener { moreButton.setOnClickListener {
showOverflowPopupWindow(moreButton) showOverflowPopupWindow(moreButton)
} }
findViewById<View>(R.id.button_no_connection_settings).setOnClickListener({ findViewById<View>(R.id.button_no_connection_settings).setOnClickListener {
showSettings() showSettings()
}) }
findViewById<View>(R.id.button_settings).setOnClickListener({ findViewById<View>(R.id.button_settings).setOnClickListener {
showSettings() showSettings()
}) }
findViewById<View>(R.id.button_no_connection_retry).setOnClickListener({ findViewById<View>(R.id.button_no_connection_retry).setOnClickListener {
// This may happen at start, when logbook is still null: better ask the logbook list // This may happen at start, when logbook is still null: better ask the logbook list
loadLogbookList() loadLogbookList()
}) }
findViewById<View>(R.id.button_sync).setOnClickListener({ findViewById<View>(R.id.button_sync).setOnClickListener {
loadLogbookList() loadLogbookList()
}) }
} }
private fun setListAdapter(items: ArrayList<LunaEvent>) { private fun setListAdapter(items: ArrayList<LunaEvent>) {
val adapter = LunaEventRecyclerAdapter(this, items) val adapter = LunaEventRecyclerAdapter(this, items)
adapter.onItemClickListener = object: LunaEventRecyclerAdapter.OnItemClickListener{ adapter.onItemClickListener = object: LunaEventRecyclerAdapter.OnItemClickListener {
override fun onItemClick(event: LunaEvent) { override fun onItemClick(event: LunaEvent) {
showEventDetailDialog(event, items) showEventDetailDialog(event, items)
} }
} }
recyclerView.adapter = adapter recyclerView.adapter = adapter
// Tages-Trenner hinzufügen
while (recyclerView.itemDecorationCount > 0) {
recyclerView.removeItemDecorationAt(0)
}
recyclerView.addItemDecoration(DaySeparatorDecoration(this, items))
} }
fun showSettings() { fun showSettings() {
@@ -169,6 +190,8 @@ class MainActivity : AppCompatActivity() {
logbookRepo = FileLogbookRepository() logbookRepo = FileLogbookRepository()
} }
signature = settingsRepository.loadSignature()
val noBreastfeeding = settingsRepository.loadNoBreastfeeding() val noBreastfeeding = settingsRepository.loadNoBreastfeeding()
findViewById<View>(R.id.layout_nipples).visibility = when (noBreastfeeding) { findViewById<View>(R.id.layout_nipples).visibility = when (noBreastfeeding) {
true -> View.GONE true -> View.GONE
@@ -178,6 +201,9 @@ class MainActivity : AppCompatActivity() {
// Update list dates // Update list dates
recyclerView.adapter?.notifyDataSetChanged() recyclerView.adapter?.notifyDataSetChanged()
// Check for ongoing breastfeeding timer
restoreBreastfeedingTimerIfNeeded()
if (logbook != null) { if (logbook != null) {
// Already running: reload data for currently selected logbook // Already running: reload data for currently selected logbook
loadLogbook(logbook!!.name) loadLogbook(logbook!!.name)
@@ -190,6 +216,10 @@ class MainActivity : AppCompatActivity() {
override fun onStop() { override fun onStop() {
handler.removeCallbacks(updateListRunnable) handler.removeCallbacks(updateListRunnable)
// Clean up breastfeeding timer UI (state is preserved in SharedPreferences)
bfTimerRunnable?.let { bfTimerHandler?.removeCallbacks(it) }
bfTimerDialog?.dismiss()
super.onStop() super.onStop()
} }
@@ -260,6 +290,26 @@ class MainActivity : AppCompatActivity() {
alertDialog.show() alertDialog.show()
} }
fun askPukeValue() {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.puke_dialog, null)
d.setTitle(R.string.log_puke_dialog_title)
d.setMessage(R.string.log_puke_dialog_description)
d.setView(dialogView)
val spinner = dialogView.findViewById<Spinner>(R.id.dialog_puke_value)
spinner.adapter = ArrayAdapter.createFromResource(this, R.array.AmountLabels, android.R.layout.simple_spinner_dropdown_item)
spinner.setSelection(1)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val pos = spinner.selectedItemPosition
logEvent(LunaEvent(LunaEvent.TYPE_PUKE, pos))
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
val alertDialog = d.create()
alertDialog.show()
}
fun askNotes(lunaEvent: LunaEvent) { fun askNotes(lunaEvent: LunaEvent) {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_notes, null) val dialogView = layoutInflater.inflate(R.layout.dialog_notes, null)
@@ -289,6 +339,119 @@ class MainActivity : AppCompatActivity() {
alertDialog.show() alertDialog.show()
} }
fun startBreastfeedingTimer(eventType: String) {
// Check if timer already running
if (bfTimerType != null) {
Toast.makeText(this, R.string.breastfeeding_timer_already_running, Toast.LENGTH_SHORT).show()
return
}
// Save timer state
bfTimerStartTime = System.currentTimeMillis()
bfTimerType = eventType
saveBreastfeedingTimerState()
// Show timer dialog
showBreastfeedingTimerDialog(eventType)
}
fun showBreastfeedingTimerDialog(eventType: String) {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.breastfeeding_timer_dialog, null)
d.setTitle(R.string.breastfeeding_timer_title)
d.setView(dialogView)
d.setCancelable(false)
val timerDisplay = dialogView.findViewById<TextView>(R.id.breastfeeding_timer_display)
val sideEmoji = dialogView.findViewById<TextView>(R.id.breastfeeding_side_emoji)
sideEmoji.text = LunaEvent(eventType).getTypeEmoji(this)
// Set up timer updates
bfTimerHandler = Handler(mainLooper)
bfTimerRunnable = object : Runnable {
override fun run() {
val elapsed = (System.currentTimeMillis() - bfTimerStartTime) / 1000
val minutes = elapsed / 60
val seconds = elapsed % 60
timerDisplay.text = String.format("%02d:%02d", minutes, seconds)
bfTimerHandler?.postDelayed(this, 1000)
}
}
bfTimerHandler?.post(bfTimerRunnable!!)
d.setPositiveButton(R.string.breastfeeding_timer_stop) { _, _ ->
stopBreastfeedingTimer()
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
cancelBreastfeedingTimer()
dialogInterface.dismiss()
}
bfTimerDialog = d.create()
bfTimerDialog?.show()
}
fun stopBreastfeedingTimer() {
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
val durationMillis = System.currentTimeMillis() - bfTimerStartTime
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
val eventType = bfTimerType
clearBreastfeedingTimerState()
if (eventType != null) {
logEvent(LunaEvent(eventType, durationMinutes))
}
}
fun cancelBreastfeedingTimer() {
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
clearBreastfeedingTimerState()
}
fun askBreastfeedingDuration(eventType: String) {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
d.setTitle(R.string.breastfeeding_duration_title)
d.setMessage(R.string.breastfeeding_duration_description)
d.setView(dialogView)
val numberPicker = dialogView.findViewById<NumberPicker>(R.id.breastfeeding_duration_picker)
numberPicker.minValue = 1
numberPicker.maxValue = 60
numberPicker.value = 15 // Default 15 minutes
numberPicker.wrapSelectorWheel = false
d.setPositiveButton(android.R.string.ok) { _, _ ->
logEvent(LunaEvent(eventType, numberPicker.value))
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
dialogInterface.dismiss()
}
d.create().show()
}
fun saveBreastfeedingTimerState() {
LocalSettingsRepository(this).saveBreastfeedingTimer(bfTimerStartTime, bfTimerType ?: "")
}
fun clearBreastfeedingTimerState() {
bfTimerStartTime = 0
bfTimerType = null
bfTimerDialog = null
LocalSettingsRepository(this).clearBreastfeedingTimer()
}
fun restoreBreastfeedingTimerIfNeeded() {
val timerState = LocalSettingsRepository(this).loadBreastfeedingTimer()
if (timerState != null && timerState.first > 0 && timerState.second.isNotEmpty()) {
bfTimerStartTime = timerState.first
bfTimerType = timerState.second
showBreastfeedingTimerDialog(timerState.second)
}
}
fun askToTrimLogbook() { fun askToTrimLogbook() {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
d.setTitle(R.string.trim_logbook_dialog_title) d.setTitle(R.string.trim_logbook_dialog_title)
@@ -340,7 +503,6 @@ class MainActivity : AppCompatActivity() {
fun showEventDetailDialog(event: LunaEvent, items: ArrayList<LunaEvent>) { fun showEventDetailDialog(event: LunaEvent, items: ArrayList<LunaEvent>) {
// Do not update list while the detail is shown, to avoid changing the object below while it is changed by the user // Do not update list while the detail is shown, to avoid changing the object below while it is changed by the user
pauseLogbookUpdate = true pauseLogbookUpdate = true
val dateFormat = DateFormat.getDateTimeInstance()
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
d.setTitle(R.string.dialog_event_detail_title) d.setTitle(R.string.dialog_event_detail_title)
val dialogView = layoutInflater.inflate(R.layout.dialog_event_detail, null) val dialogView = layoutInflater.inflate(R.layout.dialog_event_detail, null)
@@ -352,8 +514,9 @@ class MainActivity : AppCompatActivity() {
val currentDateTime = Calendar.getInstance() val currentDateTime = Calendar.getInstance()
currentDateTime.time = Date(event.time * 1000) currentDateTime.time = Date(event.time * 1000)
val dateTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_date) val dateTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_date)
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), dateFormat.format(currentDateTime.time)) dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), DateUtils.formatDateTime(event.time))
dateTextView.setOnClickListener { dateTextView.setOnClickListener {
// Show datetime picker // Show datetime picker
val startYear = currentDateTime.get(Calendar.YEAR) val startYear = currentDateTime.get(Calendar.YEAR)
@@ -366,11 +529,9 @@ class MainActivity : AppCompatActivity() {
TimePickerDialog(this, { _, hour, minute -> TimePickerDialog(this, { _, hour, minute ->
val pickedDateTime = Calendar.getInstance() val pickedDateTime = Calendar.getInstance()
pickedDateTime.set(year, month, day, hour, minute) pickedDateTime.set(year, month, day, hour, minute)
currentDateTime.time = pickedDateTime.time
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), dateFormat.format(currentDateTime.time))
// Save event and move it to the right position in the logbook // Save event and move it to the right position in the logbook
event.time = currentDateTime.time.time / 1000 // Seconds since epoch event.time = pickedDateTime.time.time / 1000 // Seconds since epoch
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), DateUtils.formatDateTime(event.time))
logbook?.sort() logbook?.sort()
recyclerView.adapter?.notifyDataSetChanged() recyclerView.adapter?.notifyDataSetChanged()
saveLogbook() saveLogbook()
@@ -378,6 +539,36 @@ class MainActivity : AppCompatActivity() {
}, startYear, startMonth, startDay).show() }, startYear, startMonth, startDay).show()
} }
// Make quantity editable for breastfeeding events
val quantityTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity)
if (event.type in listOf(
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
) && event.quantity > 0) {
quantityTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_edit, 0)
quantityTextView.compoundDrawableTintList = ColorStateList.valueOf(getColor(R.color.accent))
quantityTextView.setOnClickListener {
val pickerDialog = AlertDialog.Builder(this@MainActivity)
val pickerView = layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
val picker = pickerView.findViewById<NumberPicker>(R.id.breastfeeding_duration_picker)
picker.minValue = 1
picker.maxValue = 60
picker.value = if (event.quantity > 0) event.quantity else 15
pickerDialog.setTitle(R.string.breastfeeding_duration_title)
pickerDialog.setView(pickerView)
pickerDialog.setPositiveButton(android.R.string.ok) { _, _ ->
event.quantity = picker.value
quantityTextView.text = NumericUtils(this@MainActivity).formatEventQuantity(event)
recyclerView.adapter?.notifyDataSetChanged()
saveLogbook()
}
pickerDialog.setNegativeButton(android.R.string.cancel, null)
pickerDialog.show()
}
}
d.setView(dialogView) d.setView(dialogView)
d.setPositiveButton(R.string.dialog_event_detail_close_button) { dialogInterface, i -> dialogInterface.dismiss() } d.setPositiveButton(R.string.dialog_event_detail_close_button) { dialogInterface, i -> dialogInterface.dismiss() }
d.setNeutralButton(R.string.dialog_event_detail_delete_button) { dialogInterface, i -> deleteEvent(event) } d.setNeutralButton(R.string.dialog_event_detail_delete_button) { dialogInterface, i -> deleteEvent(event) }
@@ -389,6 +580,13 @@ class MainActivity : AppCompatActivity() {
pauseLogbookUpdate = false pauseLogbookUpdate = false
}) })
// show optional signature
if (event.signature.isNotEmpty()) {
val signatureTextEdit = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_signature)
signatureTextEdit.text = String.format(getString(R.string.dialog_event_detail_signature), event.signature)
signatureTextEdit.visibility = View.VISIBLE
}
// create next/previous links to events of the same type // create next/previous links to events of the same type
val previousTextView = dialogView.findViewById<TextView>(R.id.dialog_event_previous) val previousTextView = dialogView.findViewById<TextView>(R.id.dialog_event_previous)
@@ -638,6 +836,8 @@ class MainActivity : AppCompatActivity() {
fun logEvent(event: LunaEvent) { fun logEvent(event: LunaEvent) {
savingEvent(true) savingEvent(true)
event.signature = signature
setLoading(true) setLoading(true)
logbook?.logs?.add(0, event) logbook?.logs?.add(0, event)
recyclerView.adapter?.notifyItemInserted(0) recyclerView.adapter?.notifyItemInserted(0)
@@ -787,6 +987,10 @@ class MainActivity : AppCompatActivity() {
askTemperatureValue() askTemperatureValue()
dismiss() dismiss()
}) })
contentView.findViewById<View>(R.id.button_puke).setOnClickListener({
askPukeValue()
dismiss()
})
contentView.findViewById<View>(R.id.button_colic).setOnClickListener({ contentView.findViewById<View>(R.id.button_colic).setOnClickListener({
logEvent( logEvent(
LunaEvent(LunaEvent.TYPE_COLIC) LunaEvent(LunaEvent.TYPE_COLIC)
@@ -797,6 +1001,12 @@ class MainActivity : AppCompatActivity() {
askWeightValue() askWeightValue()
dismiss() dismiss()
}) })
contentView.findViewById<View>(R.id.button_bath).setOnClickListener({
logEvent(
LunaEvent(LunaEvent.TYPE_BATH)
)
dismiss()
})
}.also { popupWindow -> }.also { popupWindow ->
popupWindow.setOnDismissListener({ popupWindow.setOnDismissListener({
Handler(mainLooper).postDelayed({ Handler(mainLooper).postDelayed({

View File

@@ -2,6 +2,7 @@ package it.danieleverducci.lunatracker
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.EditText
import android.widget.RadioButton import android.widget.RadioButton
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
@@ -24,6 +25,7 @@ open class SettingsActivity : AppCompatActivity() {
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 switchNoBreastfeeding: SwitchMaterial
protected lateinit var textViewSignature: EditText
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -36,6 +38,7 @@ open class SettingsActivity : AppCompatActivity() {
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) switchNoBreastfeeding = findViewById(R.id.switch_no_breastfeeding)
textViewSignature = findViewById(R.id.settings_signature)
findViewById<View>(R.id.settings_save).setOnClickListener({ findViewById<View>(R.id.settings_save).setOnClickListener({
validateAndSave() validateAndSave()
@@ -52,18 +55,20 @@ open class SettingsActivity : AppCompatActivity() {
val dataRepo = settingsRepository.loadDataRepository() val dataRepo = settingsRepository.loadDataRepository()
val webDavCredentials = settingsRepository.loadWebdavCredentials() val webDavCredentials = settingsRepository.loadWebdavCredentials()
val noBreastfeeding = settingsRepository.loadNoBreastfeeding() val noBreastfeeding = settingsRepository.loadNoBreastfeeding()
val signature = settingsRepository.loadSignature()
when (dataRepo) { when (dataRepo) {
LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> radioDataLocal.isChecked = true LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> radioDataLocal.isChecked = true
LocalSettingsRepository.DATA_REPO.WEBDAV -> radioDataWebDAV.isChecked = true LocalSettingsRepository.DATA_REPO.WEBDAV -> radioDataWebDAV.isChecked = true
} }
textViewSignature.setText(signature)
switchNoBreastfeeding.isChecked = noBreastfeeding switchNoBreastfeeding.isChecked = noBreastfeeding
if (webDavCredentials != null) { if (webDavCredentials != null) {
textViewWebDAVUrl.setText(webDavCredentials[0]) textViewWebDAVUrl.text = webDavCredentials[0]
textViewWebDAVUser.setText(webDavCredentials[1]) textViewWebDAVUser.text = webDavCredentials[1]
textViewWebDAVPass.setText(webDavCredentials[2]) textViewWebDAVPass.text = webDavCredentials[2]
} }
} }
@@ -156,6 +161,7 @@ open class SettingsActivity : AppCompatActivity() {
else LocalSettingsRepository.DATA_REPO.LOCAL_FILE else LocalSettingsRepository.DATA_REPO.LOCAL_FILE
) )
settingsRepository.saveNoBreastfeeding(switchNoBreastfeeding.isChecked) settingsRepository.saveNoBreastfeeding(switchNoBreastfeeding.isChecked)
settingsRepository.saveSignature(textViewSignature.text.toString())
settingsRepository.saveWebdavCredentials( settingsRepository.saveWebdavCredentials(
textViewWebDAVUrl.text.toString(), textViewWebDAVUrl.text.toString(),
textViewWebDAVUser.text.toString(), textViewWebDAVUser.text.toString(),
@@ -170,7 +176,7 @@ open class SettingsActivity : AppCompatActivity() {
*/ */
private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) { private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) {
Thread(Runnable { Thread(Runnable {
var errors = StringBuilder() val errors = StringBuilder()
val fileLogbookRepo = FileLogbookRepository() val fileLogbookRepo = FileLogbookRepository()
val logbooks = fileLogbookRepo.getAllLogbooks(this) val logbooks = fileLogbookRepo.getAllLogbooks(this)
for (logbook in logbooks) { for (logbook in logbooks) {

View File

@@ -0,0 +1,80 @@
package it.danieleverducci.lunatracker.adapters
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.text.format.DateFormat
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import it.danieleverducci.lunatracker.R
import it.danieleverducci.lunatracker.entities.LunaEvent
import java.util.Calendar
import java.util.Date
class DaySeparatorDecoration(
private val context: Context,
private val items: List<LunaEvent>
) : RecyclerView.ItemDecoration() {
private val textPaint = Paint().apply {
color = context.getColor(R.color.grey)
textSize = 32f
textAlign = Paint.Align.CENTER
isAntiAlias = true
}
private val linePaint = Paint().apply {
color = context.getColor(R.color.grey)
strokeWidth = 1f
isAntiAlias = true
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val position = parent.getChildAdapterPosition(view)
if (shouldShowHeader(position)) {
outRect.top = 48
}
}
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val position = parent.getChildAdapterPosition(child)
if (shouldShowHeader(position)) {
val dateText = formatDate(items[position].time)
val y = child.top - 16f
// Linie links
canvas.drawLine(20f, y, parent.width / 2f - 80f, y, linePaint)
// Datum in der Mitte
canvas.drawText(dateText, parent.width / 2f, y + 10f, textPaint)
// Linie rechts
canvas.drawLine(parent.width / 2f + 80f, y, parent.width - 20f, y, linePaint)
}
}
}
private fun shouldShowHeader(position: Int): Boolean {
if (position <= 0 || position >= items.size) return false
val currentDay = getDay(items[position].time)
val previousDay = getDay(items[position - 1].time)
return currentDay != previousDay
}
private fun getDay(timestamp: Long): Long {
val cal = Calendar.getInstance()
cal.timeInMillis = timestamp * 1000
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
cal.set(Calendar.MILLISECOND, 0)
return cal.timeInMillis
}
private fun formatDate(timestamp: Long): String {
return DateFormat.getDateFormat(context).format(Date(timestamp * 1000))
}
}

View File

@@ -53,7 +53,7 @@ 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.getTypeEmoji(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 LunaEvent.TYPE_CUSTOM -> item.notes

View File

@@ -28,6 +28,8 @@ class LunaEvent: Comparable<LunaEvent> {
const val TYPE_COLIC = "COLIC" const val TYPE_COLIC = "COLIC"
const val TYPE_TEMPERATURE = "TEMPERATURE" const val TYPE_TEMPERATURE = "TEMPERATURE"
const val TYPE_FOOD = "FOOD" const val TYPE_FOOD = "FOOD"
const val TYPE_PUKE = "PUKE"
const val TYPE_BATH = "BATH"
} }
private val jo: JSONObject private val jo: JSONObject
@@ -53,6 +55,12 @@ class LunaEvent: Comparable<LunaEvent> {
set(value) { set(value) {
jo.put("notes", value) jo.put("notes", value)
} }
var signature: String
get(): String = jo.optString("signature")
set(value) {
if (value.isNotEmpty())
jo.put("signature", value)
}
constructor(jo: JSONObject) { constructor(jo: JSONObject) {
this.jo = jo this.jo = jo
@@ -90,6 +98,8 @@ class LunaEvent: Comparable<LunaEvent> {
TYPE_TEMPERATURE -> R.string.event_temperature_type TYPE_TEMPERATURE -> R.string.event_temperature_type
TYPE_COLIC -> R.string.event_colic_type TYPE_COLIC -> R.string.event_colic_type
TYPE_FOOD -> R.string.event_food_type TYPE_FOOD -> R.string.event_food_type
TYPE_PUKE -> R.string.event_puke_type
TYPE_BATH -> R.string.event_bath_type
else -> R.string.event_unknown_type else -> R.string.event_unknown_type
} }
) )
@@ -111,6 +121,8 @@ class LunaEvent: Comparable<LunaEvent> {
TYPE_TEMPERATURE -> R.string.event_temperature_desc TYPE_TEMPERATURE -> R.string.event_temperature_desc
TYPE_COLIC -> R.string.event_colic_desc TYPE_COLIC -> R.string.event_colic_desc
TYPE_FOOD -> R.string.event_food_desc TYPE_FOOD -> R.string.event_food_desc
TYPE_PUKE -> R.string.event_puke_desc
TYPE_BATH -> R.string.event_bath_desc
else -> R.string.event_unknown_desc else -> R.string.event_unknown_desc
} }
) )
@@ -128,7 +140,7 @@ class LunaEvent: Comparable<LunaEvent> {
} }
override fun toString(): String { override fun toString(): String {
return "${type} qty: $quantity time: ${Date(time * 1000)}" return "$type qty: $quantity time: ${Date(time * 1000)}"
} }
override fun compareTo(other: LunaEvent): Int { override fun compareTo(other: LunaEvent): Int {

View File

@@ -13,9 +13,9 @@ import java.io.FilenameFilter
class FileLogbookRepository: LogbookRepository { class FileLogbookRepository: LogbookRepository {
companion object { companion object {
val TAG = "FileLogbookRepository" const val TAG = "FileLogbookRepository"
val FILE_NAME_START = "data" const val FILE_NAME_START = "data"
val FILE_NAME_END = ".json" const val FILE_NAME_END = ".json"
} }
override fun loadLogbook(context: Context, name: String, listener: LogbookLoadedListener) { override fun loadLogbook(context: Context, name: String, listener: LogbookLoadedListener) {
@@ -32,7 +32,7 @@ class FileLogbookRepository: LogbookRepository {
fun loadLogbook(context: Context, name: String): Logbook { fun loadLogbook(context: Context, name: String): Logbook {
val logbook = Logbook(name) val logbook = Logbook(name)
val fileName = getFileName(name) val fileName = getFileName(name)
val file = File(context.getFilesDir(), fileName) val file = File(context.filesDir, fileName)
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()) {
@@ -58,7 +58,7 @@ class FileLogbookRepository: LogbookRepository {
fun saveLogbook(context: Context, logbook: Logbook) { fun saveLogbook(context: Context, logbook: Logbook) {
val fileName = getFileName(logbook.name) val fileName = getFileName(logbook.name)
val file = File(context.getFilesDir(), fileName) val file = File(context.filesDir, fileName)
val ja = JSONArray() val ja = JSONArray()
for (l in logbook.logs) { for (l in logbook.logs) {
ja.put(l.toJson()) ja.put(l.toJson())
@@ -82,7 +82,7 @@ class FileLogbookRepository: LogbookRepository {
} }
private fun listLogbooks(context: Context): ArrayList<String> { private fun listLogbooks(context: Context): ArrayList<String> {
val logbooksFileNames = context.getFilesDir().list(object: FilenameFilter { val logbooksFileNames = context.filesDir.list(object: FilenameFilter {
override fun accept(dir: File?, name: String?): Boolean { override fun accept(dir: File?, name: String?): Boolean {
if (name == null) if (name == null)
return false return false

View File

@@ -7,13 +7,16 @@ import androidx.core.content.edit
class LocalSettingsRepository(val context: Context) { class LocalSettingsRepository(val context: Context) {
companion object { companion object {
val SHARED_PREFS_FILE_NAME = "lunasettings" const val SHARED_PREFS_FILE_NAME = "lunasettings"
val SHARED_PREFS_BB_CONTENT = "bbcontent" const val SHARED_PREFS_BB_CONTENT = "bbcontent"
val SHARED_PREFS_DATA_REPO = "data_repo" const val SHARED_PREFS_DATA_REPO = "data_repo"
val SHARED_PREFS_DAV_URL = "webdav_url" const val SHARED_PREFS_DAV_URL = "webdav_url"
val SHARED_PREFS_DAV_USER = "webdav_user" const val SHARED_PREFS_DAV_USER = "webdav_user"
val SHARED_PREFS_DAV_PASS = "webdav_password" const val SHARED_PREFS_DAV_PASS = "webdav_password"
val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding" const val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding"
const val SHARED_PREFS_SIGNATURE = "signature"
const val SHARED_PREFS_BF_TIMER_START = "bf_timer_start"
const val SHARED_PREFS_BF_TIMER_TYPE = "bf_timer_type"
} }
enum class DATA_REPO {LOCAL_FILE, WEBDAV} enum class DATA_REPO {LOCAL_FILE, WEBDAV}
val sharedPreferences: SharedPreferences val sharedPreferences: SharedPreferences
@@ -23,15 +26,23 @@ class LocalSettingsRepository(val context: Context) {
} }
fun saveBabyBottleContent(content: Int) { fun saveBabyBottleContent(content: Int) {
sharedPreferences.edit().putInt(SHARED_PREFS_BB_CONTENT, content).apply() sharedPreferences.edit { putInt(SHARED_PREFS_BB_CONTENT, content) }
} }
fun loadBabyBottleContent(): Int { fun loadBabyBottleContent(): Int {
return sharedPreferences.getInt(SHARED_PREFS_BB_CONTENT, 1) return sharedPreferences.getInt(SHARED_PREFS_BB_CONTENT, 1)
} }
fun saveSignature(content: String) {
sharedPreferences.edit { putString(SHARED_PREFS_SIGNATURE, content) }
}
fun loadSignature(): String {
return sharedPreferences.getString(SHARED_PREFS_SIGNATURE, "") ?: ""
}
fun saveNoBreastfeeding(content: Boolean) { fun saveNoBreastfeeding(content: Boolean) {
sharedPreferences.edit().putBoolean(SHARED_PREFS_NO_BREASTFEEDING, content).apply() sharedPreferences.edit { putBoolean(SHARED_PREFS_NO_BREASTFEEDING, content) }
} }
fun loadNoBreastfeeding(): Boolean { fun loadNoBreastfeeding(): Boolean {
@@ -39,15 +50,15 @@ class LocalSettingsRepository(val context: Context) {
} }
fun saveDataRepository(repo: DATA_REPO) { fun saveDataRepository(repo: DATA_REPO) {
val spe = sharedPreferences.edit() sharedPreferences.edit(commit = true) {
spe.putString( putString(
SHARED_PREFS_DATA_REPO, SHARED_PREFS_DATA_REPO,
when (repo) { when (repo) {
DATA_REPO.WEBDAV -> "webdav" DATA_REPO.WEBDAV -> "webdav"
DATA_REPO.LOCAL_FILE -> "localfile" DATA_REPO.LOCAL_FILE -> "localfile"
} }
) )
spe.commit() }
} }
fun loadDataRepository(): DATA_REPO { fun loadDataRepository(): DATA_REPO {
@@ -60,11 +71,11 @@ class LocalSettingsRepository(val context: Context) {
} }
fun saveWebdavCredentials(url: String, username: String, password: String) { fun saveWebdavCredentials(url: String, username: String, password: String) {
val spe = sharedPreferences.edit() sharedPreferences.edit(commit = true) {
spe.putString(SHARED_PREFS_DAV_URL, url) putString(SHARED_PREFS_DAV_URL, url)
spe.putString(SHARED_PREFS_DAV_USER, username) putString(SHARED_PREFS_DAV_USER, username)
spe.putString(SHARED_PREFS_DAV_PASS, password) putString(SHARED_PREFS_DAV_PASS, password)
spe.commit() }
} }
fun loadWebdavCredentials(): Array<String>? { fun loadWebdavCredentials(): Array<String>? {
@@ -75,4 +86,25 @@ class LocalSettingsRepository(val context: Context) {
return null return null
return arrayOf(url, user, pass) return arrayOf(url, user, pass)
} }
fun saveBreastfeedingTimer(startTime: Long, eventType: String) {
sharedPreferences.edit {
putLong(SHARED_PREFS_BF_TIMER_START, startTime)
putString(SHARED_PREFS_BF_TIMER_TYPE, eventType)
}
}
fun loadBreastfeedingTimer(): Pair<Long, String>? {
val startTime = sharedPreferences.getLong(SHARED_PREFS_BF_TIMER_START, 0)
val eventType = sharedPreferences.getString(SHARED_PREFS_BF_TIMER_TYPE, null)
if (startTime == 0L || eventType == null) return null
return Pair(startTime, eventType)
}
fun clearBreastfeedingTimer() {
sharedPreferences.edit {
remove(SHARED_PREFS_BF_TIMER_START)
remove(SHARED_PREFS_BF_TIMER_TYPE)
}
}
} }

View File

@@ -1,12 +1,17 @@
package utils package utils
import android.content.Context import android.content.Context
import android.os.Build
import android.text.format.DateFormat import android.text.format.DateFormat
import it.danieleverducci.lunatracker.R import it.danieleverducci.lunatracker.R
import java.util.Date import java.util.Date
class DateUtils { class DateUtils {
companion object { companion object {
/**
* Format time duration in seconds as e.g. "2 hours, 1 min".
* Used for the duration to the next/previous event in the event details dialog.
*/
fun formatTimeDuration(context: Context, secondsDiff: Long): String { fun formatTimeDuration(context: Context, secondsDiff: Long): String {
var seconds = secondsDiff var seconds = secondsDiff
@@ -65,7 +70,8 @@ class DateUtils {
} }
/** /**
* Formats the provided unix timestamp in a string like "3 hours, 26 minutes ago) * Formats the provided unix timestamp in a string like "3 hours, 26 minutes ago".
* Used for the event list.
*/ */
fun formatTimeAgo(context: Context, unixTime: Long): String { fun formatTimeAgo(context: Context, unixTime: Long): String {
val secondsDiff = (System.currentTimeMillis() / 1000) - unixTime val secondsDiff = (System.currentTimeMillis() / 1000) - unixTime
@@ -100,5 +106,21 @@ class DateUtils {
} }
return formattedTime.toString() return formattedTime.toString()
} }
/**
* Format time as localized string without seconds. E.g. "Sept 18, 2025, 03:36 PM".
* Used in the event detail dialog.
*/
fun formatDateTime(unixTime: Long): String {
val date = Date(unixTime * 1000)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val dateFormat = android.icu.text.DateFormat.getDateTimeInstance(android.icu.text.DateFormat.DEFAULT, android.icu.text.DateFormat.SHORT)
return dateFormat.format(date)
} else {
// fallback
val dateFormat = java.text.DateFormat.getDateTimeInstance()
return dateFormat.format(date)
}
}
} }
} }

View File

@@ -3,6 +3,7 @@ package utils
import android.content.Context import android.content.Context
import android.icu.util.LocaleData import android.icu.util.LocaleData
import android.icu.util.ULocale import android.icu.util.ULocale
import android.os.Build
import it.danieleverducci.lunatracker.R import it.danieleverducci.lunatracker.R
import it.danieleverducci.lunatracker.entities.LunaEvent import it.danieleverducci.lunatracker.entities.LunaEvent
import java.text.NumberFormat import java.text.NumberFormat
@@ -14,29 +15,45 @@ class NumericUtils (val context: Context) {
val measurement_unit_weight_tiny: String val measurement_unit_weight_tiny: String
val measurement_unit_temperature_base: String val measurement_unit_temperature_base: String
private fun isMetricSystem(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val measurementSystem = LocaleData.getMeasurementSystem(ULocale.getDefault())
return (measurementSystem == LocaleData.MeasurementSystem.SI)
} else {
val locale = context.resources.configuration.locale
return when (locale.country) {
// https://en.wikipedia.org/wiki/United_States_customary_units
// https://en.wikipedia.org/wiki/Imperial_units
"US" -> false // US IMPERIAL
// UK, Myanmar, Liberia,
"GB", "MM", "LR" -> false // IMPERIAL
else -> true // METRIC
}
}
}
init { init {
this.numberFormat = NumberFormat.getInstance() this.numberFormat = NumberFormat.getInstance()
val measurementSystem = LocaleData.getMeasurementSystem(ULocale.getDefault())
this.measurement_unit_liquid_base = context.getString( this.measurement_unit_liquid_base = context.getString(
if (measurementSystem == LocaleData. MeasurementSystem.SI) if (isMetricSystem())
R.string.measurement_unit_liquid_base_metric R.string.measurement_unit_liquid_base_metric
else else
R.string.measurement_unit_liquid_base_imperial R.string.measurement_unit_liquid_base_imperial
) )
this.measurement_unit_weight_base = context.getString( this.measurement_unit_weight_base = context.getString(
if (measurementSystem == LocaleData. MeasurementSystem.SI) if (isMetricSystem())
R.string.measurement_unit_weight_base_metric R.string.measurement_unit_weight_base_metric
else else
R.string.measurement_unit_weight_base_imperial R.string.measurement_unit_weight_base_imperial
) )
this.measurement_unit_weight_tiny = context.getString( this.measurement_unit_weight_tiny = context.getString(
if (measurementSystem == LocaleData. MeasurementSystem.SI) if (isMetricSystem())
R.string.measurement_unit_weight_tiny_metric R.string.measurement_unit_weight_tiny_metric
else else
R.string.measurement_unit_weight_tiny_imperial R.string.measurement_unit_weight_tiny_imperial
) )
this.measurement_unit_temperature_base = context.getString( this.measurement_unit_temperature_base = context.getString(
if (measurementSystem == LocaleData. MeasurementSystem.SI) if (isMetricSystem())
R.string.measurement_unit_temperature_base_metric R.string.measurement_unit_temperature_base_metric
else else
R.string.measurement_unit_temperature_base_imperial R.string.measurement_unit_temperature_base_imperial
@@ -45,11 +62,15 @@ class NumericUtils (val context: Context) {
fun formatEventQuantity(item: LunaEvent): String { fun formatEventQuantity(item: LunaEvent): String {
val formatted = StringBuilder() val formatted = StringBuilder()
if ((item.quantity ?: 0) > 0) { if (item.quantity > 0) {
if (item.type == LunaEvent.TYPE_TEMPERATURE) formatted.append(when (item.type) {
formatted.append((item.quantity / 10.0f).toString()) LunaEvent.TYPE_TEMPERATURE ->
else (item.quantity / 10.0f).toString()
formatted.append(item.quantity) LunaEvent.TYPE_PUKE ->
context.resources.getStringArray(R.array.AmountLabels)[item.quantity]
else ->
item.quantity
})
formatted.append(" ") formatted.append(" ")
formatted.append( formatted.append(
@@ -58,6 +79,10 @@ class NumericUtils (val context: Context) {
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
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE ->
context.getString(R.string.measurement_unit_time_minutes)
else -> "" else -> ""
} }
) )
@@ -70,10 +95,9 @@ class NumericUtils (val context: Context) {
* @return min, max, normal * @return min, max, normal
*/ */
fun getValidEventQuantityRange(lunaEventType: String): Triple<Int, Int, Int>? { fun getValidEventQuantityRange(lunaEventType: String): Triple<Int, Int, Int>? {
val measurementSystem = LocaleData.getMeasurementSystem(ULocale.getDefault())
return when (lunaEventType) { return when (lunaEventType) {
LunaEvent.TYPE_TEMPERATURE -> { LunaEvent.TYPE_TEMPERATURE -> {
if (measurementSystem == LocaleData. MeasurementSystem.SI) if (isMetricSystem())
Triple( Triple(
context.resources.getInteger(R.integer.human_body_temp_min_metric), context.resources.getInteger(R.integer.human_body_temp_min_metric),
context.resources.getInteger(R.integer.human_body_temp_max_metric), context.resources.getInteger(R.integer.human_body_temp_max_metric),

View File

@@ -119,28 +119,57 @@
android:visibility="invisible"/> android:visibility="invisible"/>
</RadioGroup> </RadioGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/settings_signature" />
<EditText
android:id="@+id/settings_signature"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:inputType="textEmailAddress"
android:background="@drawable/textview_background"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="5dp"
android:text="@string/settings_signature_desc"/>
<LinearLayout <LinearLayout
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"
android:layout_marginTop="5dp" android:layout_marginTop="20dp">
android:layout_marginEnd="30dp">
<TextView <TextView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textStyle="bold" android:textStyle="bold"
android:text="@string/no_breastfeeding" /> android:text="@string/settings_no_breastfeeding" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_no_breastfeeding" android:id="@+id/switch_no_breastfeeding"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_weight="1" /> android:layout_weight="1" />
</LinearLayout> </LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="5dp"
android:text="@string/settings_no_breastfeeding_desc"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -0,0 +1,20 @@
<?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:layout_gravity="center"
android:gravity="center">
<NumberPicker
android:id="@+id/breastfeeding_duration_picker"
android:layout_width="100dp"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="@string/measurement_unit_time_minutes"/>
</LinearLayout>

View File

@@ -0,0 +1,35 @@
<?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:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/breastfeeding_side_emoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="60sp"/>
<TextView
android:id="@+id/breastfeeding_timer_display"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="48sp"
android:textColor="@color/accent"
android:fontFamily="monospace"
android:text="00:00"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="14sp"
android:textColor="@color/grey"
android:text="@string/breastfeeding_timer_hint"/>
</LinearLayout>

View File

@@ -4,7 +4,9 @@
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"
android:padding="20dp"> android:paddingTop="20dp"
android:paddingBottom="10dp"
android:paddingHorizontal="20dp">
<TextView <TextView
android:id="@+id/dialog_event_detail_type_emoji" android:id="@+id/dialog_event_detail_type_emoji"
@@ -61,6 +63,14 @@
</ScrollView> </ScrollView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:id="@+id/dialog_event_detail_type_signature"
android:layout_marginBottom="5dp"
android:visibility="gone"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -2,7 +2,7 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="20dp" android:padding="10dp"
android:background="@color/transparent"> android:background="@color/transparent">
<LinearLayout <LinearLayout
@@ -14,27 +14,17 @@
android:id="@+id/button_medicine" android:id="@+id/button_medicine"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="20dp" 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="@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/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_enema"/>
<TextView <TextView
android:id="@+id/button_note" android:id="@+id/button_note"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:padding="20dp" 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_note"/> android:text="@string/overflow_event_note"/>
@@ -44,17 +34,27 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:padding="20dp" 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_temperature"/> 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 <TextView
android:id="@+id/button_colic" android:id="@+id/button_colic"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:padding="20dp" 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_colic"/> android:text="@string/overflow_event_colic"/>
@@ -64,11 +64,31 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:padding="20dp" 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_scale"/> android:text="@string/overflow_event_scale"/>
<TextView
android:id="@+id/button_bath"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_bath"/>
<TextView
android:id="@+id/button_enema"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_enema"/>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -0,0 +1,17 @@
<?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:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<Spinner
android:id="@+id/dialog_puke_value"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"/>
</LinearLayout>

View File

@@ -50,9 +50,8 @@
<string name="no_connection_go_to_settings">Einstellungen</string> <string name="no_connection_go_to_settings">Einstellungen</string>
<string name="no_connection_retry">Erneut versuchen</string> <string name="no_connection_retry">Erneut versuchen</string>
<string name="no_breastfeeding">Kein Stillen</string>
<string name="settings_title">Einstellungen</string> <string name="settings_title">Einstellungen</string>
<string name="settings_no_breastfeeding">Kein Stillen</string>
<string name="settings_storage">Speicherort für Daten auswählen</string> <string name="settings_storage">Speicherort für Daten auswählen</string>
<string name="settings_storage_local">Auf dem Gerät</string> <string name="settings_storage_local">Auf dem Gerät</string>
<string name="settings_storage_local_desc">Datenschutzfreundlichste Lösung: Deine Daten verlassen dein Gerät nicht</string> <string name="settings_storage_local_desc">Datenschutzfreundlichste Lösung: Deine Daten verlassen dein Gerät nicht</string>
@@ -95,4 +94,42 @@
<string name="default_logbook_name">👶 Mein erstes Logbuch</string> <string name="default_logbook_name">👶 Mein erstes Logbuch</string>
<string name="logbook_created">Neues Logbuch erstellt: </string> <string name="logbook_created">Neues Logbuch erstellt: </string>
<string name="breastfeeding_timer_title">Stillen läuft</string>
<string name="breastfeeding_timer_stop">Stopp</string>
<string name="breastfeeding_timer_hint">Tippe Stopp wenn fertig</string>
<string name="breastfeeding_timer_already_running">Es läuft bereits eine Stillsitzung</string>
<string name="breastfeeding_duration_title">Stilldauer</string>
<string name="breastfeeding_duration_description">Dauer in Minuten eingeben</string>
<!-- Puke/Bath Events -->
<string name="log_puke_dialog_title">Spucken</string>
<string name="log_puke_dialog_description">Menge auswählen</string>
<string name="event_puke_desc">Spucken</string>
<string name="event_bath_desc">Baden</string>
<string name="overflow_event_puke">🤮 Spucken</string>
<string name="overflow_event_bath">🛁 Baden</string>
<!-- Zeitangaben -->
<string name="second_ago">Sek.</string>
<string name="seconds_ago">Sek.</string>
<string name="day_ago">Tag</string>
<string name="days_ago">Tage</string>
<string name="year_ago">Jahr</string>
<string name="years_ago">Jahre</string>
<!-- Mengenangaben -->
<string name="amount_little">Wenig</string>
<string name="amount_normal">Normal</string>
<string name="amount_plenty">Viel</string>
<!-- Signatur-Einstellungen -->
<string name="settings_signature">Signatur</string>
<string name="settings_signature_desc">Füge jedem Event eine Signatur hinzu, die andere sehen können. Nützlich wenn mehrere Personen Events hinzufügen.</string>
<string name="settings_no_breastfeeding_desc">Verstecke die Stillbuttons wenn sie nicht benötigt werden.</string>
<!-- Event-Detail-Dialog -->
<string name="dialog_event_detail_quantity">Menge</string>
<string name="dialog_event_detail_notes">Notizen</string>
<string name="dialog_event_detail_signature">von %s</string>
</resources> </resources>

View File

@@ -93,4 +93,11 @@
<string name="default_logbook_name">👶 Mon premier carnet de bord</string> <string name="default_logbook_name">👶 Mon premier carnet de bord</string>
<string name="logbook_created">Journal ajouté: </string> <string name="logbook_created">Journal ajouté: </string>
<string name="breastfeeding_timer_title">Allaitement en cours</string>
<string name="breastfeeding_timer_stop">Arrêter</string>
<string name="breastfeeding_timer_hint">Appuyez sur Arrêter quand terminé</string>
<string name="breastfeeding_timer_already_running">Une session d\'allaitement est déjà en cours</string>
<string name="breastfeeding_duration_title">Durée d\'allaitement</string>
<string name="breastfeeding_duration_description">Entrez la durée en minutes</string>
</resources> </resources>

View File

@@ -93,4 +93,11 @@
<string name="default_logbook_name">👶 Il mio primo diario</string> <string name="default_logbook_name">👶 Il mio primo diario</string>
<string name="logbook_created">Creato nuovo diario: </string> <string name="logbook_created">Creato nuovo diario: </string>
<string name="breastfeeding_timer_title">Allattamento in corso</string>
<string name="breastfeeding_timer_stop">Stop</string>
<string name="breastfeeding_timer_hint">Premi Stop quando hai finito</string>
<string name="breastfeeding_timer_already_running">Una sessione di allattamento è già in corso</string>
<string name="breastfeeding_duration_title">Durata allattamento</string>
<string name="breastfeeding_duration_description">Inserisci la durata in minuti</string>
</resources> </resources>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="AmountLabels">
<item>@string/amount_little</item>
<item>@string/amount_normal</item>
<item>@string/amount_plenty</item>
</string-array>
</resources>

View File

@@ -12,6 +12,9 @@
<string name="log_temperature_dialog_title">Temperature</string> <string name="log_temperature_dialog_title">Temperature</string>
<string name="log_temperature_dialog_description">Insert the temperature</string> <string name="log_temperature_dialog_description">Insert the temperature</string>
<string name="log_puke_dialog_title">Puke</string>
<string name="log_puke_dialog_description">Select the amount</string>
<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_scale_type" translatable="false">⚖️</string> <string name="event_scale_type" translatable="false">⚖️</string>
@@ -25,6 +28,8 @@
<string name="event_note_type" translatable="false">📝</string> <string name="event_note_type" translatable="false">📝</string>
<string name="event_temperature_type" translatable="false">🌡️</string> <string name="event_temperature_type" translatable="false">🌡️</string>
<string name="event_colic_type" translatable="false">💨</string> <string name="event_colic_type" translatable="false">💨</string>
<string name="event_puke_type" translatable="false">🤮</string>
<string name="event_bath_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> <string name="event_bottle_desc">Baby bottle</string>
@@ -40,6 +45,8 @@
<string name="event_note_desc">Note</string> <string name="event_note_desc">Note</string>
<string name="event_temperature_desc">Temperature</string> <string name="event_temperature_desc">Temperature</string>
<string name="event_colic_desc">Gaseous colic</string> <string name="event_colic_desc">Gaseous colic</string>
<string name="event_puke_desc">Puke</string>
<string name="event_bath_desc">Bath</string>
<string name="event_unknown_desc"></string> <string name="event_unknown_desc"></string>
<string name="overflow_event_scale">⚖️ Weight</string> <string name="overflow_event_scale">⚖️ Weight</string>
@@ -48,6 +55,8 @@
<string name="overflow_event_note">📝 Note</string> <string name="overflow_event_note">📝 Note</string>
<string name="overflow_event_temperature">🌡️ Temperature</string> <string name="overflow_event_temperature">🌡️ Temperature</string>
<string name="overflow_event_colic">💨 Gaseous colic</string> <string name="overflow_event_colic">💨 Gaseous colic</string>
<string name="overflow_event_puke">🤮 Puke</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>
@@ -66,14 +75,18 @@
<string name="year_ago">year</string> <string name="year_ago">year</string>
<string name="years_ago">years</string> <string name="years_ago">years</string>
<string name="amount_little">Little</string>
<string name="amount_normal">Normal</string>
<string name="amount_plenty">Plenty</string>
<string name="no_connection">No connection</string> <string name="no_connection">No connection</string>
<string name="no_connection_explain">Unable to reach WebDAV service</string> <string name="no_connection_explain">Unable to reach WebDAV service</string>
<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="no_breastfeeding">No Breastfeeding</string>
<string name="settings_title">Settings</string> <string name="settings_title">Settings</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_storage">Choose where to save data</string> <string name="settings_storage">Choose where to save data</string>
<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>
@@ -89,6 +102,8 @@
<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>
@@ -123,6 +138,7 @@
<string name="dialog_event_detail_delete_button">Delete</string> <string name="dialog_event_detail_delete_button">Delete</string>
<string name="dialog_event_detail_quantity">Quantity</string> <string name="dialog_event_detail_quantity">Quantity</string>
<string name="dialog_event_detail_notes">Notes</string> <string name="dialog_event_detail_notes">Notes</string>
<string name="dialog_event_detail_signature">by %s</string>
<string name="dialog_add_logbook_title">Add logbook</string> <string name="dialog_add_logbook_title">Add logbook</string>
<string name="dialog_add_logbook_logbookname">👶 Logbook name</string> <string name="dialog_add_logbook_logbookname">👶 Logbook name</string>
@@ -132,4 +148,12 @@
<string name="default_logbook_name">👶 My first logbook</string> <string name="default_logbook_name">👶 My first logbook</string>
<string name="logbook_created">New logbook created: </string> <string name="logbook_created">New logbook created: </string>
<string name="breastfeeding_timer_title">Breastfeeding in progress</string>
<string name="breastfeeding_timer_stop">Stop</string>
<string name="breastfeeding_timer_hint">Tap Stop when finished</string>
<string name="breastfeeding_timer_already_running">A breastfeeding session is already in progress</string>
<string name="breastfeeding_duration_title">Breastfeeding duration</string>
<string name="breastfeeding_duration_description">Enter the duration in minutes</string>
<string name="measurement_unit_time_minutes" translatable="false">min</string>
</resources> </resources>