3 Commits

Author SHA1 Message Date
97a138bdf6 Fix time display after manual duration correction
When a timer (sleep/breastfeeding) was forgotten and the duration is
manually corrected, the "X minutes ago" display now reflects the adjusted
end time instead of the original stop time.
2026-01-17 21:38:05 +01:00
2355dd4390 Add sleep tracking, statistics module and backup features
Features:
- Sleep tracking with timer and manual duration input
- Statistics module with 5 tabs (daily summary, feeding, diapers, sleep, growth)
- Export/Import backup functionality in settings
- Complete German, French and Italian translations
2026-01-12 08:27:03 +01:00
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
37 changed files with 3252 additions and 38 deletions

View File

@@ -13,24 +13,7 @@ Dedicated to my daughter Luna.
![Screenshot](fastlane/metadata/android/en-US/images/phoneScreenshots/1.png) ![Screenshot](fastlane/metadata/android/en-US/images/phoneScreenshots/1.png)
## Contributions ## Thanks for the valuable contributions to:
### Why isn't this hosted on GitHub?
I'm using a private git server just because I'm worried for the vast majority of open source code being hosted in a server property of Microsoft and being used to train theirs AI.
I didn't find a better option, BTW the Gitea project is working on implementing federation, so soon it will be possible to contribute using any other gitea server, selfhosted or not.
### How to contribute
The project is open to contribution, but with some limits:
- I'm sorry I can't accept AI-generated contributions. Reviewing a contribution requires time and effort from my side, while generating code with AI requires very little time and produces non reliable code that must be reviewed in detail. This is effectively shifting the work on my side, and in a forced way. If you feel you need a feature but you're not able to implement it by yourself, I prefer you to create an issue in the repository so I can implement it when I can, in a more mantainable way.
- I prefer to make project-wide changes (i.e. updating Android target, app name and icon, release number...) by myself.
To contribute, you'll have to create an account on this git instance. Unfortunately, I had to disable registration to avoid huge waves of fake accounts created by bots.
You can request an account writing to daniele.verducci@ichibi.eu
### Thanks for the valuable contributions to:
- Chepycou (French translation) - Chepycou (French translation)
- Daniel Neubauer (German translation) - Daniel Neubauer (German translation)

View File

@@ -12,8 +12,8 @@ android {
applicationId = "it.danieleverducci.lunatracker" applicationId = "it.danieleverducci.lunatracker"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 7 versionCode = 5
versionName = "0.9" versionName = "0.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -60,4 +60,4 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
} }

View File

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

View File

@@ -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
@@ -67,6 +69,19 @@ 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
// Sleep timer state
var sleepTimerStartTime: Long = 0
var sleepTimerDialog: AlertDialog? = null
var sleepTimerHandler: Handler? = null
var sleepTimerRunnable: Runnable? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -84,21 +99,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
@@ -119,6 +140,9 @@ class MainActivity : AppCompatActivity() {
findViewById<View>(R.id.button_settings).setOnClickListener { findViewById<View>(R.id.button_settings).setOnClickListener {
showSettings() showSettings()
} }
findViewById<View>(R.id.button_statistics).setOnClickListener {
showStatistics()
}
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()
@@ -136,6 +160,12 @@ class MainActivity : AppCompatActivity() {
} }
} }
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() {
@@ -143,6 +173,12 @@ class MainActivity : AppCompatActivity() {
startActivity(i) startActivity(i)
} }
fun showStatistics() {
val i = Intent(this, StatisticsActivity::class.java)
i.putExtra(StatisticsActivity.EXTRA_LOGBOOK_NAME, logbook?.name ?: "")
startActivity(i)
}
fun showLogbook() { fun showLogbook() {
// Show logbook // Show logbook
if (logbook == null) if (logbook == null)
@@ -180,6 +216,12 @@ class MainActivity : AppCompatActivity() {
// Update list dates // Update list dates
recyclerView.adapter?.notifyDataSetChanged() recyclerView.adapter?.notifyDataSetChanged()
// Check for ongoing breastfeeding timer
restoreBreastfeedingTimerIfNeeded()
// Check for ongoing sleep timer
restoreSleepTimerIfNeeded()
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)
@@ -192,6 +234,14 @@ 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()
// Clean up sleep timer UI (state is preserved in SharedPreferences)
sleepTimerRunnable?.let { sleepTimerHandler?.removeCallbacks(it) }
sleepTimerDialog?.dismiss()
super.onStop() super.onStop()
} }
@@ -275,7 +325,7 @@ class MainActivity : AppCompatActivity() {
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val pos = spinner.selectedItemPosition val pos = spinner.selectedItemPosition
logEvent(LunaEvent(LunaEvent.TYPE_PUKE, pos + 1)) logEvent(LunaEvent(LunaEvent.TYPE_PUKE, pos))
} }
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
val alertDialog = d.create() val alertDialog = d.create()
@@ -311,6 +361,230 @@ 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)
}
}
// Sleep timer methods
fun startSleepTimer() {
// Check if timer already running
if (sleepTimerStartTime > 0) {
Toast.makeText(this, R.string.sleep_timer_already_running, Toast.LENGTH_SHORT).show()
return
}
// Save timer state
sleepTimerStartTime = System.currentTimeMillis()
saveSleepTimerState()
// Show timer dialog
showSleepTimerDialog()
}
fun showSleepTimerDialog() {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.sleep_timer_dialog, null)
d.setTitle(R.string.sleep_timer_title)
d.setView(dialogView)
d.setCancelable(false)
val timerDisplay = dialogView.findViewById<TextView>(R.id.sleep_timer_display)
// Set up timer updates
sleepTimerHandler = Handler(mainLooper)
sleepTimerRunnable = object : Runnable {
override fun run() {
val elapsed = (System.currentTimeMillis() - sleepTimerStartTime) / 1000
val hours = elapsed / 3600
val minutes = (elapsed % 3600) / 60
val seconds = elapsed % 60
timerDisplay.text = if (hours > 0) {
String.format("%d:%02d:%02d", hours, minutes, seconds)
} else {
String.format("%02d:%02d", minutes, seconds)
}
sleepTimerHandler?.postDelayed(this, 1000)
}
}
sleepTimerHandler?.post(sleepTimerRunnable!!)
d.setPositiveButton(R.string.sleep_timer_stop) { _, _ ->
stopSleepTimer()
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
cancelSleepTimer()
dialogInterface.dismiss()
}
sleepTimerDialog = d.create()
sleepTimerDialog?.show()
}
fun stopSleepTimer() {
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
val durationMillis = System.currentTimeMillis() - sleepTimerStartTime
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
clearSleepTimerState()
logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, durationMinutes))
}
fun cancelSleepTimer() {
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
clearSleepTimerState()
}
fun askSleepDuration() {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.sleep_duration_dialog, null)
d.setTitle(R.string.sleep_duration_title)
d.setMessage(R.string.sleep_duration_description)
d.setView(dialogView)
val numberPicker = dialogView.findViewById<NumberPicker>(R.id.sleep_duration_picker)
numberPicker.minValue = 1
numberPicker.maxValue = 180 // Up to 3 hours
numberPicker.value = 30 // Default 30 minutes
numberPicker.wrapSelectorWheel = false
d.setPositiveButton(android.R.string.ok) { _, _ ->
logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, numberPicker.value))
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
dialogInterface.dismiss()
}
d.create().show()
}
fun saveSleepTimerState() {
LocalSettingsRepository(this).saveSleepTimer(sleepTimerStartTime)
}
fun clearSleepTimerState() {
sleepTimerStartTime = 0
sleepTimerDialog = null
LocalSettingsRepository(this).clearSleepTimer()
}
fun restoreSleepTimerIfNeeded() {
val startTime = LocalSettingsRepository(this).loadSleepTimer()
if (startTime > 0) {
sleepTimerStartTime = startTime
showSleepTimerDialog()
}
}
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)
@@ -398,6 +672,50 @@ class MainActivity : AppCompatActivity() {
}, startYear, startMonth, startDay).show() }, startYear, startMonth, startDay).show()
} }
// Make quantity editable for breastfeeding and sleep events
val quantityTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity)
val isBreastfeeding = event.type in listOf(
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
)
val isSleep = event.type == LunaEvent.TYPE_SLEEP
if ((isBreastfeeding || isSleep) && 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 = if (isSleep) {
layoutInflater.inflate(R.layout.sleep_duration_dialog, null)
} else {
layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
}
val picker = pickerView.findViewById<NumberPicker>(
if (isSleep) R.id.sleep_duration_picker else R.id.breastfeeding_duration_picker
)
picker.minValue = 1
picker.maxValue = if (isSleep) 180 else 60
val oldQuantity = event.quantity
picker.value = if (event.quantity > 0) Math.min(event.quantity, picker.maxValue) else if (isSleep) 30 else 15
pickerDialog.setTitle(if (isSleep) R.string.sleep_duration_title else R.string.breastfeeding_duration_title)
pickerDialog.setView(pickerView)
pickerDialog.setPositiveButton(android.R.string.ok) { _, _ ->
val newQuantity = picker.value
if (newQuantity != oldQuantity) {
// Adjust end time based on duration change (duration reduced = end time earlier)
event.time = event.time - (oldQuantity - newQuantity) * 60L
event.quantity = newQuantity
}
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) }
@@ -800,6 +1118,15 @@ class MainActivity : AppCompatActivity() {
isOutsideTouchable = true isOutsideTouchable = true
val inflater = LayoutInflater.from(anchor.context) val inflater = LayoutInflater.from(anchor.context)
contentView = inflater.inflate(R.layout.more_events_popup, null) contentView = inflater.inflate(R.layout.more_events_popup, null)
contentView.findViewById<View>(R.id.button_sleep).setOnClickListener {
startSleepTimer()
dismiss()
}
contentView.findViewById<View>(R.id.button_sleep).setOnLongClickListener {
askSleepDuration()
dismiss()
true
}
contentView.findViewById<View>(R.id.button_medicine).setOnClickListener { contentView.findViewById<View>(R.id.button_medicine).setOnClickListener {
askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE)) askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE))
dismiss() dismiss()

