Compare commits
3 Commits
master
...
b6110c2cbb
| Author | SHA1 | Date | |
|---|---|---|---|
| b6110c2cbb | |||
| dccc89a8e2 | |||
| 587fc5d3e3 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -107,3 +107,5 @@ app/release/output-metadata.json
|
|||||||
# Other
|
# Other
|
||||||
app/src/main/java/it/danieleverducci/lunatracker/TemporaryHardcodedCredentials.kt
|
app/src/main/java/it/danieleverducci/lunatracker/TemporaryHardcodedCredentials.kt
|
||||||
.kotlin/sessions/*
|
.kotlin/sessions/*
|
||||||
|
CLAUDE.md
|
||||||
|
.claude/
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -13,24 +13,7 @@ Dedicated to my daughter Luna.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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)
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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,44 @@ 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
|
||||||
|
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) { _, _ ->
|
||||||
|
event.quantity = picker.value
|
||||||
|
quantityTextView.text = NumericUtils(this@MainActivity).formatEventQuantity(event)
|
||||||
|
recyclerView.adapter?.notifyDataSetChanged()
|
||||||
|
saveLogbook()
|
||||||
|
}
|
||||||
|
pickerDialog.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
pickerDialog.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
d.setView(dialogView)
|
d.setView(dialogView)
|
||||||
d.setPositiveButton(R.string.dialog_event_detail_close_button) { dialogInterface, i -> dialogInterface.dismiss() }
|
d.setPositiveButton(R.string.dialog_event_detail_close_button) { dialogInterface, i -> dialogInterface.dismiss() }
|
||||||
d.setNeutralButton(R.string.dialog_event_detail_delete_button) { dialogInterface, i -> deleteEvent(event) }
|
d.setNeutralButton(R.string.dialog_event_detail_delete_button) { dialogInterface, i -> deleteEvent(event) }
|
||||||
@@ -800,6 +1112,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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 -> ""
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
343
app/src/main/java/utils/StatisticsCalculator.kt
Normal file
343
app/src/main/java/utils/StatisticsCalculator.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/src/main/res/drawable/ic_back.xml
Normal file
12
app/src/main/res/drawable/ic_back.xml
Normal 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>
|
||||||
12
app/src/main/res/drawable/ic_sleep.xml
Normal file
12
app/src/main/res/drawable/ic_sleep.xml
Normal 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>
|
||||||
16
app/src/main/res/drawable/ic_statistics.xml
Normal file
16
app/src/main/res/drawable/ic_statistics.xml
Normal 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>
|
||||||
17
app/src/main/res/drawable/progress_bar_accent.xml
Normal file
17
app/src/main/res/drawable/progress_bar_accent.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
57
app/src/main/res/layout/activity_statistics.xml
Normal file
57
app/src/main/res/layout/activity_statistics.xml
Normal 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>
|
||||||
20
app/src/main/res/layout/breastfeeding_duration_dialog.xml
Normal file
20
app/src/main/res/layout/breastfeeding_duration_dialog.xml
Normal 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>
|
||||||
35
app/src/main/res/layout/breastfeeding_timer_dialog.xml
Normal file
35
app/src/main/res/layout/breastfeeding_timer_dialog.xml
Normal 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>
|
||||||
274
app/src/main/res/layout/fragment_daily_summary.xml
Normal file
274
app/src/main/res/layout/fragment_daily_summary.xml
Normal 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>
|
||||||
129
app/src/main/res/layout/fragment_diaper_stats.xml
Normal file
129
app/src/main/res/layout/fragment_diaper_stats.xml
Normal 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>
|
||||||
175
app/src/main/res/layout/fragment_feeding_stats.xml
Normal file
175
app/src/main/res/layout/fragment_feeding_stats.xml
Normal 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>
|
||||||
100
app/src/main/res/layout/fragment_growth_stats.xml
Normal file
100
app/src/main/res/layout/fragment_growth_stats.xml
Normal 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>
|
||||||
106
app/src/main/res/layout/fragment_sleep_stats.xml
Normal file
106
app/src/main/res/layout/fragment_sleep_stats.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
20
app/src/main/res/layout/sleep_duration_dialog.xml
Normal file
20
app/src/main/res/layout/sleep_duration_dialog.xml
Normal 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>
|
||||||
36
app/src/main/res/layout/sleep_timer_dialog.xml
Normal file
36
app/src/main/res/layout/sleep_timer_dialog.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user