diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 97f621b..b335ff9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -30,6 +30,10 @@
android:name=".SettingsActivity"
android:label="@string/settings_title"
android:theme="@style/Theme.LunaTracker"/>
+
\ No newline at end of file
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt
index e80dddb..c3e0865 100644
--- a/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt
+++ b/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt
@@ -76,6 +76,12 @@ class MainActivity : AppCompatActivity() {
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?) {
super.onCreate(savedInstanceState)
@@ -134,6 +140,9 @@ class MainActivity : AppCompatActivity() {
findViewById(R.id.button_settings).setOnClickListener {
showSettings()
}
+ findViewById(R.id.button_statistics).setOnClickListener {
+ showStatistics()
+ }
findViewById(R.id.button_no_connection_retry).setOnClickListener {
// This may happen at start, when logbook is still null: better ask the logbook list
loadLogbookList()
@@ -164,6 +173,12 @@ class MainActivity : AppCompatActivity() {
startActivity(i)
}
+ fun showStatistics() {
+ val i = Intent(this, StatisticsActivity::class.java)
+ i.putExtra(StatisticsActivity.EXTRA_LOGBOOK_NAME, logbook?.name ?: "")
+ startActivity(i)
+ }
+
fun showLogbook() {
// Show logbook
if (logbook == null)
@@ -204,6 +219,9 @@ class MainActivity : AppCompatActivity() {
// Check for ongoing breastfeeding timer
restoreBreastfeedingTimerIfNeeded()
+ // Check for ongoing sleep timer
+ restoreSleepTimerIfNeeded()
+
if (logbook != null) {
// Already running: reload data for currently selected logbook
loadLogbook(logbook!!.name)
@@ -220,6 +238,10 @@ class MainActivity : AppCompatActivity() {
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()
}
@@ -452,6 +474,117 @@ class MainActivity : AppCompatActivity() {
}
}
+ // 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(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(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() {
val d = AlertDialog.Builder(this)
d.setTitle(R.string.trim_logbook_dialog_title)
@@ -539,24 +672,32 @@ class MainActivity : AppCompatActivity() {
}, startYear, startMonth, startDay).show()
}
- // Make quantity editable for breastfeeding events
+ // Make quantity editable for breastfeeding and sleep events
val quantityTextView = dialogView.findViewById(R.id.dialog_event_detail_type_quantity)
- if (event.type in listOf(
+ val isBreastfeeding = event.type in listOf(
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
- ) && event.quantity > 0) {
+ )
+ 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 = layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
- val picker = pickerView.findViewById(R.id.breastfeeding_duration_picker)
+ 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(
+ if (isSleep) R.id.sleep_duration_picker else R.id.breastfeeding_duration_picker
+ )
picker.minValue = 1
- picker.maxValue = 60
- picker.value = if (event.quantity > 0) event.quantity else 15
+ picker.maxValue = if (isSleep) 180 else 60
+ picker.value = if (event.quantity > 0) Math.min(event.quantity, picker.maxValue) else if (isSleep) 30 else 15
- pickerDialog.setTitle(R.string.breastfeeding_duration_title)
+ pickerDialog.setTitle(if (isSleep) R.string.sleep_duration_title else R.string.breastfeeding_duration_title)
pickerDialog.setView(pickerView)
pickerDialog.setPositiveButton(android.R.string.ok) { _, _ ->
event.quantity = picker.value
@@ -971,6 +1112,15 @@ class MainActivity : AppCompatActivity() {
isOutsideTouchable = true
val inflater = LayoutInflater.from(anchor.context)
contentView = inflater.inflate(R.layout.more_events_popup, null)
+ contentView.findViewById(R.id.button_sleep).setOnClickListener {
+ startSleepTimer()
+ dismiss()
+ }
+ contentView.findViewById(R.id.button_sleep).setOnLongClickListener {
+ askSleepDuration()
+ dismiss()
+ true
+ }
contentView.findViewById(R.id.button_medicine).setOnClickListener {
askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE))
dismiss()
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/SettingsActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/SettingsActivity.kt
index 8006986..80707e6 100644
--- a/app/src/main/java/it/danieleverducci/lunatracker/SettingsActivity.kt
+++ b/app/src/main/java/it/danieleverducci/lunatracker/SettingsActivity.kt
@@ -1,20 +1,26 @@
package it.danieleverducci.lunatracker
+import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.RadioButton
import android.widget.TextView
import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.switchmaterial.SwitchMaterial
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.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException
+import org.json.JSONArray
+import org.json.JSONObject
open class SettingsActivity : AppCompatActivity() {
protected lateinit var settingsRepository: LocalSettingsRepository
@@ -27,6 +33,15 @@ open class SettingsActivity : AppCompatActivity() {
protected lateinit var switchNoBreastfeeding: SwitchMaterial
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?) {
super.onCreate(savedInstanceState)
@@ -46,6 +61,12 @@ open class SettingsActivity : AppCompatActivity() {
findViewById(R.id.settings_cancel).setOnClickListener({
finish()
})
+ findViewById(R.id.settings_export).setOnClickListener({
+ startExport()
+ })
+ findViewById(R.id.settings_import).setOnClickListener({
+ startImport()
+ })
settingsRepository = LocalSettingsRepository(this)
loadSettings()
@@ -198,4 +219,136 @@ open class SettingsActivity : AppCompatActivity() {
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()
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/StatisticsActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/StatisticsActivity.kt
new file mode 100644
index 0000000..a15752d
--- /dev/null
+++ b/app/src/main/java/it/danieleverducci/lunatracker/StatisticsActivity.kt
@@ -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 = 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(R.id.button_back).setOnClickListener {
+ finish()
+ }
+
+ // Title with logbook name
+ logbookName = intent.getStringExtra(EXTRA_LOGBOOK_NAME) ?: ""
+ if (logbookName.isNotEmpty()) {
+ findViewById(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 = 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()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/entities/LunaEvent.kt b/app/src/main/java/it/danieleverducci/lunatracker/entities/LunaEvent.kt
index e46ab25..1de111b 100644
--- a/app/src/main/java/it/danieleverducci/lunatracker/entities/LunaEvent.kt
+++ b/app/src/main/java/it/danieleverducci/lunatracker/entities/LunaEvent.kt
@@ -30,6 +30,7 @@ class LunaEvent: Comparable {
const val TYPE_FOOD = "FOOD"
const val TYPE_PUKE = "PUKE"
const val TYPE_BATH = "BATH"
+ const val TYPE_SLEEP = "SLEEP"
}
private val jo: JSONObject
@@ -100,6 +101,7 @@ class LunaEvent: Comparable {
TYPE_FOOD -> R.string.event_food_type
TYPE_PUKE -> R.string.event_puke_type
TYPE_BATH -> R.string.event_bath_type
+ TYPE_SLEEP -> R.string.event_sleep_type
else -> R.string.event_unknown_type
}
)
@@ -123,6 +125,7 @@ class LunaEvent: Comparable {
TYPE_FOOD -> R.string.event_food_desc
TYPE_PUKE -> R.string.event_puke_desc
TYPE_BATH -> R.string.event_bath_desc
+ TYPE_SLEEP -> R.string.event_sleep_desc
else -> R.string.event_unknown_desc
}
)
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/fragments/DailySummaryFragment.kt b/app/src/main/java/it/danieleverducci/lunatracker/fragments/DailySummaryFragment.kt
new file mode 100644
index 0000000..422cd01
--- /dev/null
+++ b/app/src/main/java/it/danieleverducci/lunatracker/fragments/DailySummaryFragment.kt
@@ -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(R.id.date_header).text = dateFormat.format(Date())
+
+ // Bottle summary
+ val bottleSummary = view.findViewById(R.id.bottle_summary)
+ val bottleProgress = view.findViewById(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(R.id.breastfeeding_container)
+ val breastfeedingSummary = view.findViewById(R.id.breastfeeding_summary)
+ val breastfeedingProgress = view.findViewById(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(R.id.sleep_summary)
+ val sleepProgress = view.findViewById(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(R.id.poo_summary)
+ val peeSummary = view.findViewById(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(R.id.health_card)
+ val weightSummary = view.findViewById(R.id.weight_summary)
+ val tempSummary = view.findViewById(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
+ }
+ }
+}
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/fragments/DiaperStatsFragment.kt b/app/src/main/java/it/danieleverducci/lunatracker/fragments/DiaperStatsFragment.kt
new file mode 100644
index 0000000..3c18cd4
--- /dev/null
+++ b/app/src/main/java/it/danieleverducci/lunatracker/fragments/DiaperStatsFragment.kt
@@ -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(R.id.chart_container)
+ val chartLabels = view.findViewById(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(R.id.avg_diapers).text =
+ getString(R.string.stats_avg_diapers, stats.avgDiapersPerDay)
+ view.findViewById(R.id.avg_poo).text =
+ getString(R.string.stats_avg_poo, stats.avgPooPerDay)
+ view.findViewById(R.id.avg_pee).text =
+ getString(R.string.stats_avg_pee, stats.avgPeePerDay)
+
+ // Last poo
+ val lastPoo = view.findViewById(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
+ }
+ }
+}
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/fragments/FeedingStatsFragment.kt b/app/src/main/java/it/danieleverducci/lunatracker/fragments/FeedingStatsFragment.kt
new file mode 100644
index 0000000..e53735e
--- /dev/null
+++ b/app/src/main/java/it/danieleverducci/lunatracker/fragments/FeedingStatsFragment.kt
@@ -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(R.id.chart_container)
+ val chartLabels = view.findViewById(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(R.id.bottle_avg_daily)
+ bottleAvg.text = getString(R.string.stats_avg_ml_per_day, stats.avgBottleMlPerDay)
+
+ val feedingInterval = view.findViewById(R.id.feeding_interval)
+ feedingInterval.text = getString(R.string.stats_feeding_interval, stats.avgFeedingIntervalMinutes.toInt())
+
+ // Breastfeeding stats
+ val breastfeedingCard = view.findViewById(R.id.breastfeeding_card)
+ val totalBreastfeeding = stats.leftBreastCount + stats.rightBreastCount + stats.bothBreastCount
+
+ if (totalBreastfeeding > 0) {
+ breastfeedingCard.visibility = View.VISIBLE
+
+ val avgDuration = view.findViewById(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(R.id.left_progress)
+ val rightProgress = view.findViewById(R.id.right_progress)
+ val leftPercentText = view.findViewById(R.id.left_percent)
+ val rightPercentText = view.findViewById(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
+ }
+ }
+}
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/fragments/GrowthStatsFragment.kt b/app/src/main/java/it/danieleverducci/lunatracker/fragments/GrowthStatsFragment.kt
new file mode 100644
index 0000000..6589b01
--- /dev/null
+++ b/app/src/main/java/it/danieleverducci/lunatracker/fragments/GrowthStatsFragment.kt
@@ -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(R.id.no_data_message)
+
+ if (weightHistory.isEmpty()) {
+ noDataMessage.visibility = View.VISIBLE
+ view.findViewById(R.id.chart_container).visibility = View.GONE
+ view.findViewById(R.id.chart_labels).visibility = View.GONE
+ view.findViewById(R.id.current_weight).visibility = View.GONE
+ view.findViewById(R.id.weight_gain_week).visibility = View.GONE
+ view.findViewById(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(R.id.chart_container)
+ val chartLabels = view.findViewById(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(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(R.id.weight_gain_week)
+ val monthView = view.findViewById(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
+ }
+ }
+}
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/fragments/SleepStatsFragment.kt b/app/src/main/java/it/danieleverducci/lunatracker/fragments/SleepStatsFragment.kt
new file mode 100644
index 0000000..0de08da
--- /dev/null
+++ b/app/src/main/java/it/danieleverducci/lunatracker/fragments/SleepStatsFragment.kt
@@ -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(R.id.no_data_message)
+
+ if (!hasSleepData) {
+ noDataMessage.visibility = View.VISIBLE
+ view.findViewById(R.id.chart_container).visibility = View.GONE
+ view.findViewById(R.id.chart_labels).visibility = View.GONE
+ return
+ }
+
+ noDataMessage.visibility = View.GONE
+
+ // Draw bar chart (showing hours per day)
+ val chartContainer = view.findViewById(R.id.chart_container)
+ val chartLabels = view.findViewById(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(R.id.avg_sleep_per_day).text =
+ getString(R.string.stats_avg_sleep, avgSleepHours)
+ view.findViewById(R.id.avg_naps_per_day).text =
+ getString(R.string.stats_avg_naps, stats.avgNapsPerDay)
+ view.findViewById(R.id.avg_nap_duration).text =
+ getString(R.string.stats_avg_nap_duration, stats.avgNapDurationMin)
+ view.findViewById(R.id.longest_sleep).text =
+ getString(R.string.stats_longest_sleep, stats.longestSleepMin)
+ }
+}
diff --git a/app/src/main/java/it/danieleverducci/lunatracker/repository/LocalSettingsRepository.kt b/app/src/main/java/it/danieleverducci/lunatracker/repository/LocalSettingsRepository.kt
index 196b64e..cfdb47e 100644
--- a/app/src/main/java/it/danieleverducci/lunatracker/repository/LocalSettingsRepository.kt
+++ b/app/src/main/java/it/danieleverducci/lunatracker/repository/LocalSettingsRepository.kt
@@ -17,6 +17,7 @@ class LocalSettingsRepository(val context: Context) {
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}
val sharedPreferences: SharedPreferences
@@ -107,4 +108,20 @@ class LocalSettingsRepository(val context: Context) {
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)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/utils/NumericUtils.kt b/app/src/main/java/utils/NumericUtils.kt
index 6e7e5cb..9749d45 100644
--- a/app/src/main/java/utils/NumericUtils.kt
+++ b/app/src/main/java/utils/NumericUtils.kt
@@ -81,7 +81,8 @@ class NumericUtils (val context: Context) {
LunaEvent.TYPE_TEMPERATURE -> measurement_unit_temperature_base
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE,
- LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE ->
+ LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE,
+ LunaEvent.TYPE_SLEEP ->
context.getString(R.string.measurement_unit_time_minutes)
else -> ""
}
diff --git a/app/src/main/java/utils/StatisticsCalculator.kt b/app/src/main/java/utils/StatisticsCalculator.kt
new file mode 100644
index 0000000..aed0964
--- /dev/null
+++ b/app/src/main/java/utils/StatisticsCalculator.kt
@@ -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,
+ val dailyBreastfeedingTotals: Map,
+ 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,
+ val dailyPeeCount: Map,
+ val avgDiapersPerDay: Float,
+ val avgPooPerDay: Float,
+ val avgPeePerDay: Float,
+ val lastPooTime: Long?
+)
+
+data class SleepStats(
+ val dailyTotals: Map,
+ 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) {
+
+ 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 {
+ return events.filter { it.time >= startUnix && it.time < endUnix }
+ }
+
+ private fun getEventsForDays(days: Int): List {
+ 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()
+ val dailyBreastfeedingTotals = mutableMapOf()
+
+ 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()
+ val dailyPeeCount = mutableMapOf()
+
+ 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()
+
+ 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 {
+ 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 {
+ 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
+ }
+}
diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml
new file mode 100644
index 0000000..07ab466
--- /dev/null
+++ b/app/src/main/res/drawable/ic_back.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_sleep.xml b/app/src/main/res/drawable/ic_sleep.xml
new file mode 100644
index 0000000..81ba46a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sleep.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_statistics.xml b/app/src/main/res/drawable/ic_statistics.xml
new file mode 100644
index 0000000..c73f4bf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_statistics.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/progress_bar_accent.xml b/app/src/main/res/drawable/progress_bar_accent.xml
new file mode 100644
index 0000000..056e971
--- /dev/null
+++ b/app/src/main/res/drawable/progress_bar_accent.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index a04a510..ff01708 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -16,6 +16,15 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_daily_summary.xml b/app/src/main/res/layout/fragment_daily_summary.xml
new file mode 100644
index 0000000..da9e50c
--- /dev/null
+++ b/app/src/main/res/layout/fragment_daily_summary.xml
@@ -0,0 +1,274 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_diaper_stats.xml b/app/src/main/res/layout/fragment_diaper_stats.xml
new file mode 100644
index 0000000..d93cdb5
--- /dev/null
+++ b/app/src/main/res/layout/fragment_diaper_stats.xml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_feeding_stats.xml b/app/src/main/res/layout/fragment_feeding_stats.xml
new file mode 100644
index 0000000..d608e75
--- /dev/null
+++ b/app/src/main/res/layout/fragment_feeding_stats.xml
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_growth_stats.xml b/app/src/main/res/layout/fragment_growth_stats.xml
new file mode 100644
index 0000000..7e3407a
--- /dev/null
+++ b/app/src/main/res/layout/fragment_growth_stats.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_sleep_stats.xml b/app/src/main/res/layout/fragment_sleep_stats.xml
new file mode 100644
index 0000000..3587edc
--- /dev/null
+++ b/app/src/main/res/layout/fragment_sleep_stats.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/more_events_popup.xml b/app/src/main/res/layout/more_events_popup.xml
index 34ee31b..377d8e8 100644
--- a/app/src/main/res/layout/more_events_popup.xml
+++ b/app/src/main/res/layout/more_events_popup.xml
@@ -10,10 +10,20 @@
android:layout_height="wrap_content"
android:orientation="vertical">
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/sleep_timer_dialog.xml b/app/src/main/res/layout/sleep_timer_dialog.xml
new file mode 100644
index 0000000..93e7946
--- /dev/null
+++ b/app/src/main/res/layout/sleep_timer_dialog.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 01adda6..3027385 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -132,4 +132,86 @@
Notizen
von %s
+
+ Schlaf
+ Baby schläft
+ Aufgewacht
+ Tippen wenn Baby aufwacht
+ Es läuft bereits eine Schlafsitzung
+ Schlafdauer
+ Dauer in Minuten eingeben
+ 🌙 Schlaf
+
+
+ Statistik
+ Heute
+ Fütterung
+ Windeln
+ Schlaf
+ Wachstum
+
+ 7 Tage
+ 14 Tage
+ 30 Tage
+
+ Fütterung
+ Schlaf
+ Windeln
+ Gesundheit
+
+ Heute
+ %d mal
+ %d× heute
+ Ø: %s
+
+ Tägliche Aufnahme
+ Fläschchen
+ Stillen
+ Ø: %.1f/Tag
+ Ø: %.0f ml/Tag
+ Ø: %.0f min/Tag
+ Fütterungsintervall: Ø %d min
+ Ø Dauer: %.1f min
+ Seitenverteilung
+ Links
+ Rechts
+
+ Windeln pro Tag
+ Stuhl
+ Urin
+ Zusammenfassung
+ Ø: %.1f Windeln/Tag
+ Stuhl: %.1f/Tag
+ Urin: %.1f/Tag
+ Letzter Stuhl: %s
+
+ Schlaf pro Tag
+ Schlafanalyse
+ Ø: %.1f Stunden/Tag
+ Ø: %.1f Schläfchen/Tag
+ Ø Schläfchen: %.0f min
+ Längstes: %d min
+ Noch keine Schlafdaten erfasst
+ %.1f Std
+
+ Gewichtskurve
+ Gewicht
+ Aktuell: %s
+ Letzte 7 Tage: %+d g
+ Letzte 30 Tage: %+d g
+ Noch keine Gewichtsdaten erfasst
+ %.2f kg
+ %.1f °C
+
+
+ Datensicherung
+ Logbook exportieren
+ Alle Ereignisse als JSON-Datei speichern
+ Logbook importieren
+ Ereignisse aus JSON-Datei laden
+ %d Ereignisse exportiert
+ Export fehlgeschlagen:
+ %d Ereignisse importiert
+ Import fehlgeschlagen
+
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index b603e52..fb3bd9f 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -100,4 +100,86 @@
Durée d\'allaitement
Entrez la durée en minutes
+
+ Sommeil
+ Bébé dort
+ Réveillé
+ Appuyez quand bébé se réveille
+ Une session de sommeil est déjà en cours
+ Durée du sommeil
+ Entrez la durée en minutes
+ 🌙 Sommeil
+
+
+ Statistiques
+ Aujourd\'hui
+ Alimentation
+ Couches
+ Sommeil
+ Croissance
+
+ 7 jours
+ 14 jours
+ 30 jours
+
+ Alimentation
+ Sommeil
+ Couches
+ Santé
+
+ Aujourd\'hui
+ %d fois
+ %d× aujourd\'hui
+ Moy: %s
+
+ Apport quotidien
+ Biberon
+ Allaitement
+ Moy: %.1f/jour
+ Moy: %.0f ml/jour
+ Moy: %.0f min/jour
+ Intervalle: moy. %d min
+ Durée moy: %.1f min
+ Répartition des côtés
+ Gauche
+ Droite
+
+ Couches par jour
+ Selles
+ Urine
+ Résumé
+ Moy: %.1f couches/jour
+ Selles: %.1f/jour
+ Urine: %.1f/jour
+ Dernières selles: %s
+
+ Sommeil par jour
+ Analyse du sommeil
+ Moy: %.1f heures/jour
+ Moy: %.1f siestes/jour
+ Sieste moy: %.0f min
+ Plus long: %d min
+ Aucune donnée de sommeil enregistrée
+ %.1f h
+
+ Courbe de poids
+ Poids
+ Actuel: %s
+ 7 derniers jours: %+d g
+ 30 derniers jours: %+d g
+ Aucune donnée de poids enregistrée
+ %.2f kg
+ %.1f °C
+
+
+ Sauvegarde des données
+ Exporter le journal
+ Enregistrer tous les événements en fichier JSON
+ Importer un journal
+ Charger les événements depuis un fichier JSON
+ %d événements exportés
+ Échec de l\'export:
+ %d événements importés
+ Échec de l\'import
+
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index d94bccc..017d19b 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -100,4 +100,86 @@
Durata allattamento
Inserisci la durata in minuti
+
+ Sonno
+ Il bimbo dorme
+ Svegliato
+ Premi quando il bimbo si sveglia
+ Una sessione di sonno è già in corso
+ Durata del sonno
+ Inserisci la durata in minuti
+ 🌙 Sonno
+
+
+ Statistiche
+ Oggi
+ Alimentazione
+ Pannolini
+ Sonno
+ Crescita
+
+ 7 giorni
+ 14 giorni
+ 30 giorni
+
+ Alimentazione
+ Sonno
+ Pannolini
+ Salute
+
+ Oggi
+ %d volte
+ %d× oggi
+ Media: %s
+
+ Assunzione giornaliera
+ Biberon
+ Allattamento
+ Media: %.1f/giorno
+ Media: %.0f ml/giorno
+ Media: %.0f min/giorno
+ Intervallo: media %d min
+ Durata media: %.1f min
+ Distribuzione lati
+ Sinistra
+ Destra
+
+ Pannolini al giorno
+ Cacca
+ Pipì
+ Riepilogo
+ Media: %.1f pannolini/giorno
+ Cacca: %.1f/giorno
+ Pipì: %.1f/giorno
+ Ultima cacca: %s
+
+ Sonno al giorno
+ Analisi del sonno
+ Media: %.1f ore/giorno
+ Media: %.1f sonnellini/giorno
+ Sonnellino medio: %.0f min
+ Più lungo: %d min
+ Nessun dato sul sonno registrato
+ %.1f h
+
+ Curva di peso
+ Peso
+ Attuale: %s
+ Ultimi 7 giorni: %+d g
+ Ultimi 30 giorni: %+d g
+ Nessun dato sul peso registrato
+ %.2f kg
+ %.1f °C
+
+
+ Backup dati
+ Esporta diario
+ Salva tutti gli eventi come file JSON
+ Importa diario
+ Carica eventi da file JSON
+ %d eventi esportati
+ Esportazione fallita:
+ %d eventi importati
+ Importazione fallita
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index acb0826..c1e2327 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -30,6 +30,7 @@
💨
🤮
🛁
+ 🌙
\?
Baby bottle
@@ -47,6 +48,7 @@
Gaseous colic
Puke
Bath
+ Sleep
⚖️ Weight
@@ -156,4 +158,85 @@
Enter the duration in minutes
min
+
+ Baby is sleeping
+ Woke up
+ Tap when baby wakes up
+ A sleep session is already in progress
+ Sleep duration
+ Enter the duration in minutes
+ 🌙 Sleep
+
+
+ Statistics
+ Today
+ Feeding
+ Diapers
+ Sleep
+ Growth
+
+ 7 days
+ 14 days
+ 30 days
+
+ Feeding
+ Sleep
+ Diapers
+ Health
+
+ Today
+ %d times
+ %d× today
+ Avg: %s
+
+ Daily intake
+ Bottle
+ Breastfeeding
+ Avg: %.1f/day
+ Avg: %.0f ml/day
+ Avg: %.0f min/day
+ Feeding interval: avg. %d min
+ Avg duration: %.1f min
+ Side distribution
+ Left
+ Right
+
+ Diapers per day
+ Poo
+ Pee
+ Summary
+ Avg: %.1f diapers/day
+ Poo: %.1f/day
+ Pee: %.1f/day
+ Last poo: %s
+
+ Sleep per day
+ Sleep analysis
+ Avg: %.1f hours/day
+ Avg: %.1f naps/day
+ Avg nap: %.0f min
+ Longest: %d min
+ No sleep data recorded yet
+ %.1f h
+
+ Weight curve
+ Weight
+ Current: %s
+ Last 7 days: %+d g
+ Last 30 days: %+d g
+ No weight data recorded yet
+ %.2f kg
+ %.1f °C
+
+
+ Data Backup
+ Export Logbook
+ Save all events as JSON file
+ Import Logbook
+ Load events from JSON file
+ Exported %d events
+ Export failed:
+ Imported %d events
+ Import failed
+
\ No newline at end of file