View File

@@ -1,20 +1,26 @@
package it.danieleverducci.lunatracker package it.danieleverducci.lunatracker
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.RadioButton import android.widget.RadioButton
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.switchmaterial.SwitchMaterial
import com.thegrizzlylabs.sardineandroid.impl.SardineException import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent
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.LogbookListObtainedListener import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException import okio.IOException
import org.json.JSONArray
import org.json.JSONObject
open class SettingsActivity : AppCompatActivity() { open class SettingsActivity : AppCompatActivity() {
protected lateinit var settingsRepository: LocalSettingsRepository protected lateinit var settingsRepository: LocalSettingsRepository
@@ -27,6 +33,15 @@ open class SettingsActivity : AppCompatActivity() {
protected lateinit var switchNoBreastfeeding: SwitchMaterial protected lateinit var switchNoBreastfeeding: SwitchMaterial
protected lateinit var textViewSignature: EditText protected lateinit var textViewSignature: EditText
// Activity Result Launchers for Export/Import
private val exportLauncher = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri -> uri?.let { exportLogbookToUri(it) } }
private val importLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri -> uri?.let { importLogbookFromUri(it) } }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -46,6 +61,12 @@ open class SettingsActivity : AppCompatActivity() {
findViewById<View>(R.id.settings_cancel).setOnClickListener({ findViewById<View>(R.id.settings_cancel).setOnClickListener({
finish() finish()
}) })
findViewById<View>(R.id.settings_export).setOnClickListener({
startExport()
})
findViewById<View>(R.id.settings_import).setOnClickListener({
startImport()
})
settingsRepository = LocalSettingsRepository(this) settingsRepository = LocalSettingsRepository(this)
loadSettings() loadSettings()
@@ -198,4 +219,136 @@ open class SettingsActivity : AppCompatActivity() {
fun onCopyLocalLogbooksToWebdavFinished(errors: String?) fun onCopyLocalLogbooksToWebdavFinished(errors: String?)
} }
// Export/Import functionality
private fun startExport() {
val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.US)
.format(java.util.Date())
exportLauncher.launch("lunatracker_backup_$timestamp.json")
}
private fun startImport() {
importLauncher.launch(arrayOf("application/json"))
}
private fun exportLogbookToUri(uri: Uri) {
progressIndicator.visibility = View.VISIBLE
Thread {
try {
val fileLogbookRepo = FileLogbookRepository()
val logbooks = fileLogbookRepo.getAllLogbooks(this)
val json = JSONObject().apply {
put("version", 1)
put("app", "LunaTracker")
put("exported_at", System.currentTimeMillis())
put("logbooks", JSONArray().apply {
logbooks.forEach { logbook ->
put(JSONObject().apply {
put("name", logbook.name)
put("events", JSONArray().apply {
logbook.logs.forEach { event ->
put(event.toJson())
}
})
})
}
})
}
contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(json.toString(2).toByteArray(Charsets.UTF_8))
}
val eventCount = logbooks.sumOf { it.logs.size }
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.export_success, eventCount),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.export_error) + e.message,
Toast.LENGTH_SHORT
).show()
}
}
}.start()
}
private fun importLogbookFromUri(uri: Uri) {
progressIndicator.visibility = View.VISIBLE
Thread {
try {
val jsonString = contentResolver.openInputStream(uri)?.bufferedReader()?.readText()
?: throw Exception("Could not read file")
val json = JSONObject(jsonString)
val version = json.optInt("version", 1)
val fileLogbookRepo = FileLogbookRepository()
var totalEvents = 0
if (json.has("logbooks")) {
// New format with multiple logbooks
val logbooksArray = json.getJSONArray("logbooks")
for (i in 0 until logbooksArray.length()) {
val logbookJson = logbooksArray.getJSONObject(i)
val name = logbookJson.optString("name", "")
val eventsArray = logbookJson.getJSONArray("events")
val logbook = Logbook(name)
for (j in 0 until eventsArray.length()) {
try {
logbook.logs.add(LunaEvent(eventsArray.getJSONObject(j)))
totalEvents++
} catch (e: IllegalArgumentException) {
// Skip invalid events
}
}
fileLogbookRepo.saveLogbook(this, logbook)
}
} else if (json.has("events")) {
// Old format with single logbook
val name = json.optString("logbook_name", "")
val eventsArray = json.getJSONArray("events")
val logbook = Logbook(name)
for (i in 0 until eventsArray.length()) {
try {
logbook.logs.add(LunaEvent(eventsArray.getJSONObject(i)))
totalEvents++
} catch (e: IllegalArgumentException) {
// Skip invalid events
}
}
fileLogbookRepo.saveLogbook(this, logbook)
}
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.import_success, totalEvents),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.import_error) + ": " + e.message,
Toast.LENGTH_LONG
).show()
}
}
}.start()
}
} }

View File

@@ -0,0 +1,207 @@
package it.danieleverducci.lunatracker
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.fragments.DailySummaryFragment
import it.danieleverducci.lunatracker.fragments.DiaperStatsFragment
import it.danieleverducci.lunatracker.fragments.FeedingStatsFragment
import it.danieleverducci.lunatracker.fragments.GrowthStatsFragment
import it.danieleverducci.lunatracker.fragments.SleepStatsFragment
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookLoadedListener
import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException
import org.json.JSONException
import utils.StatisticsCalculator
class StatisticsActivity : AppCompatActivity() {
companion object {
const val TAG = "StatisticsActivity"
const val EXTRA_LOGBOOK_NAME = "logbook_name"
}
private lateinit var viewPager: ViewPager2
private lateinit var tabLayout: TabLayout
private lateinit var periodSpinner: Spinner
private var events: List<LunaEvent> = emptyList()
private var selectedPeriod: Int = 7 // Default 7 days
private var logbookName: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_statistics)
// Back button
findViewById<ImageView>(R.id.button_back).setOnClickListener {
finish()
}
// Title with logbook name
logbookName = intent.getStringExtra(EXTRA_LOGBOOK_NAME) ?: ""
if (logbookName.isNotEmpty()) {
findViewById<TextView>(R.id.statistics_title).text =
"${getString(R.string.statistics_title)} - $logbookName"
}
// Period spinner
periodSpinner = findViewById(R.id.period_spinner)
val periods = arrayOf(
getString(R.string.stats_period_7days),
getString(R.string.stats_period_14days),
getString(R.string.stats_period_30days)
)
val periodAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, periods)
periodAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
periodSpinner.adapter = periodAdapter
periodSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedPeriod = when (position) {
0 -> 7
1 -> 14
2 -> 30
else -> 7
}
notifyFragmentsOfPeriodChange()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
// ViewPager and TabLayout
viewPager = findViewById(R.id.view_pager)
tabLayout = findViewById(R.id.tab_layout)
// Load events
loadEvents()
}
private fun loadEvents() {
val settingsRepo = LocalSettingsRepository(this)
val repository: LogbookRepository = when (settingsRepo.loadDataRepository()) {
LocalSettingsRepository.DATA_REPO.WEBDAV -> {
val credentials = settingsRepo.loadWebdavCredentials()
if (credentials != null) {
WebDAVLogbookRepository(credentials[0], credentials[1], credentials[2])
} else {
FileLogbookRepository()
}
}
LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> FileLogbookRepository()
}
repository.loadLogbook(this, logbookName, object : LogbookLoadedListener {
override fun onLogbookLoaded(logbook: Logbook) {
runOnUiThread {
events = logbook.logs
setupViewPager()
}
}
override fun onIOError(error: IOException) {
Log.e(TAG, "IO error loading logbook", error)
runOnUiThread {
events = emptyList()
setupViewPager()
}
}
override fun onWebDAVError(error: SardineException) {
Log.e(TAG, "WebDAV error loading logbook", error)
runOnUiThread {
events = emptyList()
setupViewPager()
}
}
override fun onJSONError(error: JSONException) {
Log.e(TAG, "JSON error loading logbook", error)
runOnUiThread {
events = emptyList()
setupViewPager()
}
}
override fun onError(error: Exception) {
Log.e(TAG, "Error loading logbook", error)
runOnUiThread {
events = emptyList()
setupViewPager()
}
}
})
}
private fun setupViewPager() {
val adapter = StatisticsPagerAdapter(this)
viewPager.adapter = adapter
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
0 -> getString(R.string.stats_tab_today)
1 -> getString(R.string.stats_tab_feeding)
2 -> getString(R.string.stats_tab_diapers)
3 -> getString(R.string.stats_tab_sleep)
4 -> getString(R.string.stats_tab_growth)
else -> ""
}
}.attach()
}
private fun notifyFragmentsOfPeriodChange() {
if (events.isEmpty()) return
// Force fragment refresh by recreating adapter
val currentItem = viewPager.currentItem
viewPager.adapter = StatisticsPagerAdapter(this)
viewPager.setCurrentItem(currentItem, false)
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = when (position) {
0 -> getString(R.string.stats_tab_today)
1 -> getString(R.string.stats_tab_feeding)
2 -> getString(R.string.stats_tab_diapers)
3 -> getString(R.string.stats_tab_sleep)
4 -> getString(R.string.stats_tab_growth)
else -> ""
}
}.attach()
}
fun getEvents(): List<LunaEvent> = events
fun getSelectedPeriod(): Int = selectedPeriod
fun getCalculator(): StatisticsCalculator = StatisticsCalculator(events)
private inner class StatisticsPagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = 5
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> DailySummaryFragment()
1 -> FeedingStatsFragment()
2 -> DiaperStatsFragment()
3 -> SleepStatsFragment()
4 -> GrowthStatsFragment()
else -> DailySummaryFragment()
}
}
}
}

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

@@ -30,6 +30,7 @@ class LunaEvent: Comparable<LunaEvent> {
const val TYPE_FOOD = "FOOD" const val TYPE_FOOD = "FOOD"
const val TYPE_PUKE = "PUKE" const val TYPE_PUKE = "PUKE"
const val TYPE_BATH = "BATH" const val TYPE_BATH = "BATH"
const val TYPE_SLEEP = "SLEEP"
} }
private val jo: JSONObject private val jo: JSONObject
@@ -100,6 +101,7 @@ class LunaEvent: Comparable<LunaEvent> {
TYPE_FOOD -> R.string.event_food_type TYPE_FOOD -> R.string.event_food_type
TYPE_PUKE -> R.string.event_puke_type TYPE_PUKE -> R.string.event_puke_type
TYPE_BATH -> R.string.event_bath_type TYPE_BATH -> R.string.event_bath_type
TYPE_SLEEP -> R.string.event_sleep_type
else -> R.string.event_unknown_type else -> R.string.event_unknown_type
} }
) )
@@ -123,6 +125,7 @@ class LunaEvent: Comparable<LunaEvent> {
TYPE_FOOD -> R.string.event_food_desc TYPE_FOOD -> R.string.event_food_desc
TYPE_PUKE -> R.string.event_puke_desc TYPE_PUKE -> R.string.event_puke_desc
TYPE_BATH -> R.string.event_bath_desc TYPE_BATH -> R.string.event_bath_desc
TYPE_SLEEP -> R.string.event_sleep_desc
else -> R.string.event_unknown_desc else -> R.string.event_unknown_desc
} }
) )

View File

@@ -0,0 +1,123 @@
package it.danieleverducci.lunatracker.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.fragment.app.Fragment
import it.danieleverducci.lunatracker.R
import it.danieleverducci.lunatracker.StatisticsActivity
import utils.DailySummary
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class DailySummaryFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_daily_summary, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateUI(view)
}
private fun updateUI(view: View) {
val activity = activity as? StatisticsActivity ?: return
val calculator = activity.getCalculator()
val period = activity.getSelectedPeriod()
// Get today's summary and average for comparison
val todaySummary = calculator.getTodaySummary()
val feedingStats = calculator.getFeedingStats(period)
val sleepStats = calculator.getSleepStats(period)
val diaperStats = calculator.getDiaperStats(period)
// Date header
val dateFormat = SimpleDateFormat("EEEE, d MMMM", Locale.getDefault())
view.findViewById<TextView>(R.id.date_header).text = dateFormat.format(Date())
// Bottle summary
val bottleSummary = view.findViewById<TextView>(R.id.bottle_summary)
val bottleProgress = view.findViewById<ProgressBar>(R.id.bottle_progress)
val avgBottle = feedingStats.avgBottleMlPerDay.toInt()
bottleSummary.text = "${todaySummary.totalBottleMl} ml (${todaySummary.bottleCount}×) | Ø $avgBottle ml"
if (avgBottle > 0) {
bottleProgress.max = (avgBottle * 1.5).toInt()
bottleProgress.progress = todaySummary.totalBottleMl
}
// Breastfeeding summary
val breastfeedingContainer = view.findViewById<LinearLayout>(R.id.breastfeeding_container)
val breastfeedingSummary = view.findViewById<TextView>(R.id.breastfeeding_summary)
val breastfeedingProgress = view.findViewById<ProgressBar>(R.id.breastfeeding_progress)
if (todaySummary.breastfeedingCount > 0 || feedingStats.avgBreastfeedingMinPerDay > 0) {
breastfeedingContainer.visibility = View.VISIBLE
val avgBf = feedingStats.avgBreastfeedingMinPerDay.toInt()
breastfeedingSummary.text = "${todaySummary.totalBreastfeedingMin} min (${todaySummary.breastfeedingCount}×) | Ø $avgBf min"
if (avgBf > 0) {
breastfeedingProgress.max = (avgBf * 1.5).toInt()
breastfeedingProgress.progress = todaySummary.totalBreastfeedingMin
}
} else {
breastfeedingContainer.visibility = View.GONE
}
// Sleep summary
val sleepSummary = view.findViewById<TextView>(R.id.sleep_summary)
val sleepProgress = view.findViewById<ProgressBar>(R.id.sleep_progress)
val avgSleepMin = sleepStats.avgSleepMinPerDay.toInt()
val todaySleepHours = todaySummary.totalSleepMin / 60f
val avgSleepHours = avgSleepMin / 60f
sleepSummary.text = String.format(Locale.getDefault(), "%.1f h (%d×) | Ø %.1f h",
todaySleepHours, todaySummary.sleepCount, avgSleepHours)
if (avgSleepMin > 0) {
sleepProgress.max = (avgSleepMin * 1.5).toInt()
sleepProgress.progress = todaySummary.totalSleepMin
}
// Diaper summaries
val pooSummary = view.findViewById<TextView>(R.id.poo_summary)
val peeSummary = view.findViewById<TextView>(R.id.pee_summary)
pooSummary.text = String.format(Locale.getDefault(), "%d× | Ø %.1f",
todaySummary.diaperPooCount, diaperStats.avgPooPerDay)
peeSummary.text = String.format(Locale.getDefault(), "%d× | Ø %.1f",
todaySummary.diaperPeeCount, diaperStats.avgPeePerDay)
// Health card (weight/temperature)
val healthCard = view.findViewById<LinearLayout>(R.id.health_card)
val weightSummary = view.findViewById<TextView>(R.id.weight_summary)
val tempSummary = view.findViewById<TextView>(R.id.temperature_summary)
if (todaySummary.latestWeight != null || todaySummary.latestTemperature != null) {
healthCard.visibility = View.VISIBLE
if (todaySummary.latestWeight != null) {
val weightKg = todaySummary.latestWeight / 1000f
weightSummary.text = "⚖️ ${String.format(Locale.getDefault(), "%.2f kg", weightKg)}"
weightSummary.visibility = View.VISIBLE
} else {
weightSummary.visibility = View.GONE
}
if (todaySummary.latestTemperature != null) {
val tempC = todaySummary.latestTemperature / 10f
tempSummary.text = "🌡️ ${String.format(Locale.getDefault(), "%.1f °C", tempC)}"
tempSummary.visibility = View.VISIBLE
} else {
tempSummary.visibility = View.GONE
}
} else {
healthCard.visibility = View.GONE
}
}
}

View File

@@ -0,0 +1,124 @@
package it.danieleverducci.lunatracker.fragments
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
import it.danieleverducci.lunatracker.R
import it.danieleverducci.lunatracker.StatisticsActivity
import utils.DateUtils
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class DiaperStatsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_diaper_stats, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateUI(view)
}
private fun updateUI(view: View) {
val activity = activity as? StatisticsActivity ?: return
val calculator = activity.getCalculator()
val period = activity.getSelectedPeriod()
val stats = calculator.getDiaperStats(period)
// Draw stacked bar chart
val chartContainer = view.findViewById<LinearLayout>(R.id.chart_container)
val chartLabels = view.findViewById<LinearLayout>(R.id.chart_labels)
chartContainer.removeAllViews()
chartLabels.removeAllViews()
val sortedDays = stats.dailyPooCount.keys.sorted().takeLast(period)
var maxValue = 1
for (day in sortedDays) {
val total = (stats.dailyPooCount[day] ?: 0) + (stats.dailyPeeCount[day] ?: 0)
if (total > maxValue) maxValue = total
}
val dateFormat = SimpleDateFormat("E", Locale.getDefault())
for (day in sortedDays) {
val pooCount = stats.dailyPooCount[day] ?: 0
val peeCount = stats.dailyPeeCount[day] ?: 0
val total = pooCount + peeCount
// Bar container
val barContainer = LinearLayout(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f)
orientation = LinearLayout.VERTICAL
gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
setPadding(4, 0, 4, 0)
}
// Pee bar (lighter, on bottom)
if (peeCount > 0) {
val peeHeight = (peeCount.toFloat() / maxValue * 100).toInt()
val peeBar = View(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(peeHeight * resources.displayMetrics.density).toInt()
)
setBackgroundColor(0x66FFE68F.toInt()) // Semi-transparent accent
}
barContainer.addView(peeBar, 0)
}
// Poo bar (solid, on top)
if (pooCount > 0) {
val pooHeight = (pooCount.toFloat() / maxValue * 100).toInt()
val pooBar = View(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(pooHeight * resources.displayMetrics.density).toInt()
)
setBackgroundColor(resources.getColor(R.color.accent, null))
}
barContainer.addView(pooBar, 0)
}
chartContainer.addView(barContainer)
// Label
val label = TextView(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
text = dateFormat.format(Date(day * 1000))
textSize = 10f
gravity = Gravity.CENTER
}
chartLabels.addView(label)
}
// Summary stats
view.findViewById<TextView>(R.id.avg_diapers).text =
getString(R.string.stats_avg_diapers, stats.avgDiapersPerDay)
view.findViewById<TextView>(R.id.avg_poo).text =
getString(R.string.stats_avg_poo, stats.avgPooPerDay)
view.findViewById<TextView>(R.id.avg_pee).text =
getString(R.string.stats_avg_pee, stats.avgPeePerDay)
// Last poo
val lastPoo = view.findViewById<TextView>(R.id.last_poo)
if (stats.lastPooTime != null) {
val timeAgo = DateUtils.formatTimeAgo(requireContext(), stats.lastPooTime)
lastPoo.text = getString(R.string.stats_last_poo, timeAgo)
lastPoo.visibility = View.VISIBLE
} else {
lastPoo.visibility = View.GONE
}
}
}

View File

@@ -0,0 +1,123 @@
package it.danieleverducci.lunatracker.fragments
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.fragment.app.Fragment
import it.danieleverducci.lunatracker.R
import it.danieleverducci.lunatracker.StatisticsActivity
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class FeedingStatsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_feeding_stats, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateUI(view)
}
private fun updateUI(view: View) {
val activity = activity as? StatisticsActivity ?: return
val calculator = activity.getCalculator()
val period = activity.getSelectedPeriod()
val stats = calculator.getFeedingStats(period)
// Draw bar chart
val chartContainer = view.findViewById<LinearLayout>(R.id.chart_container)
val chartLabels = view.findViewById<LinearLayout>(R.id.chart_labels)
chartContainer.removeAllViews()
chartLabels.removeAllViews()
val sortedDays = stats.dailyBottleTotals.keys.sorted().takeLast(period)
val maxValue = (stats.dailyBottleTotals.values.maxOrNull() ?: 1).coerceAtLeast(1)
val dateFormat = SimpleDateFormat("E", Locale.getDefault())
for (day in sortedDays) {
val value = stats.dailyBottleTotals[day] ?: 0
// Bar
val barContainer = LinearLayout(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f)
orientation = LinearLayout.VERTICAL
gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
setPadding(4, 0, 4, 0)
}
val barHeight = if (maxValue > 0) (value.toFloat() / maxValue * 100).toInt() else 0
val bar = View(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
0
).apply {
height = (barHeight * resources.displayMetrics.density).toInt()
}
setBackgroundColor(resources.getColor(R.color.accent, null))
}
barContainer.addView(bar)
chartContainer.addView(barContainer)
// Label
val label = TextView(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
text = dateFormat.format(Date(day * 1000))
textSize = 10f
gravity = Gravity.CENTER
}
chartLabels.addView(label)
}
// Bottle stats
val bottleAvg = view.findViewById<TextView>(R.id.bottle_avg_daily)
bottleAvg.text = getString(R.string.stats_avg_ml_per_day, stats.avgBottleMlPerDay)
val feedingInterval = view.findViewById<TextView>(R.id.feeding_interval)
feedingInterval.text = getString(R.string.stats_feeding_interval, stats.avgFeedingIntervalMinutes.toInt())
// Breastfeeding stats
val breastfeedingCard = view.findViewById<LinearLayout>(R.id.breastfeeding_card)
val totalBreastfeeding = stats.leftBreastCount + stats.rightBreastCount + stats.bothBreastCount
if (totalBreastfeeding > 0) {
breastfeedingCard.visibility = View.VISIBLE
val avgDuration = view.findViewById<TextView>(R.id.breastfeeding_avg_duration)
avgDuration.text = getString(R.string.stats_avg_duration, stats.avgBreastfeedingDuration)
// Side distribution (excluding "both")
val sideTotal = stats.leftBreastCount + stats.rightBreastCount
if (sideTotal > 0) {
val leftPercent = (stats.leftBreastCount.toFloat() / sideTotal * 100).toInt()
val rightPercent = (stats.rightBreastCount.toFloat() / sideTotal * 100).toInt()
val leftProgress = view.findViewById<ProgressBar>(R.id.left_progress)
val rightProgress = view.findViewById<ProgressBar>(R.id.right_progress)
val leftPercentText = view.findViewById<TextView>(R.id.left_percent)
val rightPercentText = view.findViewById<TextView>(R.id.right_percent)
leftProgress.max = 100
leftProgress.progress = leftPercent
rightProgress.max = 100
rightProgress.progress = rightPercent
leftPercentText.text = "$leftPercent%"
rightPercentText.text = "$rightPercent%"
}
} else {
breastfeedingCard.visibility = View.GONE
}
}
}

View File

@@ -0,0 +1,139 @@
package it.danieleverducci.lunatracker.fragments
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
import it.danieleverducci.lunatracker.R
import it.danieleverducci.lunatracker.StatisticsActivity
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class GrowthStatsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_growth_stats, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateUI(view)
}
private fun updateUI(view: View) {
val activity = activity as? StatisticsActivity ?: return
val calculator = activity.getCalculator()
val weightHistory = calculator.getWeightHistory()
val noDataMessage = view.findViewById<TextView>(R.id.no_data_message)
if (weightHistory.isEmpty()) {
noDataMessage.visibility = View.VISIBLE
view.findViewById<View>(R.id.chart_container).visibility = View.GONE
view.findViewById<View>(R.id.chart_labels).visibility = View.GONE
view.findViewById<TextView>(R.id.current_weight).visibility = View.GONE
view.findViewById<TextView>(R.id.weight_gain_week).visibility = View.GONE
view.findViewById<TextView>(R.id.weight_gain_month).visibility = View.GONE
return
}
noDataMessage.visibility = View.GONE
// Draw weight chart (line chart approximated with bars)
val chartContainer = view.findViewById<LinearLayout>(R.id.chart_container)
val chartLabels = view.findViewById<LinearLayout>(R.id.chart_labels)
chartContainer.removeAllViews()
chartLabels.removeAllViews()
val recentWeights = weightHistory.takeLast(10) // Show last 10 measurements
val minWeight = recentWeights.minOfOrNull { it.weightGrams } ?: 0
val maxWeight = recentWeights.maxOfOrNull { it.weightGrams } ?: 1
val weightRange = (maxWeight - minWeight).coerceAtLeast(100) // At least 100g range
val dateFormat = SimpleDateFormat("d/M", Locale.getDefault())
for (point in recentWeights) {
// Bar container
val barContainer = LinearLayout(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f)
orientation = LinearLayout.VERTICAL
gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
setPadding(4, 0, 4, 0)
}
// Calculate relative height (showing weight above minimum)
val relativeWeight = point.weightGrams - minWeight + (weightRange * 0.1).toInt()
val barHeight = (relativeWeight.toFloat() / (weightRange * 1.2) * 100).toInt()
val bar = View(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(barHeight * resources.displayMetrics.density).toInt()
)
setBackgroundColor(resources.getColor(R.color.accent, null))
}
barContainer.addView(bar)
// Weight value on top
val weightLabel = TextView(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
val kg = point.weightGrams / 1000f
text = String.format(Locale.getDefault(), "%.1f", kg)
textSize = 8f
gravity = Gravity.CENTER
}
barContainer.addView(weightLabel, 0)
chartContainer.addView(barContainer)
// Date label
val label = TextView(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
text = dateFormat.format(Date(point.time * 1000))
textSize = 10f
gravity = Gravity.CENTER
}
chartLabels.addView(label)
}
// Current weight
val currentWeight = weightHistory.lastOrNull()
if (currentWeight != null) {
val kg = currentWeight.weightGrams / 1000f
view.findViewById<TextView>(R.id.current_weight).text =
getString(R.string.stats_current_weight, String.format(Locale.getDefault(), "%.2f kg", kg))
}
// Weight gain calculations
val gainWeek = calculator.getWeightGainForDays(7)
val gainMonth = calculator.getWeightGainForDays(30)
val weekView = view.findViewById<TextView>(R.id.weight_gain_week)
val monthView = view.findViewById<TextView>(R.id.weight_gain_month)
if (gainWeek != null) {
weekView.text = getString(R.string.stats_weight_gain_week, gainWeek)
weekView.visibility = View.VISIBLE
} else {
weekView.visibility = View.GONE
}
if (gainMonth != null) {
monthView.text = getString(R.string.stats_weight_gain_month, gainMonth)
monthView.visibility = View.VISIBLE
} else {
monthView.visibility = View.GONE
}
}
}

View File

@@ -0,0 +1,105 @@
package it.danieleverducci.lunatracker.fragments
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
import it.danieleverducci.lunatracker.R
import it.danieleverducci.lunatracker.StatisticsActivity
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class SleepStatsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_sleep_stats, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateUI(view)
}
private fun updateUI(view: View) {
val activity = activity as? StatisticsActivity ?: return
val calculator = activity.getCalculator()
val period = activity.getSelectedPeriod()
val stats = calculator.getSleepStats(period)
// Check if we have any sleep data
val hasSleepData = stats.dailyTotals.values.any { it > 0 }
val noDataMessage = view.findViewById<TextView>(R.id.no_data_message)
if (!hasSleepData) {
noDataMessage.visibility = View.VISIBLE
view.findViewById<View>(R.id.chart_container).visibility = View.GONE
view.findViewById<View>(R.id.chart_labels).visibility = View.GONE
return
}
noDataMessage.visibility = View.GONE
// Draw bar chart (showing hours per day)
val chartContainer = view.findViewById<LinearLayout>(R.id.chart_container)
val chartLabels = view.findViewById<LinearLayout>(R.id.chart_labels)
chartContainer.removeAllViews()
chartLabels.removeAllViews()
val sortedDays = stats.dailyTotals.keys.sorted().takeLast(period)
val maxValue = (stats.dailyTotals.values.maxOrNull() ?: 1).coerceAtLeast(1)
val dateFormat = SimpleDateFormat("E", Locale.getDefault())
for (day in sortedDays) {
val minutes = stats.dailyTotals[day] ?: 0
// Bar container
val barContainer = LinearLayout(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f)
orientation = LinearLayout.VERTICAL
gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
setPadding(4, 0, 4, 0)
}
val barHeight = if (maxValue > 0) (minutes.toFloat() / maxValue * 100).toInt() else 0
val bar = View(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(barHeight * resources.displayMetrics.density).toInt()
)
setBackgroundColor(resources.getColor(R.color.accent, null))
}
barContainer.addView(bar)
chartContainer.addView(barContainer)
// Label
val label = TextView(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
text = dateFormat.format(Date(day * 1000))
textSize = 10f
gravity = Gravity.CENTER
}
chartLabels.addView(label)
}
// Summary stats
val avgSleepHours = stats.avgSleepMinPerDay / 60f
view.findViewById<TextView>(R.id.avg_sleep_per_day).text =
getString(R.string.stats_avg_sleep, avgSleepHours)
view.findViewById<TextView>(R.id.avg_naps_per_day).text =
getString(R.string.stats_avg_naps, stats.avgNapsPerDay)
view.findViewById<TextView>(R.id.avg_nap_duration).text =
getString(R.string.stats_avg_nap_duration, stats.avgNapDurationMin)
view.findViewById<TextView>(R.id.longest_sleep).text =
getString(R.string.stats_longest_sleep, stats.longestSleepMin)
}
}

View File

@@ -15,6 +15,9 @@ class LocalSettingsRepository(val context: Context) {
const val SHARED_PREFS_DAV_PASS = "webdav_password" const val SHARED_PREFS_DAV_PASS = "webdav_password"
const val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding" const val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding"
const val SHARED_PREFS_SIGNATURE = "signature" 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"
const val SHARED_PREFS_SLEEP_TIMER_START = "sleep_timer_start"
} }
enum class DATA_REPO {LOCAL_FILE, WEBDAV} enum class DATA_REPO {LOCAL_FILE, WEBDAV}
val sharedPreferences: SharedPreferences val sharedPreferences: SharedPreferences
@@ -84,4 +87,41 @@ 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)
}
}
fun saveSleepTimer(startTime: Long) {
sharedPreferences.edit {
putLong(SHARED_PREFS_SLEEP_TIMER_START, startTime)
}
}
fun loadSleepTimer(): Long {
return sharedPreferences.getLong(SHARED_PREFS_SLEEP_TIMER_START, 0)
}
fun clearSleepTimer() {
sharedPreferences.edit {
remove(SHARED_PREFS_SLEEP_TIMER_START)
}
}
} }

View File

@@ -67,7 +67,7 @@ class NumericUtils (val context: Context) {
LunaEvent.TYPE_TEMPERATURE -> LunaEvent.TYPE_TEMPERATURE ->
(item.quantity / 10.0f).toString() (item.quantity / 10.0f).toString()
LunaEvent.TYPE_PUKE -> LunaEvent.TYPE_PUKE ->
context.resources.getStringArray(R.array.AmountLabels)[item.quantity - 1] context.resources.getStringArray(R.array.AmountLabels)[item.quantity]
else -> else ->
item.quantity item.quantity
}) })
@@ -79,6 +79,11 @@ 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,
LunaEvent.TYPE_SLEEP ->
context.getString(R.string.measurement_unit_time_minutes)
else -> "" else -> ""
} }
) )

View File

@@ -0,0 +1,343 @@
package utils
import it.danieleverducci.lunatracker.entities.LunaEvent
import java.util.Calendar
/**
* Data classes for statistics results
*/
data class DailySummary(
val date: Long,
val totalBottleMl: Int,
val bottleCount: Int,
val totalBreastfeedingMin: Int,
val breastfeedingCount: Int,
val breastfeedingLeftCount: Int,
val breastfeedingRightCount: Int,
val totalSleepMin: Int,
val sleepCount: Int,
val diaperPooCount: Int,
val diaperPeeCount: Int,
val totalFoodCount: Int,
val latestWeight: Int?,
val latestTemperature: Int?
)
data class FeedingStats(
val dailyBottleTotals: Map<Long, Int>,
val dailyBreastfeedingTotals: Map<Long, Int>,
val avgBottleMlPerDay: Float,
val avgBreastfeedingMinPerDay: Float,
val leftBreastCount: Int,
val rightBreastCount: Int,
val bothBreastCount: Int,
val avgBreastfeedingDuration: Float,
val avgFeedingIntervalMinutes: Long
)
data class DiaperStats(
val dailyPooCount: Map<Long, Int>,
val dailyPeeCount: Map<Long, Int>,
val avgDiapersPerDay: Float,
val avgPooPerDay: Float,
val avgPeePerDay: Float,
val lastPooTime: Long?
)
data class SleepStats(
val dailyTotals: Map<Long, Int>,
val avgSleepMinPerDay: Float,
val avgNapsPerDay: Float,
val avgNapDurationMin: Float,
val longestSleepMin: Int,
val lastSleepTime: Long?
)
data class WeightPoint(
val time: Long,
val weightGrams: Int
)
data class TemperaturePoint(
val time: Long,
val temperatureDeciCelsius: Int
)
/**
* Calculator for statistics based on LunaEvent data
*/
class StatisticsCalculator(private val events: List<LunaEvent>) {
private fun getStartOfDay(unixTimeSeconds: Long): Long {
val cal = Calendar.getInstance()
cal.timeInMillis = unixTimeSeconds * 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 / 1000
}
private fun getEventsInRange(startUnix: Long, endUnix: Long): List<LunaEvent> {
return events.filter { it.time >= startUnix && it.time < endUnix }
}
private fun getEventsForDays(days: Int): List<LunaEvent> {
val now = System.currentTimeMillis() / 1000
val startOfToday = getStartOfDay(now)
val startTime = startOfToday - (days - 1) * 24 * 60 * 60
return events.filter { it.time >= startTime }
}
/**
* Get summary for a specific day (unix timestamp in seconds)
*/
fun getDailySummary(dayUnix: Long): DailySummary {
val startOfDay = getStartOfDay(dayUnix)
val endOfDay = startOfDay + 24 * 60 * 60
val dayEvents = getEventsInRange(startOfDay, endOfDay)
val bottleEvents = dayEvents.filter { it.type == LunaEvent.TYPE_BABY_BOTTLE }
val breastfeedingEvents = dayEvents.filter {
it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE ||
it.type == LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE ||
it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
}
val sleepEvents = dayEvents.filter { it.type == LunaEvent.TYPE_SLEEP }
val pooEvents = dayEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_POO }
val peeEvents = dayEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_PEE }
val foodEvents = dayEvents.filter { it.type == LunaEvent.TYPE_FOOD }
val weightEvents = dayEvents.filter { it.type == LunaEvent.TYPE_WEIGHT }
val tempEvents = dayEvents.filter { it.type == LunaEvent.TYPE_TEMPERATURE }
return DailySummary(
date = startOfDay,
totalBottleMl = bottleEvents.sumOf { it.quantity },
bottleCount = bottleEvents.size,
totalBreastfeedingMin = breastfeedingEvents.sumOf { it.quantity },
breastfeedingCount = breastfeedingEvents.size,
breastfeedingLeftCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE },
breastfeedingRightCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE },
totalSleepMin = sleepEvents.sumOf { it.quantity },
sleepCount = sleepEvents.size,
diaperPooCount = pooEvents.size,
diaperPeeCount = peeEvents.size,
totalFoodCount = foodEvents.size,
latestWeight = weightEvents.maxByOrNull { it.time }?.quantity,
latestTemperature = tempEvents.maxByOrNull { it.time }?.quantity
)
}
/**
* Get today's summary
*/
fun getTodaySummary(): DailySummary {
return getDailySummary(System.currentTimeMillis() / 1000)
}
/**
* Get feeding statistics for the last N days
*/
fun getFeedingStats(days: Int): FeedingStats {
val relevantEvents = getEventsForDays(days)
val now = System.currentTimeMillis() / 1000
val startOfToday = getStartOfDay(now)
// Daily totals
val dailyBottleTotals = mutableMapOf<Long, Int>()
val dailyBreastfeedingTotals = mutableMapOf<Long, Int>()
for (i in 0 until days) {
val dayStart = startOfToday - i * 24 * 60 * 60
dailyBottleTotals[dayStart] = 0
dailyBreastfeedingTotals[dayStart] = 0
}
val bottleEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_BABY_BOTTLE }
val breastfeedingEvents = relevantEvents.filter {
it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE ||
it.type == LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE ||
it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
}
bottleEvents.forEach { event ->
val dayStart = getStartOfDay(event.time)
dailyBottleTotals[dayStart] = (dailyBottleTotals[dayStart] ?: 0) + event.quantity
}
breastfeedingEvents.forEach { event ->
val dayStart = getStartOfDay(event.time)
dailyBreastfeedingTotals[dayStart] = (dailyBreastfeedingTotals[dayStart] ?: 0) + event.quantity
}
// Breastfeeding side distribution
val leftCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE }
val rightCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE }
val bothCount = breastfeedingEvents.count { it.type == LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE }
// Average breastfeeding duration
val avgBreastfeedingDuration = if (breastfeedingEvents.isNotEmpty()) {
breastfeedingEvents.sumOf { it.quantity }.toFloat() / breastfeedingEvents.size
} else 0f
// Average feeding interval (all feeding events sorted by time)
val allFeedingEvents = (bottleEvents + breastfeedingEvents).sortedBy { it.time }
val avgFeedingIntervalMinutes = if (allFeedingEvents.size > 1) {
var totalInterval = 0L
for (i in 1 until allFeedingEvents.size) {
totalInterval += allFeedingEvents[i].time - allFeedingEvents[i-1].time
}
(totalInterval / (allFeedingEvents.size - 1)) / 60
} else 0L
return FeedingStats(
dailyBottleTotals = dailyBottleTotals,
dailyBreastfeedingTotals = dailyBreastfeedingTotals,
avgBottleMlPerDay = if (days > 0) dailyBottleTotals.values.sum().toFloat() / days else 0f,
avgBreastfeedingMinPerDay = if (days > 0) dailyBreastfeedingTotals.values.sum().toFloat() / days else 0f,
leftBreastCount = leftCount,
rightBreastCount = rightCount,
bothBreastCount = bothCount,
avgBreastfeedingDuration = avgBreastfeedingDuration,
avgFeedingIntervalMinutes = avgFeedingIntervalMinutes
)
}
/**
* Get diaper statistics for the last N days
*/
fun getDiaperStats(days: Int): DiaperStats {
val relevantEvents = getEventsForDays(days)
val now = System.currentTimeMillis() / 1000
val startOfToday = getStartOfDay(now)
val dailyPooCount = mutableMapOf<Long, Int>()
val dailyPeeCount = mutableMapOf<Long, Int>()
for (i in 0 until days) {
val dayStart = startOfToday - i * 24 * 60 * 60
dailyPooCount[dayStart] = 0
dailyPeeCount[dayStart] = 0
}
val pooEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_POO }
val peeEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_DIAPERCHANGE_PEE }
pooEvents.forEach { event ->
val dayStart = getStartOfDay(event.time)
dailyPooCount[dayStart] = (dailyPooCount[dayStart] ?: 0) + 1
}
peeEvents.forEach { event ->
val dayStart = getStartOfDay(event.time)
dailyPeeCount[dayStart] = (dailyPeeCount[dayStart] ?: 0) + 1
}
val totalDiapers = pooEvents.size + peeEvents.size
return DiaperStats(
dailyPooCount = dailyPooCount,
dailyPeeCount = dailyPeeCount,
avgDiapersPerDay = if (days > 0) totalDiapers.toFloat() / days else 0f,
avgPooPerDay = if (days > 0) pooEvents.size.toFloat() / days else 0f,
avgPeePerDay = if (days > 0) peeEvents.size.toFloat() / days else 0f,
lastPooTime = pooEvents.maxByOrNull { it.time }?.time
)
}
/**
* Get sleep statistics for the last N days
*/
fun getSleepStats(days: Int): SleepStats {
val relevantEvents = getEventsForDays(days)
val now = System.currentTimeMillis() / 1000
val startOfToday = getStartOfDay(now)
val dailyTotals = mutableMapOf<Long, Int>()
for (i in 0 until days) {
val dayStart = startOfToday - i * 24 * 60 * 60
dailyTotals[dayStart] = 0
}
val sleepEvents = relevantEvents.filter { it.type == LunaEvent.TYPE_SLEEP }
sleepEvents.forEach { event ->
val dayStart = getStartOfDay(event.time)
dailyTotals[dayStart] = (dailyTotals[dayStart] ?: 0) + event.quantity
}
val totalSleepMin = sleepEvents.sumOf { it.quantity }
val avgNapDuration = if (sleepEvents.isNotEmpty()) {
totalSleepMin.toFloat() / sleepEvents.size
} else 0f
val longestSleep = sleepEvents.maxOfOrNull { it.quantity } ?: 0
return SleepStats(
dailyTotals = dailyTotals,
avgSleepMinPerDay = if (days > 0) totalSleepMin.toFloat() / days else 0f,
avgNapsPerDay = if (days > 0) sleepEvents.size.toFloat() / days else 0f,
avgNapDurationMin = avgNapDuration,
longestSleepMin = longestSleep,
lastSleepTime = sleepEvents.maxByOrNull { it.time }?.time
)
}
/**
* Get weight history (all weight measurements)
*/
fun getWeightHistory(): List<WeightPoint> {
return events
.filter { it.type == LunaEvent.TYPE_WEIGHT && it.quantity > 0 }
.sortedBy { it.time }
.map { WeightPoint(it.time, it.quantity) }
}
/**
* Get temperature history
*/
fun getTemperatureHistory(): List<TemperaturePoint> {
return events
.filter { it.type == LunaEvent.TYPE_TEMPERATURE && it.quantity > 0 }
.sortedBy { it.time }
.map { TemperaturePoint(it.time, it.quantity) }
}
/**
* Calculate weight gain over the last N days
*/
fun getWeightGainForDays(days: Int): Int? {
val weights = getWeightHistory()
if (weights.size < 2) return null
val now = System.currentTimeMillis() / 1000
val startTime = now - days * 24 * 60 * 60
val recentWeight = weights.lastOrNull() ?: return null
val olderWeight = weights.filter { it.time <= startTime }.lastOrNull()
?: weights.firstOrNull()
?: return null
if (recentWeight.time == olderWeight.time) return null
return recentWeight.weightGrams - olderWeight.weightGrams
}
/**
* Get average daily values for a type of event over N days
*/
fun getAverageDailyCount(type: String, days: Int): Float {
val relevantEvents = getEventsForDays(days).filter { it.type == type }
return if (days > 0) relevantEvents.size.toFloat() / days else 0f
}
/**
* Get average daily quantity sum for a type of event over N days
*/
fun getAverageDailyQuantity(type: String, days: Int): Float {
val relevantEvents = getEventsForDays(days).filter { it.type == type }
val total = relevantEvents.sumOf { it.quantity }
return if (days > 0) total.toFloat() / days else 0f
}
}

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9c0,-0.46 -0.04,-0.92 -0.1,-1.36c-0.98,1.37 -2.58,2.26 -4.4,2.26c-2.98,0 -5.4,-2.42 -5.4,-5.4c0,-1.81 0.89,-3.42 2.26,-4.4C12.92,3.04 12.46,3 12,3L12,3z"/>
</vector>

View File

@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M16,6l2.29,2.29 -4.88,4.88 -4,-4L2,16.59 3.41,18l6,-6 4,4 6.3,-6.29L22,12V6z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M4,20h16v2H4z"/>
</vector>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="4dp"/>
<solid android:color="#33FFFFFF"/>
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="4dp"/>
<solid android:color="@color/accent"/>
</shape>
</clip>
</item>
</layer-list>

View File

@@ -16,6 +16,15 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView
android:id="@+id/button_statistics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_gravity="end"
android:src="@drawable/ic_statistics"
app:tint="@color/grey"/>
<ImageView <ImageView
android:id="@+id/button_settings" android:id="@+id/button_settings"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -170,6 +170,44 @@
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:text="@string/settings_no_breastfeeding_desc"/> android:text="@string/settings_no_breastfeeding_desc"/>
<!-- Data Backup Section -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:layout_marginTop="30dp"
android:text="@string/settings_backup_title"/>
<Button
android:id="@+id/settings_export"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@string/settings_export"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:text="@string/settings_export_desc"/>
<Button
android:id="@+id/settings_import"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@string/settings_import"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:text="@string/settings_import_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,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingHorizontal="15dp">
<ImageView
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:src="@drawable/ic_back"
app:tint="@color/grey"/>
<TextView
android:id="@+id/statistics_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/statistics_title"
android:textSize="22sp"
android:gravity="center"/>
<Spinner
android:id="@+id/period_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="80dp"/>
</LinearLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:tabMode="scrollable"
app:tabGravity="start"
app:tabTextColor="@color/grey"
app:tabSelectedTextColor="@color/accent"
app:tabIndicatorColor="@color/accent"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>

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

@@ -0,0 +1,274 @@
<?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="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/date_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="@color/accent"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="20dp"/>
<!-- Feeding Card -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_feeding_title"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/event_bottle_type"
android:textSize="24sp"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="10dp">
<TextView
android:id="@+id/bottle_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"/>
<ProgressBar
android:id="@+id/bottle_progress"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_marginTop="5dp"
style="@android:style/Widget.ProgressBar.Horizontal"
android:progressDrawable="@drawable/progress_bar_accent"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/breastfeeding_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/event_breastfeeding_both_type"
android:textSize="24sp"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="10dp">
<TextView
android:id="@+id/breastfeeding_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"/>
<ProgressBar
android:id="@+id/breastfeeding_progress"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_marginTop="5dp"
style="@android:style/Widget.ProgressBar.Horizontal"
android:progressDrawable="@drawable/progress_bar_accent"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- Sleep Card -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_sleep_title"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/event_sleep_type"
android:textSize="24sp"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="10dp">
<TextView
android:id="@+id/sleep_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"/>
<ProgressBar
android:id="@+id/sleep_progress"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_marginTop="5dp"
style="@android:style/Widget.ProgressBar.Horizontal"
android:progressDrawable="@drawable/progress_bar_accent"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- Diapers Card -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_diapers_title"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/event_diaperchange_poo_type"
android:textSize="24sp"/>
<TextView
android:id="@+id/poo_summary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:textSize="14sp"
android:gravity="center_vertical"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/event_diaperchange_pee_type"
android:textSize="24sp"/>
<TextView
android:id="@+id/pee_summary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="10dp"
android:textSize="14sp"
android:gravity="center_vertical"/>
</LinearLayout>
</LinearLayout>
<!-- Weight/Temperature Card -->
<LinearLayout
android:id="@+id/health_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp"
android:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_health_title"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<TextView
android:id="@+id/weight_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="14sp"/>
<TextView
android:id="@+id/temperature_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="14sp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,129 @@
<?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="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Daily Chart -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_diapers_per_day"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<LinearLayout
android:id="@+id/chart_container"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="bottom"/>
<LinearLayout
android:id="@+id/chart_labels"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp"
android:gravity="center">
<View
android:layout_width="16dp"
android:layout_height="16dp"
android:background="@color/accent"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="20dp"
android:text="@string/stats_poo"
android:textSize="12sp"/>
<View
android:layout_width="16dp"
android:layout_height="16dp"
android:background="#66FFE68F"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@string/stats_pee"
android:textSize="12sp"/>
</LinearLayout>
</LinearLayout>
<!-- Summary Stats -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_summary"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<TextView
android:id="@+id/avg_diapers"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="14sp"/>
<TextView
android:id="@+id/avg_poo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="14sp"/>
<TextView
android:id="@+id/avg_pee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="14sp"/>
<TextView
android:id="@+id/last_poo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:textSize="14sp"
android:textStyle="bold"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,175 @@
<?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="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Daily Chart -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_daily_intake"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<LinearLayout
android:id="@+id/chart_container"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="bottom"/>
<LinearLayout
android:id="@+id/chart_labels"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"/>
</LinearLayout>
<!-- Bottle Stats -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_bottle_title"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<TextView
android:id="@+id/bottle_avg_daily"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="14sp"/>
<TextView
android:id="@+id/feeding_interval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="14sp"/>
</LinearLayout>
<!-- Breastfeeding Stats -->
<LinearLayout
android:id="@+id/breastfeeding_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_breastfeeding_title"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<TextView
android:id="@+id/breastfeeding_avg_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="14sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@string/stats_side_distribution"
android:textSize="14sp"
android:textStyle="bold"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stats_left"
android:textSize="14sp"/>
<ProgressBar
android:id="@+id/left_progress"
android:layout_width="0dp"
android:layout_height="16dp"
android:layout_weight="1"
android:layout_marginHorizontal="10dp"
style="@android:style/Widget.ProgressBar.Horizontal"
android:progressDrawable="@drawable/progress_bar_accent"/>
<TextView
android:id="@+id/left_percent"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:textSize="14sp"
android:gravity="end"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stats_right"
android:textSize="14sp"/>
<ProgressBar
android:id="@+id/right_progress"
android:layout_width="0dp"
android:layout_height="16dp"
android:layout_weight="1"
android:layout_marginHorizontal="10dp"
style="@android:style/Widget.ProgressBar.Horizontal"
android:progressDrawable="@drawable/progress_bar_accent"/>
<TextView
android:id="@+id/right_percent"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:textSize="14sp"
android:gravity="end"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,100 @@
<?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="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Weight Chart -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_weight_curve"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<LinearLayout
android:id="@+id/chart_container"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="bottom"/>
<LinearLayout
android:id="@+id/chart_labels"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"/>
</LinearLayout>
<!-- Weight Summary -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_weight_summary"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<TextView
android:id="@+id/current_weight"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="20sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/weight_gain_week"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="14sp"/>
<TextView
android:id="@+id/weight_gain_month"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="14sp"/>
</LinearLayout>
<!-- No Data Message -->
<TextView
android:id="@+id/no_data_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="40dp"
android:text="@string/stats_no_weight_data"
android:textSize="16sp"
android:textColor="@color/grey"
android:visibility="gone"/>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,106 @@
<?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="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Daily Chart -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_sleep_per_day"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<LinearLayout
android:id="@+id/chart_container"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="bottom"/>
<LinearLayout
android:id="@+id/chart_labels"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"/>
</LinearLayout>
<!-- Sleep Summary -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/button_background"
android:padding="15dp"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/stats_sleep_analysis"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/accent"/>
<TextView
android:id="@+id/avg_sleep_per_day"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="14sp"/>
<TextView
android:id="@+id/avg_naps_per_day"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="14sp"/>
<TextView
android:id="@+id/avg_nap_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="14sp"/>
<TextView
android:id="@+id/longest_sleep"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="14sp"/>
</LinearLayout>
<!-- No Data Message -->
<TextView
android:id="@+id/no_data_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="40dp"
android:text="@string/stats_no_sleep_data"
android:textSize="16sp"
android:textColor="@color/grey"
android:visibility="gone"/>
</LinearLayout>
</ScrollView>

View File

@@ -10,10 +10,20 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView
android:id="@+id/button_sleep"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_sleep"/>
<TextView <TextView
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:layout_marginTop="10dp"
android:padding="10dp" android:padding="10dp"
android:background="@drawable/dropdown_list_item_background" android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText" style="@style/OverflowMenuText"

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/sleep_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,36 @@
<?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/sleep_emoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/event_sleep_type"
android:textSize="60sp"/>
<TextView
android:id="@+id/sleep_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/sleep_timer_hint"/>
</LinearLayout>

View File

@@ -94,4 +94,124 @@
<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>
<!-- Schlaf-Tracking -->
<string name="event_sleep_desc">Schlaf</string>
<string name="sleep_timer_title">Baby schläft</string>
<string name="sleep_timer_stop">Aufgewacht</string>
<string name="sleep_timer_hint">Tippen wenn Baby aufwacht</string>
<string name="sleep_timer_already_running">Es läuft bereits eine Schlafsitzung</string>
<string name="sleep_duration_title">Schlafdauer</string>
<string name="sleep_duration_description">Dauer in Minuten eingeben</string>
<string name="overflow_event_sleep">🌙 Schlaf</string>
<!-- Statistik -->
<string name="statistics_title">Statistik</string>
<string name="stats_tab_today">Heute</string>
<string name="stats_tab_feeding">Fütterung</string>
<string name="stats_tab_diapers">Windeln</string>
<string name="stats_tab_sleep">Schlaf</string>
<string name="stats_tab_growth">Wachstum</string>
<string name="stats_period_7days">7 Tage</string>
<string name="stats_period_14days">14 Tage</string>
<string name="stats_period_30days">30 Tage</string>
<string name="stats_feeding_title">Fütterung</string>
<string name="stats_sleep_title">Schlaf</string>
<string name="stats_diapers_title">Windeln</string>
<string name="stats_health_title">Gesundheit</string>
<string name="stats_today">Heute</string>
<string name="stats_times">%d mal</string>
<string name="stats_count_format">%d× heute</string>
<string name="stats_avg_format">Ø: %s</string>
<string name="stats_daily_intake">Tägliche Aufnahme</string>
<string name="stats_bottle_title">Fläschchen</string>
<string name="stats_breastfeeding_title">Stillen</string>
<string name="stats_avg_per_day">Ø: %.1f/Tag</string>
<string name="stats_avg_ml_per_day">Ø: %.0f ml/Tag</string>
<string name="stats_avg_min_per_day">Ø: %.0f min/Tag</string>
<string name="stats_feeding_interval">Fütterungsintervall: Ø %d min</string>
<string name="stats_avg_duration">Ø Dauer: %.1f min</string>
<string name="stats_side_distribution">Seitenverteilung</string>
<string name="stats_left">Links</string>
<string name="stats_right">Rechts</string>
<string name="stats_diapers_per_day">Windeln pro Tag</string>
<string name="stats_poo">Stuhl</string>
<string name="stats_pee">Urin</string>
<string name="stats_summary">Zusammenfassung</string>
<string name="stats_avg_diapers">Ø: %.1f Windeln/Tag</string>
<string name="stats_avg_poo">Stuhl: %.1f/Tag</string>
<string name="stats_avg_pee">Urin: %.1f/Tag</string>
<string name="stats_last_poo">Letzter Stuhl: %s</string>
<string name="stats_sleep_per_day">Schlaf pro Tag</string>
<string name="stats_sleep_analysis">Schlafanalyse</string>
<string name="stats_avg_sleep">Ø: %.1f Stunden/Tag</string>
<string name="stats_avg_naps">Ø: %.1f Schläfchen/Tag</string>
<string name="stats_avg_nap_duration">Ø Schläfchen: %.0f min</string>
<string name="stats_longest_sleep">Längstes: %d min</string>
<string name="stats_no_sleep_data">Noch keine Schlafdaten erfasst</string>
<string name="stats_hours_format">%.1f Std</string>
<string name="stats_weight_curve">Gewichtskurve</string>
<string name="stats_weight_summary">Gewicht</string>
<string name="stats_current_weight">Aktuell: %s</string>
<string name="stats_weight_gain_week">Letzte 7 Tage: %+d g</string>
<string name="stats_weight_gain_month">Letzte 30 Tage: %+d g</string>
<string name="stats_no_weight_data">Noch keine Gewichtsdaten erfasst</string>
<string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</string>
<!-- Export/Import -->
<string name="settings_backup_title">Datensicherung</string>
<string name="settings_export">Logbook exportieren</string>
<string name="settings_export_desc">Alle Ereignisse als JSON-Datei speichern</string>
<string name="settings_import">Logbook importieren</string>
<string name="settings_import_desc">Ereignisse aus JSON-Datei laden</string>
<string name="export_success">%d Ereignisse exportiert</string>
<string name="export_error">Export fehlgeschlagen: </string>
<string name="import_success">%d Ereignisse importiert</string>
<string name="import_error">Import fehlgeschlagen</string>
</resources> </resources>

View File

@@ -93,4 +93,93 @@
<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>
<!-- Suivi du sommeil -->
<string name="event_sleep_desc">Sommeil</string>
<string name="sleep_timer_title">Bébé dort</string>
<string name="sleep_timer_stop">Réveillé</string>
<string name="sleep_timer_hint">Appuyez quand bébé se réveille</string>
<string name="sleep_timer_already_running">Une session de sommeil est déjà en cours</string>
<string name="sleep_duration_title">Durée du sommeil</string>
<string name="sleep_duration_description">Entrez la durée en minutes</string>
<string name="overflow_event_sleep">🌙 Sommeil</string>
<!-- Statistiques -->
<string name="statistics_title">Statistiques</string>
<string name="stats_tab_today">Aujourd\'hui</string>
<string name="stats_tab_feeding">Alimentation</string>
<string name="stats_tab_diapers">Couches</string>
<string name="stats_tab_sleep">Sommeil</string>
<string name="stats_tab_growth">Croissance</string>
<string name="stats_period_7days">7 jours</string>
<string name="stats_period_14days">14 jours</string>
<string name="stats_period_30days">30 jours</string>
<string name="stats_feeding_title">Alimentation</string>
<string name="stats_sleep_title">Sommeil</string>
<string name="stats_diapers_title">Couches</string>
<string name="stats_health_title">Santé</string>
<string name="stats_today">Aujourd\'hui</string>
<string name="stats_times">%d fois</string>
<string name="stats_count_format">%d× aujourd\'hui</string>
<string name="stats_avg_format">Moy: %s</string>
<string name="stats_daily_intake">Apport quotidien</string>
<string name="stats_bottle_title">Biberon</string>
<string name="stats_breastfeeding_title">Allaitement</string>
<string name="stats_avg_per_day">Moy: %.1f/jour</string>
<string name="stats_avg_ml_per_day">Moy: %.0f ml/jour</string>
<string name="stats_avg_min_per_day">Moy: %.0f min/jour</string>
<string name="stats_feeding_interval">Intervalle: moy. %d min</string>
<string name="stats_avg_duration">Durée moy: %.1f min</string>
<string name="stats_side_distribution">Répartition des côtés</string>
<string name="stats_left">Gauche</string>
<string name="stats_right">Droite</string>
<string name="stats_diapers_per_day">Couches par jour</string>
<string name="stats_poo">Selles</string>
<string name="stats_pee">Urine</string>
<string name="stats_summary">Résumé</string>
<string name="stats_avg_diapers">Moy: %.1f couches/jour</string>
<string name="stats_avg_poo">Selles: %.1f/jour</string>
<string name="stats_avg_pee">Urine: %.1f/jour</string>
<string name="stats_last_poo">Dernières selles: %s</string>
<string name="stats_sleep_per_day">Sommeil par jour</string>
<string name="stats_sleep_analysis">Analyse du sommeil</string>
<string name="stats_avg_sleep">Moy: %.1f heures/jour</string>
<string name="stats_avg_naps">Moy: %.1f siestes/jour</string>
<string name="stats_avg_nap_duration">Sieste moy: %.0f min</string>
<string name="stats_longest_sleep">Plus long: %d min</string>
<string name="stats_no_sleep_data">Aucune donnée de sommeil enregistrée</string>
<string name="stats_hours_format">%.1f h</string>
<string name="stats_weight_curve">Courbe de poids</string>
<string name="stats_weight_summary">Poids</string>
<string name="stats_current_weight">Actuel: %s</string>
<string name="stats_weight_gain_week">7 derniers jours: %+d g</string>
<string name="stats_weight_gain_month">30 derniers jours: %+d g</string>
<string name="stats_no_weight_data">Aucune donnée de poids enregistrée</string>
<string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</string>
<!-- Export/Import -->
<string name="settings_backup_title">Sauvegarde des données</string>
<string name="settings_export">Exporter le journal</string>
<string name="settings_export_desc">Enregistrer tous les événements en fichier JSON</string>
<string name="settings_import">Importer un journal</string>
<string name="settings_import_desc">Charger les événements depuis un fichier JSON</string>
<string name="export_success">%d événements exportés</string>
<string name="export_error">Échec de l\'export: </string>
<string name="import_success">%d événements importés</string>
<string name="import_error">Échec de l\'import</string>
</resources> </resources>

View File

@@ -93,4 +93,93 @@
<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>
<!-- Tracciamento del sonno -->
<string name="event_sleep_desc">Sonno</string>
<string name="sleep_timer_title">Il bimbo dorme</string>
<string name="sleep_timer_stop">Svegliato</string>
<string name="sleep_timer_hint">Premi quando il bimbo si sveglia</string>
<string name="sleep_timer_already_running">Una sessione di sonno è già in corso</string>
<string name="sleep_duration_title">Durata del sonno</string>
<string name="sleep_duration_description">Inserisci la durata in minuti</string>
<string name="overflow_event_sleep">🌙 Sonno</string>
<!-- Statistiche -->
<string name="statistics_title">Statistiche</string>
<string name="stats_tab_today">Oggi</string>
<string name="stats_tab_feeding">Alimentazione</string>
<string name="stats_tab_diapers">Pannolini</string>
<string name="stats_tab_sleep">Sonno</string>
<string name="stats_tab_growth">Crescita</string>
<string name="stats_period_7days">7 giorni</string>
<string name="stats_period_14days">14 giorni</string>
<string name="stats_period_30days">30 giorni</string>
<string name="stats_feeding_title">Alimentazione</string>
<string name="stats_sleep_title">Sonno</string>
<string name="stats_diapers_title">Pannolini</string>
<string name="stats_health_title">Salute</string>
<string name="stats_today">Oggi</string>
<string name="stats_times">%d volte</string>
<string name="stats_count_format">%d× oggi</string>
<string name="stats_avg_format">Media: %s</string>
<string name="stats_daily_intake">Assunzione giornaliera</string>
<string name="stats_bottle_title">Biberon</string>
<string name="stats_breastfeeding_title">Allattamento</string>
<string name="stats_avg_per_day">Media: %.1f/giorno</string>
<string name="stats_avg_ml_per_day">Media: %.0f ml/giorno</string>
<string name="stats_avg_min_per_day">Media: %.0f min/giorno</string>
<string name="stats_feeding_interval">Intervallo: media %d min</string>
<string name="stats_avg_duration">Durata media: %.1f min</string>
<string name="stats_side_distribution">Distribuzione lati</string>
<string name="stats_left">Sinistra</string>
<string name="stats_right">Destra</string>
<string name="stats_diapers_per_day">Pannolini al giorno</string>
<string name="stats_poo">Cacca</string>
<string name="stats_pee">Pipì</string>
<string name="stats_summary">Riepilogo</string>
<string name="stats_avg_diapers">Media: %.1f pannolini/giorno</string>
<string name="stats_avg_poo">Cacca: %.1f/giorno</string>
<string name="stats_avg_pee">Pipì: %.1f/giorno</string>
<string name="stats_last_poo">Ultima cacca: %s</string>
<string name="stats_sleep_per_day">Sonno al giorno</string>
<string name="stats_sleep_analysis">Analisi del sonno</string>
<string name="stats_avg_sleep">Media: %.1f ore/giorno</string>
<string name="stats_avg_naps">Media: %.1f sonnellini/giorno</string>
<string name="stats_avg_nap_duration">Sonnellino medio: %.0f min</string>
<string name="stats_longest_sleep">Più lungo: %d min</string>
<string name="stats_no_sleep_data">Nessun dato sul sonno registrato</string>
<string name="stats_hours_format">%.1f h</string>
<string name="stats_weight_curve">Curva di peso</string>
<string name="stats_weight_summary">Peso</string>
<string name="stats_current_weight">Attuale: %s</string>
<string name="stats_weight_gain_week">Ultimi 7 giorni: %+d g</string>
<string name="stats_weight_gain_month">Ultimi 30 giorni: %+d g</string>
<string name="stats_no_weight_data">Nessun dato sul peso registrato</string>
<string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</string>
<!-- Export/Import -->
<string name="settings_backup_title">Backup dati</string>
<string name="settings_export">Esporta diario</string>
<string name="settings_export_desc">Salva tutti gli eventi come file JSON</string>
<string name="settings_import">Importa diario</string>
<string name="settings_import_desc">Carica eventi da file JSON</string>
<string name="export_success">%d eventi esportati</string>
<string name="export_error">Esportazione fallita: </string>
<string name="import_success">%d eventi importati</string>
<string name="import_error">Importazione fallita</string>
</resources> </resources>

View File

@@ -30,6 +30,7 @@
<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_puke_type" translatable="false">🤮</string>
<string name="event_bath_type" translatable="false">🛁</string> <string name="event_bath_type" translatable="false">🛁</string>
<string name="event_sleep_type" translatable="false">🌙</string>
<string name="event_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>
@@ -47,6 +48,7 @@
<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_puke_desc">Puke</string>
<string name="event_bath_desc">Bath</string> <string name="event_bath_desc">Bath</string>
<string name="event_sleep_desc">Sleep</string>
<string name="event_unknown_desc"></string> <string name="event_unknown_desc"></string>
<string name="overflow_event_scale">⚖️ Weight</string> <string name="overflow_event_scale">⚖️ Weight</string>
@@ -148,4 +150,93 @@
<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>
<!-- Sleep tracking -->
<string name="sleep_timer_title">Baby is sleeping</string>
<string name="sleep_timer_stop">Woke up</string>
<string name="sleep_timer_hint">Tap when baby wakes up</string>
<string name="sleep_timer_already_running">A sleep session is already in progress</string>
<string name="sleep_duration_title">Sleep duration</string>
<string name="sleep_duration_description">Enter the duration in minutes</string>
<string name="overflow_event_sleep">🌙 Sleep</string>
<!-- Statistics -->
<string name="statistics_title">Statistics</string>
<string name="stats_tab_today">Today</string>
<string name="stats_tab_feeding">Feeding</string>
<string name="stats_tab_diapers">Diapers</string>
<string name="stats_tab_sleep">Sleep</string>
<string name="stats_tab_growth">Growth</string>
<string name="stats_period_7days">7 days</string>
<string name="stats_period_14days">14 days</string>
<string name="stats_period_30days">30 days</string>
<string name="stats_feeding_title">Feeding</string>
<string name="stats_sleep_title">Sleep</string>
<string name="stats_diapers_title">Diapers</string>
<string name="stats_health_title">Health</string>
<string name="stats_today">Today</string>
<string name="stats_times">%d times</string>
<string name="stats_count_format">%d× today</string>
<string name="stats_avg_format">Avg: %s</string>
<string name="stats_daily_intake">Daily intake</string>
<string name="stats_bottle_title">Bottle</string>
<string name="stats_breastfeeding_title">Breastfeeding</string>
<string name="stats_avg_per_day">Avg: %.1f/day</string>
<string name="stats_avg_ml_per_day">Avg: %.0f ml/day</string>
<string name="stats_avg_min_per_day">Avg: %.0f min/day</string>
<string name="stats_feeding_interval">Feeding interval: avg. %d min</string>
<string name="stats_avg_duration">Avg duration: %.1f min</string>
<string name="stats_side_distribution">Side distribution</string>
<string name="stats_left">Left</string>
<string name="stats_right">Right</string>
<string name="stats_diapers_per_day">Diapers per day</string>
<string name="stats_poo">Poo</string>
<string name="stats_pee">Pee</string>
<string name="stats_summary">Summary</string>
<string name="stats_avg_diapers">Avg: %.1f diapers/day</string>
<string name="stats_avg_poo">Poo: %.1f/day</string>
<string name="stats_avg_pee">Pee: %.1f/day</string>
<string name="stats_last_poo">Last poo: %s</string>
<string name="stats_sleep_per_day">Sleep per day</string>
<string name="stats_sleep_analysis">Sleep analysis</string>
<string name="stats_avg_sleep">Avg: %.1f hours/day</string>
<string name="stats_avg_naps">Avg: %.1f naps/day</string>
<string name="stats_avg_nap_duration">Avg nap: %.0f min</string>
<string name="stats_longest_sleep">Longest: %d min</string>
<string name="stats_no_sleep_data">No sleep data recorded yet</string>
<string name="stats_hours_format">%.1f h</string>
<string name="stats_weight_curve">Weight curve</string>
<string name="stats_weight_summary">Weight</string>
<string name="stats_current_weight">Current: %s</string>
<string name="stats_weight_gain_week">Last 7 days: %+d g</string>
<string name="stats_weight_gain_month">Last 30 days: %+d g</string>
<string name="stats_no_weight_data">No weight data recorded yet</string>
<string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</string>
<!-- Export/Import -->
<string name="settings_backup_title">Data Backup</string>
<string name="settings_export">Export Logbook</string>
<string name="settings_export_desc">Save all events as JSON file</string>
<string name="settings_import">Import Logbook</string>
<string name="settings_import_desc">Load events from JSON file</string>
<string name="export_success">Exported %d events</string>
<string name="export_error">Export failed: </string>
<string name="import_success">Imported %d events</string>
<string name="import_error">Import failed</string>
</resources> </resources>