40 Commits

Author SHA1 Message Date
97a138bdf6 Fix time display after manual duration correction
When a timer (sleep/breastfeeding) was forgotten and the duration is
manually corrected, the "X minutes ago" display now reflects the adjusted
end time instead of the original stop time.
2026-01-17 21:38:05 +01:00
2355dd4390 Add sleep tracking, statistics module and backup features
Features:
- Sleep tracking with timer and manual duration input
- Statistics module with 5 tabs (daily summary, feeding, diapers, sleep, growth)
- Export/Import backup functionality in settings
- Complete German, French and Italian translations
2026-01-12 08:27:03 +01:00
587fc5d3e3 Add breastfeeding duration tracking and UI improvements
Features:
- Breastfeeding timer: Click to start, stop to save duration
- Manual duration input: Long-press for NumberPicker (1-60 min)
- Edit breastfeeding duration: Click on duration in event details
- Day separators: Visual dividers between days in event list
- German translations: Added missing strings for puke/bath events,
  time units, amount labels, signature settings, event details

The breastfeeding timer state persists across app restarts.
2026-01-08 09:32:30 +01:00
193e21ce25 Merge pull request 'Add puke and bath events, add signature setting' (#16) from mwarning/luna-tracker:master into develop
Reviewed-on: penguin86/luna-tracker#16
2025-11-05 07:56:01 +01:00
7f67c758c9 activity_setting: fine tune layout style 2025-10-26 20:38:26 +01:00
dfa64d71a8 add signature setting
For multiple users it helps to
keep track about who did what.
2025-10-26 20:38:26 +01:00
b7180068f3 DateUtils: move event details formatting to DateUtils
Also do not display seconds, because it is not
meaningful and is not selected in date picker.
2025-10-26 20:38:22 +01:00
36b848b95e add bath event type 2025-10-26 13:54:16 +01:00
a1bde917f8 add no-breastfeeding help text 2025-10-26 13:54:16 +01:00
4f4ff5ed21 more_events_popup: move enema to bottom and adjust padding
Enemas are usually are rare thing. Let's
move it to the bottom. Also adjust padding
to have more space to display all items.
2025-10-26 13:54:16 +01:00
453d838470 add puke event 2025-10-26 13:54:04 +01:00
34aa092722 NumericUtils: provide fallback for LocaleData.getMeasurementSystem
LocaleData.getMeasurementSystem is available at API level 28
but the app supports API level 21.
2025-09-29 03:34:08 +02:00
961e7b90e7 small code cleanup
No code behavior has been changed.
2025-09-29 03:33:30 +02:00
be77c7fb22 Added "get on F-Droid" button to readme 2025-09-21 09:28:02 +02:00
a7c44df553 Bumped version 2025-09-21 09:21:04 +02:00
928112adb8 Updated gradle wrapper 2025-09-21 09:20:47 +02:00
b90dc92874 Link to contributors in fastlane 2025-09-21 09:20:21 +02:00
36481a1194 Added thanks to Moritz Warning contribution 2025-09-21 09:12:11 +02:00
d4adb9d981 Merge pull request 'improvements' (#10) from mwarning/luna-tracker:improvements into master
Reviewed-on: penguin86/luna-tracker#10
2025-09-21 09:01:03 +02:00
5df3b31e64 activity_main: lower margin from 10 to 5dp
The buttons are big enough for fingers.
Let's make the view more compact.
2025-09-19 12:41:55 +02:00
0a96fb91d7 DateUtils: simplify code 2025-09-19 12:41:55 +02:00
f4c526ff8d add previous/next event link to details dialog
The links will point to the previous/next
event of the same type
2025-09-19 12:41:51 +02:00
ea88fd6bb6 make more strings translateable 2025-09-16 21:11:52 +02:00
3e2bdca083 make some val constant 2025-09-16 21:11:52 +02:00
e5e486c315 remove unused imports 2025-09-16 21:11:52 +02:00
3f648a6490 remove stray 1 character from time 2025-09-16 21:11:52 +02:00
cc6ebc398c activity_main: move progress bar next to event list title
Save some vertical space and prevent vertical size change/flicker.
2025-09-16 21:11:52 +02:00
e1f48e6524 MainActivity: support 24h format 2025-09-16 21:11:52 +02:00
5e1c67d52d MainActivity: small code cleanup 2025-09-16 21:11:52 +02:00
3a457b2ae6 bigger font for event detail dialog 2025-09-16 21:11:52 +02:00
a2fd04d233 remove log event title
Keep the main page simpler and have a bit more
space for log messages.
2025-09-16 21:11:52 +02:00
f1f73b65f7 add setting to disable breastfeeding buttons
Some women do not breastfeed. Hide the buttons
in order to have more space for log messages.
2025-09-16 21:11:49 +02:00
5c247f5948 replace dp with sp
As suggested by android-studio.
2025-09-16 12:23:44 +02:00
83b77b40d6 values: escape apostrophe characters 2025-09-16 12:23:44 +02:00
25f7b1fc00 add password toggle view icon 2025-09-16 12:23:44 +02:00
a635e46180 layout_marginLeft is deprecated 2025-09-16 12:23:40 +02:00
1080fea3f2 Merge pull request 'German translation by Daniel Neubauer + updated README.md' (#9) from develop into master
Reviewed-on: penguin86/luna-tracker#9
2025-09-02 09:22:47 +02:00
6232aedd04 German translation by Daniel Neubauer 2025-09-02 08:58:42 +02:00
30dc958a0c Merge pull request 'added french language translation and corrected a few english typos' (#6) from chepycou/luna-tracker:master into master
Reviewed-on: penguin86/luna-tracker#6
2025-07-02 13:22:15 +02:00
Chepycou
1de99e89e5 added french language translation and corrected a few english typos 2025-07-01 16:58:51 +02:00
60 changed files with 4145 additions and 255 deletions

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
LunaTracker

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@@ -1,5 +1,7 @@
# 🌜 LunaTracker 🌛
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/it/packages/it.danieleverducci.lunatracker/)
LunaTracker is a newborn baby tracking app.
Parenting can be tough. You get home from the hospital, exhausted, with this little fragile unknown thingy that has no user manual and a single way to let you know something's not ok: crying.
@@ -10,3 +12,9 @@ This app is meant to log all the relevant events (diaper change, breastfeeding,
Dedicated to my daughter Luna.
![Screenshot](fastlane/metadata/android/en-US/images/phoneScreenshots/1.png)
## Thanks for the valuable contributions to:
- Chepycou (French translation)
- Daniel Neubauer (German translation)
- Moritz Warning (Various bugfixes and new features)

View File

@@ -12,8 +12,8 @@ android {
applicationId = "it.danieleverducci.lunatracker"
minSdk = 21
targetSdk = 34
versionCode = 4
versionName = "0.6"
versionCode = 5
versionName = "0.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

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

View File

@@ -4,6 +4,7 @@ import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.DialogInterface
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.os.Handler
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.slider.Slider
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.adapters.DaySeparatorDecoration
import it.danieleverducci.lunatracker.adapters.LunaEventRecyclerAdapter
import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent
@@ -39,16 +41,16 @@ import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import kotlinx.coroutines.Runnable
import okio.IOException
import org.json.JSONException
import utils.DateUtils
import utils.NumericUtils
import java.text.DateFormat
import java.util.Calendar
import java.util.Date
class MainActivity : AppCompatActivity() {
companion object {
val TAG = "MainActivity"
val UPDATE_EVERY_SECS: Long = 30
val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false
const val TAG = "MainActivity"
const val UPDATE_EVERY_SECS: Long = 30
const val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false
}
var logbook: Logbook? = null
@@ -57,6 +59,7 @@ class MainActivity : AppCompatActivity() {
lateinit var buttonsContainer: ViewGroup
lateinit var recyclerView: RecyclerView
lateinit var handler: Handler
var signature = ""
var savingEvent = false
val updateListRunnable: Runnable = Runnable {
if (logbook != null && !pauseLogbookUpdate)
@@ -66,6 +69,19 @@ class MainActivity : AppCompatActivity() {
var logbookRepo: LogbookRepository? = null
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?) {
super.onCreate(savedInstanceState)
@@ -74,30 +90,36 @@ class MainActivity : AppCompatActivity() {
// Show view
setContentView(R.layout.activity_main)
progressIndicator = findViewById<LinearProgressIndicator>(R.id.progress_indicator)
buttonsContainer = findViewById<ViewGroup>(R.id.buttons_container)
recyclerView = findViewById<RecyclerView>(R.id.list_events)
progressIndicator = findViewById(R.id.progress_indicator)
buttonsContainer = findViewById(R.id.buttons_container)
recyclerView = findViewById(R.id.list_events)
recyclerView.setLayoutManager(LinearLayoutManager(applicationContext))
// Set listeners
findViewById<View>(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog(true) }
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_nipple_left).setOnClickListener { logEvent(
LunaEvent(
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE
)
) }
findViewById<View>(R.id.button_nipple_both).setOnClickListener { logEvent(
LunaEvent(
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE
)
) }
findViewById<View>(R.id.button_nipple_right).setOnClickListener { logEvent(
LunaEvent(
LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
)
) }
findViewById<View>(R.id.button_nipple_left).setOnClickListener {
startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE)
}
findViewById<View>(R.id.button_nipple_left).setOnLongClickListener {
askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE)
true
}
findViewById<View>(R.id.button_nipple_both).setOnClickListener {
startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE)
}
findViewById<View>(R.id.button_nipple_both).setOnLongClickListener {
askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_BOTH_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(
LunaEvent(
LunaEvent.TYPE_DIAPERCHANGE_POO
@@ -112,29 +134,38 @@ class MainActivity : AppCompatActivity() {
moreButton.setOnClickListener {
showOverflowPopupWindow(moreButton)
}
findViewById<View>(R.id.button_no_connection_settings).setOnClickListener({
findViewById<View>(R.id.button_no_connection_settings).setOnClickListener {
showSettings()
})
findViewById<View>(R.id.button_settings).setOnClickListener({
}
findViewById<View>(R.id.button_settings).setOnClickListener {
showSettings()
})
findViewById<View>(R.id.button_no_connection_retry).setOnClickListener({
}
findViewById<View>(R.id.button_statistics).setOnClickListener {
showStatistics()
}
findViewById<View>(R.id.button_no_connection_retry).setOnClickListener {
// This may happen at start, when logbook is still null: better ask the logbook list
loadLogbookList()
})
findViewById<View>(R.id.button_sync).setOnClickListener({
}
findViewById<View>(R.id.button_sync).setOnClickListener {
loadLogbookList()
})
}
}
private fun setListAdapter(items: ArrayList<LunaEvent>) {
val adapter = LunaEventRecyclerAdapter(this, items)
adapter.onItemClickListener = object: LunaEventRecyclerAdapter.OnItemClickListener {
override fun onItemClick(event: LunaEvent) {
showEventDetailDialog(event)
showEventDetailDialog(event, items)
}
}
recyclerView.adapter = adapter
// Tages-Trenner hinzufügen
while (recyclerView.itemDecorationCount > 0) {
recyclerView.removeItemDecorationAt(0)
}
recyclerView.addItemDecoration(DaySeparatorDecoration(this, items))
}
fun showSettings() {
@@ -142,6 +173,12 @@ class MainActivity : AppCompatActivity() {
startActivity(i)
}
fun showStatistics() {
val i = Intent(this, StatisticsActivity::class.java)
i.putExtra(StatisticsActivity.EXTRA_LOGBOOK_NAME, logbook?.name ?: "")
startActivity(i)
}
fun showLogbook() {
// Show logbook
if (logbook == null)
@@ -168,9 +205,23 @@ class MainActivity : AppCompatActivity() {
logbookRepo = FileLogbookRepository()
}
signature = settingsRepository.loadSignature()
val noBreastfeeding = settingsRepository.loadNoBreastfeeding()
findViewById<View>(R.id.layout_nipples).visibility = when (noBreastfeeding) {
true -> View.GONE
false -> View.VISIBLE
}
// Update list dates
recyclerView.adapter?.notifyDataSetChanged()
// Check for ongoing breastfeeding timer
restoreBreastfeedingTimerIfNeeded()
// Check for ongoing sleep timer
restoreSleepTimerIfNeeded()
if (logbook != null) {
// Already running: reload data for currently selected logbook
loadLogbook(logbook!!.name)
@@ -183,6 +234,14 @@ class MainActivity : AppCompatActivity() {
override fun onStop() {
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()
}
@@ -253,6 +312,26 @@ class MainActivity : AppCompatActivity() {
alertDialog.show()
}
fun askPukeValue() {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.puke_dialog, null)
d.setTitle(R.string.log_puke_dialog_title)
d.setMessage(R.string.log_puke_dialog_description)
d.setView(dialogView)
val spinner = dialogView.findViewById<Spinner>(R.id.dialog_puke_value)
spinner.adapter = ArrayAdapter.createFromResource(this, R.array.AmountLabels, android.R.layout.simple_spinner_dropdown_item)
spinner.setSelection(1)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val pos = spinner.selectedItemPosition
logEvent(LunaEvent(LunaEvent.TYPE_PUKE, pos))
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
val alertDialog = d.create()
alertDialog.show()
}
fun askNotes(lunaEvent: LunaEvent) {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_notes, null)
@@ -282,6 +361,230 @@ class MainActivity : AppCompatActivity() {
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() {
val d = AlertDialog.Builder(this)
d.setTitle(R.string.trim_logbook_dialog_title)
@@ -302,25 +605,52 @@ class MainActivity : AppCompatActivity() {
alertDialog.show()
}
fun showEventDetailDialog(event: LunaEvent) {
fun getPreviousSameEvent(event: LunaEvent, items: ArrayList<LunaEvent>): LunaEvent? {
var previousEvent: LunaEvent? = null
for (item in items) {
if (item.type == event.type && item.time < event.time) {
if (previousEvent == null) {
previousEvent = item
} else if (previousEvent.time < item.time) {
previousEvent = item
}
}
}
return previousEvent
}
fun getNextSameEvent(event: LunaEvent, items: ArrayList<LunaEvent>): LunaEvent? {
var nextEvent: LunaEvent? = null
for (item in items) {
if (item.type == event.type && item.time > event.time) {
if (nextEvent == null) {
nextEvent = item
} else if (nextEvent.time > item.time) {
nextEvent = item
}
}
}
return nextEvent
}
fun showEventDetailDialog(event: LunaEvent, items: ArrayList<LunaEvent>) {
// Do not update list while the detail is shown, to avoid changing the object below while it is changed by the user
pauseLogbookUpdate = true
val dateFormat = DateFormat.getDateTimeInstance();
val d = AlertDialog.Builder(this)
d.setTitle(R.string.dialog_event_detail_title)
val dialogView = layoutInflater.inflate(R.layout.dialog_event_detail, null)
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_emoji).setText(event.getTypeEmoji(this))
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_description).setText(event.getTypeDescription(this))
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity).setText(
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_emoji).text = event.getTypeEmoji(this)
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_description).text = event.getTypeDescription(this)
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity).text =
NumericUtils(this).formatEventQuantity(event)
)
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_notes).setText(event.notes)
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_notes).text = event.notes
val currentDateTime = Calendar.getInstance()
currentDateTime.time = Date(event.time * 1000)
val dateTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_date)
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), dateFormat.format(currentDateTime.time))
dateTextView.setOnClickListener({
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), DateUtils.formatDateTime(event.time))
dateTextView.setOnClickListener {
// Show datetime picker
val startYear = currentDateTime.get(Calendar.YEAR)
val startMonth = currentDateTime.get(Calendar.MONTH)
@@ -328,21 +658,63 @@ class MainActivity : AppCompatActivity() {
val startHour = currentDateTime.get(Calendar.HOUR_OF_DAY)
val startMinute = currentDateTime.get(Calendar.MINUTE)
DatePickerDialog(this, DatePickerDialog.OnDateSetListener { _, year, month, day ->
TimePickerDialog(this, TimePickerDialog.OnTimeSetListener { _, hour, minute ->
DatePickerDialog(this, { _, year, month, day ->
TimePickerDialog(this, { _, hour, minute ->
val pickedDateTime = Calendar.getInstance()
pickedDateTime.set(year, month, day, hour, minute)
currentDateTime.time = pickedDateTime.time
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), dateFormat.format(currentDateTime.time))
// Save event and move it to the right position in the logbook
event.time = currentDateTime.time.time / 1000 // Seconds since epoch
event.time = pickedDateTime.time.time / 1000 // Seconds since epoch
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), DateUtils.formatDateTime(event.time))
logbook?.sort()
recyclerView.adapter?.notifyDataSetChanged()
saveLogbook()
}, startHour, startMinute, false).show()
}, startHour, startMinute, android.text.format.DateFormat.is24HourFormat(this@MainActivity)).show()
}, startYear, startMonth, startDay).show()
})
}
// Make quantity editable for breastfeeding and sleep events
val quantityTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity)
val isBreastfeeding = event.type in listOf(
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
)
val isSleep = event.type == LunaEvent.TYPE_SLEEP
if ((isBreastfeeding || isSleep) && event.quantity > 0) {
quantityTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_edit, 0)
quantityTextView.compoundDrawableTintList = ColorStateList.valueOf(getColor(R.color.accent))
quantityTextView.setOnClickListener {
val pickerDialog = AlertDialog.Builder(this@MainActivity)
val pickerView = if (isSleep) {
layoutInflater.inflate(R.layout.sleep_duration_dialog, null)
} else {
layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
}
val picker = pickerView.findViewById<NumberPicker>(
if (isSleep) R.id.sleep_duration_picker else R.id.breastfeeding_duration_picker
)
picker.minValue = 1
picker.maxValue = if (isSleep) 180 else 60
val oldQuantity = event.quantity
picker.value = if (event.quantity > 0) Math.min(event.quantity, picker.maxValue) else if (isSleep) 30 else 15
pickerDialog.setTitle(if (isSleep) R.string.sleep_duration_title else R.string.breastfeeding_duration_title)
pickerDialog.setView(pickerView)
pickerDialog.setPositiveButton(android.R.string.ok) { _, _ ->
val newQuantity = picker.value
if (newQuantity != oldQuantity) {
// Adjust end time based on duration change (duration reduced = end time earlier)
event.time = event.time - (oldQuantity - newQuantity) * 60L
event.quantity = newQuantity
}
quantityTextView.text = NumericUtils(this@MainActivity).formatEventQuantity(event)
recyclerView.adapter?.notifyDataSetChanged()
saveLogbook()
}
pickerDialog.setNegativeButton(android.R.string.cancel, null)
pickerDialog.show()
}
}
d.setView(dialogView)
d.setPositiveButton(R.string.dialog_event_detail_close_button) { dialogInterface, i -> dialogInterface.dismiss() }
@@ -354,6 +726,44 @@ class MainActivity : AppCompatActivity() {
// Resume logbook update
pauseLogbookUpdate = false
})
// show optional signature
if (event.signature.isNotEmpty()) {
val signatureTextEdit = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_signature)
signatureTextEdit.text = String.format(getString(R.string.dialog_event_detail_signature), event.signature)
signatureTextEdit.visibility = View.VISIBLE
}
// create next/previous links to events of the same type
val previousTextView = dialogView.findViewById<TextView>(R.id.dialog_event_previous)
val nextTextView = dialogView.findViewById<TextView>(R.id.dialog_event_next)
val nextEvent = getNextSameEvent(event, items)
val previousEvent = getPreviousSameEvent(event, items)
if (previousEvent != null) {
val emoji = previousEvent.getTypeEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, event.time - previousEvent.time)
previousTextView.text = String.format("⬅️ %s %s", emoji, time)
previousTextView.setOnClickListener {
alertDialog.cancel()
showEventDetailDialog(previousEvent, items)
}
} else {
previousTextView.visibility = View.GONE
}
if (nextEvent != null) {
val emoji = nextEvent.getTypeEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.time - event.time)
nextTextView.text = String.format("%s %s ➡️", time, emoji)
nextTextView.setOnClickListener {
alertDialog.cancel()
showEventDetailDialog(nextEvent, items)
}
} else {
nextTextView.visibility = View.GONE
}
}
fun showAddLogbookDialog(requestedByUser: Boolean) {
@@ -392,7 +802,7 @@ class MainActivity : AppCompatActivity() {
sAdapter.setDropDownViewResource(R.layout.row_logbook_spinner)
for (ln in logbooksNames) {
sAdapter.add(
if (ln.isEmpty()) getString(R.string.default_logbook_name) else ln
ln.ifEmpty { getString(R.string.default_logbook_name) }
)
}
spinner.adapter = sAdapter
@@ -410,7 +820,6 @@ class MainActivity : AppCompatActivity() {
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
})
}
@@ -560,7 +969,6 @@ class MainActivity : AppCompatActivity() {
onRepoError(getString(R.string.settings_generic_error) + error.toString())
})
}
})
}
@@ -575,6 +983,8 @@ class MainActivity : AppCompatActivity() {
fun logEvent(event: LunaEvent) {
savingEvent(true)
event.signature = signature
setLoading(true)
logbook?.logs?.add(0, event)
recyclerView.adapter?.notifyItemInserted(0)
@@ -708,10 +1118,19 @@ class MainActivity : AppCompatActivity() {
isOutsideTouchable = true
val inflater = LayoutInflater.from(anchor.context)
contentView = inflater.inflate(R.layout.more_events_popup, null)
contentView.findViewById<View>(R.id.button_medicine).setOnClickListener({
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 {
askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE))
dismiss()
})
}
contentView.findViewById<View>(R.id.button_enema).setOnClickListener({
logEvent(LunaEvent(LunaEvent.TYPE_ENEMA))
dismiss()
@@ -724,6 +1143,10 @@ class MainActivity : AppCompatActivity() {
askTemperatureValue()
dismiss()
})
contentView.findViewById<View>(R.id.button_puke).setOnClickListener({
askPukeValue()
dismiss()
})
contentView.findViewById<View>(R.id.button_colic).setOnClickListener({
logEvent(
LunaEvent(LunaEvent.TYPE_COLIC)
@@ -734,6 +1157,12 @@ class MainActivity : AppCompatActivity() {
askWeightValue()
dismiss()
})
contentView.findViewById<View>(R.id.button_bath).setOnClickListener({
logEvent(
LunaEvent(LunaEvent.TYPE_BATH)
)
dismiss()
})
}.also { popupWindow ->
popupWindow.setOnDismissListener({
Handler(mainLooper).postDelayed({

View File

@@ -1,20 +1,26 @@
package it.danieleverducci.lunatracker
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.RadioButton
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.switchmaterial.SwitchMaterial
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException
import org.json.JSONException
import org.json.JSONArray
import org.json.JSONObject
open class SettingsActivity : AppCompatActivity() {
protected lateinit var settingsRepository: LocalSettingsRepository
@@ -24,6 +30,17 @@ open class SettingsActivity : AppCompatActivity() {
protected lateinit var textViewWebDAVUser: TextView
protected lateinit var textViewWebDAVPass: TextView
protected lateinit var progressIndicator: LinearProgressIndicator
protected lateinit var switchNoBreastfeeding: SwitchMaterial
protected lateinit var textViewSignature: EditText
// Activity Result Launchers for Export/Import
private val exportLauncher = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri -> uri?.let { exportLogbookToUri(it) } }
private val importLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri -> uri?.let { importLogbookFromUri(it) } }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -35,12 +52,21 @@ open class SettingsActivity : AppCompatActivity() {
textViewWebDAVUser = findViewById(R.id.settings_data_webdav_user)
textViewWebDAVPass = findViewById(R.id.settings_data_webdav_pass)
progressIndicator = findViewById(R.id.progress_indicator)
switchNoBreastfeeding = findViewById(R.id.switch_no_breastfeeding)
textViewSignature = findViewById(R.id.settings_signature)
findViewById<View>(R.id.settings_save).setOnClickListener({
validateAndSave()
})
findViewById<View>(R.id.settings_cancel).setOnClickListener({
finish()
})
findViewById<View>(R.id.settings_export).setOnClickListener({
startExport()
})
findViewById<View>(R.id.settings_import).setOnClickListener({
startImport()
})
settingsRepository = LocalSettingsRepository(this)
loadSettings()
@@ -49,15 +75,21 @@ open class SettingsActivity : AppCompatActivity() {
fun loadSettings() {
val dataRepo = settingsRepository.loadDataRepository()
val webDavCredentials = settingsRepository.loadWebdavCredentials()
val noBreastfeeding = settingsRepository.loadNoBreastfeeding()
val signature = settingsRepository.loadSignature()
when (dataRepo) {
LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> radioDataLocal.isChecked = true
LocalSettingsRepository.DATA_REPO.WEBDAV -> radioDataWebDAV.isChecked = true
}
textViewSignature.setText(signature)
switchNoBreastfeeding.isChecked = noBreastfeeding
if (webDavCredentials != null) {
textViewWebDAVUrl.setText(webDavCredentials[0])
textViewWebDAVUser.setText(webDavCredentials[1])
textViewWebDAVPass.setText(webDavCredentials[2])
textViewWebDAVUrl.text = webDavCredentials[0]
textViewWebDAVUser.text = webDavCredentials[1]
textViewWebDAVPass.text = webDavCredentials[2]
}
}
@@ -149,6 +181,8 @@ open class SettingsActivity : AppCompatActivity() {
if (radioDataWebDAV.isChecked) LocalSettingsRepository.DATA_REPO.WEBDAV
else LocalSettingsRepository.DATA_REPO.LOCAL_FILE
)
settingsRepository.saveNoBreastfeeding(switchNoBreastfeeding.isChecked)
settingsRepository.saveSignature(textViewSignature.text.toString())
settingsRepository.saveWebdavCredentials(
textViewWebDAVUrl.text.toString(),
textViewWebDAVUser.text.toString(),
@@ -163,7 +197,7 @@ open class SettingsActivity : AppCompatActivity() {
*/
private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) {
Thread(Runnable {
var errors = StringBuilder()
val errors = StringBuilder()
val fileLogbookRepo = FileLogbookRepository()
val logbooks = fileLogbookRepo.getAllLogbooks(this)
for (logbook in logbooks) {
@@ -185,4 +219,136 @@ open class SettingsActivity : AppCompatActivity() {
fun onCopyLocalLogbooksToWebdavFinished(errors: String?)
}
// Export/Import functionality
private fun startExport() {
val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.US)
.format(java.util.Date())
exportLauncher.launch("lunatracker_backup_$timestamp.json")
}
private fun startImport() {
importLauncher.launch(arrayOf("application/json"))
}
private fun exportLogbookToUri(uri: Uri) {
progressIndicator.visibility = View.VISIBLE
Thread {
try {
val fileLogbookRepo = FileLogbookRepository()
val logbooks = fileLogbookRepo.getAllLogbooks(this)
val json = JSONObject().apply {
put("version", 1)
put("app", "LunaTracker")
put("exported_at", System.currentTimeMillis())
put("logbooks", JSONArray().apply {
logbooks.forEach { logbook ->
put(JSONObject().apply {
put("name", logbook.name)
put("events", JSONArray().apply {
logbook.logs.forEach { event ->
put(event.toJson())
}
})
})
}
})
}
contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(json.toString(2).toByteArray(Charsets.UTF_8))
}
val eventCount = logbooks.sumOf { it.logs.size }
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.export_success, eventCount),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.export_error) + e.message,
Toast.LENGTH_SHORT
).show()
}
}
}.start()
}
private fun importLogbookFromUri(uri: Uri) {
progressIndicator.visibility = View.VISIBLE
Thread {
try {
val jsonString = contentResolver.openInputStream(uri)?.bufferedReader()?.readText()
?: throw Exception("Could not read file")
val json = JSONObject(jsonString)
val version = json.optInt("version", 1)
val fileLogbookRepo = FileLogbookRepository()
var totalEvents = 0
if (json.has("logbooks")) {
// New format with multiple logbooks
val logbooksArray = json.getJSONArray("logbooks")
for (i in 0 until logbooksArray.length()) {
val logbookJson = logbooksArray.getJSONObject(i)
val name = logbookJson.optString("name", "")
val eventsArray = logbookJson.getJSONArray("events")
val logbook = Logbook(name)
for (j in 0 until eventsArray.length()) {
try {
logbook.logs.add(LunaEvent(eventsArray.getJSONObject(j)))
totalEvents++
} catch (e: IllegalArgumentException) {
// Skip invalid events
}
}
fileLogbookRepo.saveLogbook(this, logbook)
}
} else if (json.has("events")) {
// Old format with single logbook
val name = json.optString("logbook_name", "")
val eventsArray = json.getJSONArray("events")
val logbook = Logbook(name)
for (i in 0 until eventsArray.length()) {
try {
logbook.logs.add(LunaEvent(eventsArray.getJSONObject(i)))
totalEvents++
} catch (e: IllegalArgumentException) {
// Skip invalid events
}
}
fileLogbookRepo.saveLogbook(this, logbook)
}
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.import_success, totalEvents),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.import_error) + ": " + e.message,
Toast.LENGTH_LONG
).show()
}
}
}.start()
}
}

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ package it.danieleverducci.lunatracker.entities
class Logbook(val name: String) {
companion object {
val MAX_SAFE_LOGBOOK_SIZE = 30000
const val MAX_SAFE_LOGBOOK_SIZE = 30000
}
val logs = ArrayList<LunaEvent>()

View File

@@ -14,20 +14,23 @@ import java.util.Date
class LunaEvent: Comparable<LunaEvent> {
companion object {
val TYPE_BABY_BOTTLE = "BABY_BOTTLE"
val TYPE_WEIGHT = "WEIGHT"
val TYPE_BREASTFEEDING_LEFT_NIPPLE = "BREASTFEEDING_LEFT_NIPPLE"
val TYPE_BREASTFEEDING_BOTH_NIPPLE = "BREASTFEEDING_BOTH_NIPPLE"
val TYPE_BREASTFEEDING_RIGHT_NIPPLE = "BREASTFEEDING_RIGHT_NIPPLE"
val TYPE_DIAPERCHANGE_POO = "DIAPERCHANGE_POO"
val TYPE_DIAPERCHANGE_PEE = "DIAPERCHANGE_PEE"
val TYPE_MEDICINE = "MEDICINE"
val TYPE_ENEMA = "ENEMA"
val TYPE_NOTE = "NOTE"
val TYPE_CUSTOM = "CUSTOM"
val TYPE_COLIC = "COLIC"
val TYPE_TEMPERATURE = "TEMPERATURE"
val TYPE_FOOD = "FOOD"
const val TYPE_BABY_BOTTLE = "BABY_BOTTLE"
const val TYPE_WEIGHT = "WEIGHT"
const val TYPE_BREASTFEEDING_LEFT_NIPPLE = "BREASTFEEDING_LEFT_NIPPLE"
const val TYPE_BREASTFEEDING_BOTH_NIPPLE = "BREASTFEEDING_BOTH_NIPPLE"
const val TYPE_BREASTFEEDING_RIGHT_NIPPLE = "BREASTFEEDING_RIGHT_NIPPLE"
const val TYPE_DIAPERCHANGE_POO = "DIAPERCHANGE_POO"
const val TYPE_DIAPERCHANGE_PEE = "DIAPERCHANGE_PEE"
const val TYPE_MEDICINE = "MEDICINE"
const val TYPE_ENEMA = "ENEMA"
const val TYPE_NOTE = "NOTE"
const val TYPE_CUSTOM = "CUSTOM"
const val TYPE_COLIC = "COLIC"
const val TYPE_TEMPERATURE = "TEMPERATURE"
const val TYPE_FOOD = "FOOD"
const val TYPE_PUKE = "PUKE"
const val TYPE_BATH = "BATH"
const val TYPE_SLEEP = "SLEEP"
}
private val jo: JSONObject
@@ -53,6 +56,12 @@ class LunaEvent: Comparable<LunaEvent> {
set(value) {
jo.put("notes", value)
}
var signature: String
get(): String = jo.optString("signature")
set(value) {
if (value.isNotEmpty())
jo.put("signature", value)
}
constructor(jo: JSONObject) {
this.jo = jo
@@ -90,6 +99,9 @@ class LunaEvent: Comparable<LunaEvent> {
TYPE_TEMPERATURE -> R.string.event_temperature_type
TYPE_COLIC -> R.string.event_colic_type
TYPE_FOOD -> R.string.event_food_type
TYPE_PUKE -> R.string.event_puke_type
TYPE_BATH -> R.string.event_bath_type
TYPE_SLEEP -> R.string.event_sleep_type
else -> R.string.event_unknown_type
}
)
@@ -111,6 +123,9 @@ class LunaEvent: Comparable<LunaEvent> {
TYPE_TEMPERATURE -> R.string.event_temperature_desc
TYPE_COLIC -> R.string.event_colic_desc
TYPE_FOOD -> R.string.event_food_desc
TYPE_PUKE -> R.string.event_puke_desc
TYPE_BATH -> R.string.event_bath_desc
TYPE_SLEEP -> R.string.event_sleep_desc
else -> R.string.event_unknown_desc
}
)
@@ -128,7 +143,7 @@ class LunaEvent: Comparable<LunaEvent> {
}
override fun toString(): String {
return "${type} qty: $quantity time: ${Date(time * 1000)}"
return "$type qty: $quantity time: ${Date(time * 1000)}"
}
override fun compareTo(other: LunaEvent): Int {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,21 @@ package it.danieleverducci.lunatracker.repository
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import androidx.core.content.edit
class LocalSettingsRepository(val context: Context) {
companion object {
val SHARED_PREFS_FILE_NAME = "lunasettings"
val SHARED_PREFS_BB_CONTENT = "bbcontent"
val SHARED_PREFS_DATA_REPO = "data_repo"
val SHARED_PREFS_DAV_URL = "webdav_url"
val SHARED_PREFS_DAV_USER = "webdav_user"
val SHARED_PREFS_DAV_PASS = "webdav_password"
const val SHARED_PREFS_FILE_NAME = "lunasettings"
const val SHARED_PREFS_BB_CONTENT = "bbcontent"
const val SHARED_PREFS_DATA_REPO = "data_repo"
const val SHARED_PREFS_DAV_URL = "webdav_url"
const val SHARED_PREFS_DAV_USER = "webdav_user"
const val SHARED_PREFS_DAV_PASS = "webdav_password"
const val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding"
const val SHARED_PREFS_SIGNATURE = "signature"
const val SHARED_PREFS_BF_TIMER_START = "bf_timer_start"
const val SHARED_PREFS_BF_TIMER_TYPE = "bf_timer_type"
const val SHARED_PREFS_SLEEP_TIMER_START = "sleep_timer_start"
}
enum class DATA_REPO {LOCAL_FILE, WEBDAV}
val sharedPreferences: SharedPreferences
@@ -21,23 +27,39 @@ class LocalSettingsRepository(val context: Context) {
}
fun saveBabyBottleContent(content: Int) {
sharedPreferences.edit().putInt(SHARED_PREFS_BB_CONTENT, content).apply()
sharedPreferences.edit { putInt(SHARED_PREFS_BB_CONTENT, content) }
}
fun loadBabyBottleContent(): Int {
return sharedPreferences.getInt(SHARED_PREFS_BB_CONTENT, 1)
}
fun saveSignature(content: String) {
sharedPreferences.edit { putString(SHARED_PREFS_SIGNATURE, content) }
}
fun loadSignature(): String {
return sharedPreferences.getString(SHARED_PREFS_SIGNATURE, "") ?: ""
}
fun saveNoBreastfeeding(content: Boolean) {
sharedPreferences.edit { putBoolean(SHARED_PREFS_NO_BREASTFEEDING, content) }
}
fun loadNoBreastfeeding(): Boolean {
return sharedPreferences.getBoolean(SHARED_PREFS_NO_BREASTFEEDING, false)
}
fun saveDataRepository(repo: DATA_REPO) {
val spe = sharedPreferences.edit()
spe.putString(
sharedPreferences.edit(commit = true) {
putString(
SHARED_PREFS_DATA_REPO,
when (repo) {
DATA_REPO.WEBDAV -> "webdav"
DATA_REPO.LOCAL_FILE -> "localfile"
}
)
spe.commit()
}
}
fun loadDataRepository(): DATA_REPO {
@@ -50,11 +72,11 @@ class LocalSettingsRepository(val context: Context) {
}
fun saveWebdavCredentials(url: String, username: String, password: String) {
val spe = sharedPreferences.edit()
spe.putString(SHARED_PREFS_DAV_URL, url)
spe.putString(SHARED_PREFS_DAV_USER, username)
spe.putString(SHARED_PREFS_DAV_PASS, password)
spe.commit()
sharedPreferences.edit(commit = true) {
putString(SHARED_PREFS_DAV_URL, url)
putString(SHARED_PREFS_DAV_USER, username)
putString(SHARED_PREFS_DAV_PASS, password)
}
}
fun loadWebdavCredentials(): Array<String>? {
@@ -65,4 +87,41 @@ class LocalSettingsRepository(val context: Context) {
return null
return arrayOf(url, user, pass)
}
fun saveBreastfeedingTimer(startTime: Long, eventType: String) {
sharedPreferences.edit {
putLong(SHARED_PREFS_BF_TIMER_START, startTime)
putString(SHARED_PREFS_BF_TIMER_TYPE, eventType)
}
}
fun loadBreastfeedingTimer(): Pair<Long, String>? {
val startTime = sharedPreferences.getLong(SHARED_PREFS_BF_TIMER_START, 0)
val eventType = sharedPreferences.getString(SHARED_PREFS_BF_TIMER_TYPE, null)
if (startTime == 0L || eventType == null) return null
return Pair(startTime, eventType)
}
fun clearBreastfeedingTimer() {
sharedPreferences.edit {
remove(SHARED_PREFS_BF_TIMER_START)
remove(SHARED_PREFS_BF_TIMER_TYPE)
}
}
fun saveSleepTimer(startTime: Long) {
sharedPreferences.edit {
putLong(SHARED_PREFS_SLEEP_TIMER_START, startTime)
}
}
fun loadSleepTimer(): Long {
return sharedPreferences.getLong(SHARED_PREFS_SLEEP_TIMER_START, 0)
}
fun clearSleepTimer() {
sharedPreferences.edit {
remove(SHARED_PREFS_SLEEP_TIMER_START)
}
}
}

View File

@@ -1,15 +1,77 @@
package utils
import android.content.Context
import android.os.Build
import android.text.format.DateFormat
import it.danieleverducci.lunatracker.R
import java.util.Date
class DateUtils {
companion object {
/**
* Format time duration in seconds as e.g. "2 hours, 1 min".
* Used for the duration to the next/previous event in the event details dialog.
*/
fun formatTimeDuration(context: Context, secondsDiff: Long): String {
var seconds = secondsDiff
val years = (seconds / (365 * 24 * 60 * 60F)).toLong()
seconds -= years * (365 * 24 * 60 * 60)
val days = (seconds / (24 * 60 * 60F)).toLong()
seconds -= days * (24 * 60 * 60)
val hours = (seconds / (60 * 60F)).toLong()
seconds -= hours * (60 * 60)
val minutes = (seconds / 60F).toLong()
seconds -= minutes * 60
fun format(value1: Long, value2: Long, resIdSingular1: Int, resIdPlural1: Int, resIdSingular2: Int, resIdPlural2: Int): String {
val builder = StringBuilder()
if (value1 == 0L) {
// omit
} else if (value1 == 1L) {
builder.append(value1)
builder.append(" ")
builder.append(context.getString(resIdSingular1))
} else {
builder.append(value1)
builder.append(" ")
builder.append(context.getString(resIdPlural1))
}
if (value1 > 0L && value2 > 0L) {
builder.append(", ")
}
if (value2 == 0L) {
// omit
} else if (value2 == 1L) {
builder.append(value2)
builder.append(" ")
builder.append(context.getString(resIdSingular2))
} else {
builder.append(value2)
builder.append(" ")
builder.append(context.getString(resIdPlural2))
}
return builder.toString()
}
if (years > 0) {
return format(years, days, R.string.year_ago, R.string.years_ago, R.string.day_ago, R.string.days_ago)
} else if (days > 0) {
return format(days, hours, R.string.day_ago, R.string.days_ago, R.string.hour_ago, R.string.hours_ago)
} else if (hours > 0) {
return format(hours, minutes, R.string.hour_ago, R.string.hours_ago, R.string.minute_ago, R.string.minutes_ago)
} else if (minutes > 0) {
return format(minutes, seconds, R.string.minute_ago, R.string.minute_ago, R.string.second_ago, R.string.seconds_ago)
} else {
return context.getString(R.string.now)
}
}
/**
* Formats the provided unix timestamp in a string like "3 hours, 26 minutes ago)
* Formats the provided unix timestamp in a string like "3 hours, 26 minutes ago".
* Used for the event list.
*/
fun formatTimeAgo(context: Context, unixTime: Long): String {
val secondsDiff = (System.currentTimeMillis() / 1000) - unixTime
@@ -25,10 +87,10 @@ class DateUtils {
return DateFormat.getDateFormat(context).format(Date(unixTime*1000)) + "\n" +
DateFormat.getTimeFormat(context).format(Date(unixTime*1000))
var formattedTime = StringBuilder()
val formattedTime = StringBuilder()
if (hoursAgo > 0) {
formattedTime.append(hoursAgo).append(" ")
if (hoursAgo.toInt() == 1)
if (hoursAgo == 1)
formattedTime.append(context.getString(R.string.hour_ago))
else
formattedTime.append(context.getString(R.string.hours_ago))
@@ -37,12 +99,28 @@ class DateUtils {
if (formattedTime.isNotEmpty())
formattedTime.append(", ")
formattedTime.append(minutesAgo).append(" ")
if (minutesAgo.toInt() == 1)
if (minutesAgo == 1)
formattedTime.append(context.getString(R.string.minute_ago))
else
formattedTime.append(context.getString(R.string.minutes_ago))
}
return formattedTime.toString()
}
/**
* Format time as localized string without seconds. E.g. "Sept 18, 2025, 03:36 PM".
* Used in the event detail dialog.
*/
fun formatDateTime(unixTime: Long): String {
val date = Date(unixTime * 1000)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val dateFormat = android.icu.text.DateFormat.getDateTimeInstance(android.icu.text.DateFormat.DEFAULT, android.icu.text.DateFormat.SHORT)
return dateFormat.format(date)
} else {
// fallback
val dateFormat = java.text.DateFormat.getDateTimeInstance()
return dateFormat.format(date)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,15 @@
android:layout_height="wrap_content"
android: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
android:id="@+id/button_settings"
android:layout_width="wrap_content"
@@ -30,7 +39,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/title"
android:textSize="26dp"
android:textSize="26sp"
android:gravity="center"/>
<ImageView
@@ -47,7 +56,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="38dp"
android:layout_margin="10dp"
android:layout_margin="5dp"
android:orientation="horizontal"
android:gravity="center_vertical">
@@ -68,23 +77,17 @@
android:id="@+id/logbooks_add_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textStyle="bold"
android:textColor="@color/accent"
android:textSize="20dp"
android:textSize="20sp"
android:text="+"
android:background="@drawable/button_background"/>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/log_an_event"
android:gravity="center_horizontal"/>
<LinearLayout
android:id="@+id/buttons_container"
android:layout_width="match_parent"
@@ -101,10 +104,10 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:layout_margin="10dp"
android:layout_margin="5dp"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="50dp"
android:textSize="50sp"
android:text="@string/event_bottle_type"/>
<TextView
@@ -112,49 +115,50 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="10dp"
android:layout_margin="5dp"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="50dp"
android:textSize="50sp"
android:text="@string/event_food_type"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:id="@+id/layout_nipples">
<TextView
android:id="@+id/button_nipple_left"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30dp"
android:textSize="30sp"
android:text="🤱⬅️"/>
<TextView
android:id="@+id/button_nipple_both"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30dp"
android:textSize="30sp"
android:text="🤱↔️"/>
<TextView
android:id="@+id/button_nipple_right"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30dp"
android:textSize="30sp"
android:text="🤱➡️️"/>
</LinearLayout>
@@ -167,29 +171,29 @@
android:id="@+id/button_change_poo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_margin="5dp"
android:layout_weight="2"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30dp"
android:textSize="30sp"
android:text="🚼 💩"/>
<TextView
android:id="@+id/button_change_pee"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_margin="5dp"
android:layout_weight="2"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30dp"
android:textSize="30sp"
android:text="🚼 💧"/>
<ImageView
android:id="@+id/button_more"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
@@ -200,22 +204,32 @@
</LinearLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_indicator"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:indeterminate="true"
app:indicatorColor="@color/accent"
android:visibility="invisible"/>
android:layout_marginTop="5dp"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/logbook"
android:textColor="@color/accent"
android:textStyle="bold"/>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_indicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center"
android:layout_marginHorizontal="10dp"
android:indeterminate="true"
app:indicatorColor="@color/accent"
android:visibility="invisible"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_events"
android:layout_width="match_parent"
@@ -247,7 +261,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textSize="30dp"
android:textSize="30sp"
android:textColor="@color/accent"
android:text="@string/no_connection"/>
@@ -264,7 +278,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:drawableLeft="@drawable/ic_sync"
android:drawableStart="@drawable/ic_sync"
android:drawableTint="@color/white"
android:drawablePadding="10dp"
android:text="@string/no_connection_retry"
@@ -275,7 +289,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:drawableLeft="@drawable/ic_settings"
android:drawableStart="@drawable/ic_settings"
android:drawableTint="@color/white"
android:drawablePadding="10dp"
android:text="@string/no_connection_go_to_settings"

View File

@@ -39,26 +39,26 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:text="@string/settings_storage_local_desc"/>
<RadioButton android:id="@+id/settings_data_webdav"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginTop="20dp"
android:text="@string/settings_storage_dav"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:text="@string/settings_storage_dav_desc"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:textStyle="bold"
android:text="@string/settings_storage_dav_url"/>
@@ -66,7 +66,7 @@
android:id="@+id/settings_data_webdav_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:hint="@string/settings_storage_dav_url_hint"
android:inputType="textUri"
android:background="@drawable/textview_background"/>
@@ -75,7 +75,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:textStyle="bold"
android:text="@string/settings_storage_dav_user"/>
@@ -83,7 +83,7 @@
android:id="@+id/settings_data_webdav_user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:inputType="textEmailAddress"
android:background="@drawable/textview_background"/>
@@ -91,17 +91,23 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:textStyle="bold"
android:text="@string/settings_storage_dav_pass"/>
<EditText
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/settings_data_webdav_pass"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:inputType="textPassword"
android:background="@drawable/textview_background"/>
android:background="@drawable/textview_background"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_indicator"
@@ -109,12 +115,103 @@
android:layout_height="wrap_content"
android:indeterminate="true"
app:indicatorColor="@color/accent"
android:layout_margin="20dp"
android:layout_marginTop="20dp"
android:visibility="invisible"/>
</RadioGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/settings_signature" />
<EditText
android:id="@+id/settings_signature"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:inputType="textEmailAddress"
android:background="@drawable/textview_background"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="5dp"
android:text="@string/settings_signature_desc"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="20dp">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/settings_no_breastfeeding" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_no_breastfeeding"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_weight="1" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="5dp"
android:text="@string/settings_no_breastfeeding_desc"/>
<!-- 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
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="horizontal">
<Button
@@ -122,7 +219,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@android:string/cancel"/>
@@ -137,7 +234,6 @@
android:text="@android:string/ok"/>
</LinearLayout>
</RadioGroup>
</LinearLayout>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingHorizontal="15dp">
<ImageView
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:src="@drawable/ic_back"
app:tint="@color/grey"/>
<TextView
android:id="@+id/statistics_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/statistics_title"
android:textSize="22sp"
android:gravity="center"/>
<Spinner
android:id="@+id/period_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="80dp"/>
</LinearLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:tabMode="scrollable"
app:tabGravity="start"
app:tabTextColor="@color/grey"
app:tabSelectedTextColor="@color/accent"
app:tabIndicatorColor="@color/accent"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center">
<NumberPicker
android:id="@+id/breastfeeding_duration_picker"
android:layout_width="100dp"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="@string/measurement_unit_time_minutes"/>
</LinearLayout>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/breastfeeding_side_emoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="60sp"/>
<TextView
android:id="@+id/breastfeeding_timer_display"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="48sp"
android:textColor="@color/accent"
android:fontFamily="monospace"
android:text="00:00"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="14sp"
android:textColor="@color/grey"
android:text="@string/breastfeeding_timer_hint"/>
</LinearLayout>

View File

@@ -1,26 +1,29 @@
<?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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">
android:paddingTop="20dp"
android:paddingBottom="10dp"
android:paddingHorizontal="20dp">
<TextView
android:id="@+id/dialog_event_detail_type_emoji"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textSize="60dp"
android:textSize="60sp"
android:text="@string/event_diaperchange_pee_type" />
<TextView
android:id="@+id/dialog_event_detail_type_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginTop="12dp"
android:gravity="center_horizontal"
android:textColor="@color/accent"
android:textSize="24dp"
android:textSize="32sp"
android:text="@string/event_diaperchange_pee_desc" />
<TextView
@@ -30,18 +33,20 @@
android:layout_marginTop="20dp"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:drawableEnd="@drawable/ic_edit"
android:drawablePadding="10dp"
android:drawableTint="@color/accent"
android:textSize="16sp"
android:textStyle="bold"
android:text="@string/dialog_event_detail_datetime_icon"/>
android:text="@string/dialog_event_detail_datetime_icon"
app:drawableEndCompat="@drawable/ic_edit" />
<TextView
android:id="@+id/dialog_event_detail_type_quantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Quantity"/>
android:textSize="28sp"
android:text="@string/dialog_event_detail_quantity"/>
<ScrollView
android:layout_width="match_parent"
@@ -53,7 +58,45 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="italic"
android:text="Notes"/>
android:textSize="20sp"
android:text="@string/dialog_event_detail_notes"/>
</ScrollView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:id="@+id/dialog_event_detail_type_signature"
android:layout_marginBottom="5dp"
android:visibility="gone"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/dialog_event_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:textSize="12sp"
android:text="" />
<Space
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent" />
<TextView
android:id="@+id/dialog_event_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textSize="12sp"
android:text="" />
</LinearLayout>
</LinearLayout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,6 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:text="g"/>
</LinearLayout>

View File

@@ -14,6 +14,6 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:text="ml"/>
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<Spinner
android:id="@+id/dialog_puke_value"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"/>
</LinearLayout>

View File

@@ -11,7 +11,7 @@
android:id="@+id/type"
android:layout_width="90dp"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingStart="10dp"
android:textSize="28sp"
android:lines="1"
android:maxLines="1"
@@ -25,7 +25,7 @@
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/accent"
android:text="Description"/>
android:text="@string/row_luna_event_description"/>
<TextView
android:id="@+id/quantity"
@@ -35,7 +35,7 @@
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:gravity="center_horizontal"
android:text="Qty"/>
android:text="@string/row_luna_event_quantity"/>
<TextView
android:id="@+id/time"
@@ -46,6 +46,6 @@
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="2"
android:text="Time"/>
android:text="@string/row_luna_event_time"/>
</LinearLayout>

View File

@@ -31,7 +31,7 @@
android:maxLines="2"
android:gravity="center_horizontal"
android:textColor="@color/accent"
android:text="Description"/>
android:text="@string/row_luna_event_description"/>
<TextView
android:id="@+id/quantity"
@@ -41,7 +41,7 @@
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:gravity="center_horizontal"
android:text="Qty"/>
android:text="@string/dialog_event_detail_quantity"/>
<TextView
android:id="@+id/time"
@@ -52,7 +52,7 @@
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="2"
android:text="Time"/>
android:text="@string/row_luna_event_time"/>
</LinearLayout>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center">
<NumberPicker
android:id="@+id/sleep_duration_picker"
android:layout_width="100dp"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="@string/measurement_unit_time_minutes"/>
</LinearLayout>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/sleep_emoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/event_sleep_type"
android:textSize="60sp"/>
<TextView
android:id="@+id/sleep_timer_display"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="48sp"
android:textColor="@color/accent"
android:fontFamily="monospace"
android:text="00:00"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="14sp"
android:textColor="@color/grey"
android:text="@string/sleep_timer_hint"/>
</LinearLayout>

View File

@@ -0,0 +1,217 @@
<resources>
<string name="app_name">LunaTracker</string>
<string name="title">🌜 LunaTracker 🌛</string>
<string name="logbook">Ereignisprotokoll</string>
<string name="log_bottle_dialog_title">Fläschchen</string>
<string name="log_bottle_dialog_description">Trinkmenge eingeben</string>
<string name="log_weight_dialog_title">Gewicht</string>
<string name="log_weight_dialog_description">Gewicht eingeben</string>
<string name="log_temperature_dialog_title">Temperatur</string>
<string name="log_temperature_dialog_description">Temperatur eingeben</string>
<string name="event_bottle_desc">Fläschchen</string>
<string name="event_food_desc">Essen</string>
<string name="event_scale_desc">Gewicht</string>
<string name="event_breastfeeding_left_desc">Stillen (links)</string>
<string name="event_breastfeeding_both_desc">Stillen</string>
<string name="event_breastfeeding_right_desc">Stillen (rechts)</string>
<string name="event_diaperchange_poo_desc">Windelwechsel (Stuhl)</string>
<string name="event_diaperchange_pee_desc">Windelwechsel (Urin)</string>
<string name="event_medicine_desc">Medikament</string>
<string name="event_enema_desc">Einlauf</string>
<string name="event_note_desc">Notiz</string>
<string name="event_temperature_desc">Temperatur</string>
<string name="event_colic_desc">Blähungskolik</string>
<string name="event_unknown_desc"></string>
<string name="overflow_event_scale">⚖️ Gewicht</string>
<string name="overflow_event_medicine">💊 Medikament</string>
<string name="overflow_event_enema">🪠 Einlauf</string>
<string name="overflow_event_note">📝 Notiz</string>
<string name="overflow_event_temperature">🌡️ Temperatur</string>
<string name="overflow_event_colic">💨 Blähungskolik</string>
<string name="toast_event_added">Ereignis gespeichert</string>
<string name="toast_logbook_saved">Logbuch gespeichert</string>
<string name="toast_event_add_error">Ereignis konnte nicht protokolliert werden</string>
<string name="toast_integer_error">Ungültiger Wert. Bitte eine Ganzzahl eingeben.</string>
<string name="now">jetzt</string>
<string name="hour_ago">Std.</string>
<string name="hours_ago">Std.</string>
<string name="minute_ago">Min.</string>
<string name="minutes_ago">Min.</string>
<string name="no_connection">Keine Verbindung</string>
<string name="no_connection_explain">WebDAV-Dienst nicht erreichbar</string>
<string name="no_connection_go_to_settings">Einstellungen</string>
<string name="no_connection_retry">Erneut versuchen</string>
<string name="settings_title">Einstellungen</string>
<string name="settings_no_breastfeeding">Kein Stillen</string>
<string name="settings_storage">Speicherort für Daten auswählen</string>
<string name="settings_storage_local">Auf dem Gerät</string>
<string name="settings_storage_local_desc">Datenschutzfreundlichste Lösung: Deine Daten verlassen dein Gerät nicht</string>
<string name="settings_storage_dav">Auf einem WebDAV-Server</string>
<string name="settings_storage_dav_desc">Du kannst jeden WebDAV-Dienst (z.B. Nextcloud, ownCloud, Dropbox, …) verwenden, um die Daten zu speichern. Auf diese Weise kannst du sie zwischen mehreren Geräten synchronisieren (z.B. dem vom Vater, der Mutter, der Großmutter, …). Dazu benötigst du die WebDAV-URL. Du findest diese in der Dokumentation deines Anbieters (z.B. bei Nextcloud im Bereich Einstellungen der Dateien-App).</string>
<string name="settings_storage_dav_url">WebDAV URL</string>
<string name="settings_storage_dav_url_hint">https://</string>
<string name="settings_storage_dav_user">Benutzername</string>
<string name="settings_storage_dav_pass">Passwort</string>
<string name="settings_network_error">Server nicht erreichbar: </string>
<string name="settings_webdav_error_denied">Falscher WebDAV-Benutzer oder falsches Passwort</string>
<string name="settings_webdav_error_server_offline">WebDAV-Server ist derzeit nicht verfügbar</string>
<string name="settings_webdav_error_generic">Fehler beim Zugriff auf WebDAV:</string>
<string name="settings_webdav_creation_error_generic">Eine Datei konnte auf dem WebDAV-Server nicht gespeichert werden:</string>
<string name="settings_webdav_creation_ok">Erfolgreich mit dem WebDAV-Server verbunden</string>
<string name="settings_json_error">Es befindet sich eine Speicherdatei auf dem Server, aber sie ist beschädigt oder unlesbar. Bitte lösche die Datei.</string>
<string name="settings_generic_error">Fehler: </string>
<string name="settings_webdav_upload_error">Fehler beim Hochladen des lokalen Logbuchs %1$s zu WebDAV: %2$s</string>
<string name="trim_logbook_dialog_title">Dein Logbuch ist ziemlich groß!</string>
<string name="trim_logbook_dialog_message_local">Deine Logbuchdatei wird sehr groß. Wir empfehlen, die ältesten Einträge zu entfernen, um Abstürze zu vermeiden.</string>
<string name="trim_logbook_dialog_message_dav">Deine Logbuchdatei wird sehr groß. Wir empfehlen, die ältesten Einträge (durch Entfernen) zu bereinigen, um Abstürze zu vermeiden. Wenn du alle Einträge behalten möchtest, sichere bitte die Datei "lunatracker_logbook.json" auf dem WebDAVServer oder benenne sie um, um ein neues Logbuch zu beginnen und das alte zu behalten.</string>
<string name="trim_logbook_dialog_button_ok">Jetzt bereinigen</string>
<string name="trim_logbook_dialog_button_cancel">Später erinnern</string>
<string name="log_notes_dialog_description">Notizen:</string>
<string name="log_medicine_dialog_description">Medikamentenname, Menge, Art, Notizen, …:</string>
<string name="log_notes_dialog_qty_hint">Menge (optional)</string>
<string name="log_notes_dialog_note_hint">Notiz eingeben</string>
<string name="dialog_event_detail_title">Ereignisdetails</string>
<string name="dialog_event_detail_close_button">OK</string>
<string name="dialog_event_detail_delete_button">Löschen</string>
<string name="dialog_add_logbook_title">Logbuch hinzufügen</string>
<string name="dialog_add_logbook_logbookname">👶 Logbuchname</string>
<string name="dialog_add_logbook_message">Gib einen Namen ein, um dieses Logbuch zu bezeichnen. Dieser Name erscheint oben auf dem Bildschirm und wird, falls du WebDAV verwendest, auch im Dateinamen der gespeicherten Datei enthalten sein.</string>
<string name="dialog_add_logbook_message_intro">Willkommen! Um diese App zu benutzen, musst du mindestens ein Logbuch erstellen. Am besten benennst du es nach dem Namen deines Kindes.</string>
<string name="default_logbook_name">👶 Mein erstes Logbuch</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>

View File

@@ -0,0 +1,185 @@
<resources>
<string name="app_name">LunaTracker</string>
<string name="title">🌜 LunaTracker 🌛</string>
<string name="logbook">Entrées enregistrées</string>
<string name="log_bottle_dialog_title">Biberon</string>
<string name="log_bottle_dialog_description">Renseignez la quantité contenue dans le biberon</string>
<string name="log_weight_dialog_title">Poids</string>
<string name="log_weight_dialog_description">Renseignez le poids</string>
<string name="log_temperature_dialog_title">Température</string>
<string name="log_temperature_dialog_description">Renseignez la Température</string>
<string name="event_bottle_desc">Biberon</string>
<string name="event_food_desc">Nourriture</string>
<string name="event_scale_desc">Poids</string>
<string name="event_breastfeeding_left_desc">Allaitement (sein gauche)</string>
<string name="event_breastfeeding_both_desc">Allaitement</string>
<string name="event_breastfeeding_right_desc">Allaitement (sein droit)</string>
<string name="event_diaperchange_poo_desc">Change (selle)</string>
<string name="event_diaperchange_pee_desc">Change (urine)</string>
<string name="event_medicine_desc">Médicament</string>
<string name="event_enema_desc">Lavement</string>
<string name="event_note_desc">Note</string>
<string name="event_temperature_desc">Température</string>
<string name="event_colic_desc">Colique gazeuse</string>
<string name="event_unknown_desc"></string>
<string name="overflow_event_scale">⚖️ Poids</string>
<string name="overflow_event_medicine">💊 Médicament</string>
<string name="overflow_event_enema">🪠 Lavement</string>
<string name="overflow_event_note">📝 Note</string>
<string name="overflow_event_temperature">🌡️ Température</string>
<string name="overflow_event_colic">💨 Colique gazeuse</string>
<string name="toast_event_added">Entrée ajoutée</string>
<string name="toast_logbook_saved">Journal ajouté</string>
<string name="toast_event_add_error">Impossible d\'enregistrer cette entrée</string>
<string name="toast_integer_error">Valeur invalide. Renseignez un nombre.</string>
<string name="now">maintenant </string>
<string name="hour_ago">heure</string>
<string name="hours_ago">heures</string>
<string name="minute_ago">min</string>
<string name="minutes_ago">mins</string>
<string name="no_connection">Pas de connexion internet</string>
<string name="no_connection_explain">Impossible de se connecter au service WebDAV</string>
<string name="no_connection_go_to_settings">Paramètres</string>
<string name="no_connection_retry">Réessayer</string>
<string name="settings_title">Paramètres</string>
<string name="settings_storage">Choisir le lieu de stockage des données</string>
<string name="settings_storage_local">Sur l\'appareil\'</string>
<string name="settings_storage_local_desc">La solution la plus respectueuse de la vie privée : les données ne quittent pas l\'appareil</string>
<string name="settings_storage_dav">Sur un serveur WebDAV</string>
<string name="settings_storage_dav_desc">Vous pouvez utiliser n\'importe quel service WebDAV (par exemple Nextcloud, Owncloud, Dropbox, Box...) pour sauvegarder les données. De cette façon, vous pouvez les synchroniser entre plusieurs appareils (Ex : celui de papa, celui de maman, celui de grand-mère...). Vous aurez besoin de l\'adresse WebDAV, que vous trouverez dans la documentation de votre fournisseur (par exemple, pour Nextcloud web, vous la trouverez dans les paramètres de l\'application Fichiers).</string>
<string name="settings_storage_dav_url">URL WebDAV</string>
<string name="settings_storage_dav_url_hint">https://</string>
<string name="settings_storage_dav_user">Nom d\'utilisateur</string>
<string name="settings_storage_dav_pass">Mot de passe</string>
<string name="settings_network_error">Impossible d\'accéder au serveur: </string>
<string name="settings_webdav_error_denied">Nom d\'utilisateur ou mot de passe incorrect pour le service WebDAV</string>
<string name="settings_webdav_error_server_offline">Impossible d\'accéder au serveur WebDAV</string>
<string name="settings_webdav_error_generic">Une erreur est survenue en essayant d\'accéder au serveur WebDAV:</string>
<string name="settings_webdav_creation_error_generic">Impossible de sauvegarder un fichier sur le serveur WebDAV:</string>
<string name="settings_webdav_creation_ok">Connexion réussie avec le serveur WebDAV</string>
<string name="settings_json_error">Il y a un fichier sur le serveur WebDAV, toutefois il est corronpu ou illisible. Merci de le supprimer et réessayer</string>
<string name="settings_generic_error">Erreur: </string>
<string name="settings_webdav_upload_error">Une erreur est survenue en téléversant le journal local %1$s sur %2$s</string>
<string name="trim_logbook_dialog_title">Votre journal grossit !</string>
<string name="trim_logbook_dialog_message_local">Le fichier de votre journal a beaucoup grossi. Nous recommandons de supprimer les entrées les plus vieilles pour éviter des crashs de l\'application.</string>
<string name="trim_logbook_dialog_message_dav">Le fichier de votre journal a beaucoup grossi. Nous recommandons de supprimer les entrées les plus vieilles pour éviter des crashs de l\'application. Si vous voulez garder un historique, vous pouvez sauvegarder le fichier "lunatracker_logbook.json" ou le renommer pour créer un nouveau journal.</string>
<string name="trim_logbook_dialog_button_ok">Supprimer les vieilles entrées maintenant</string>
<string name="trim_logbook_dialog_button_cancel">Me rappeller plus tard</string>
<string name="log_notes_dialog_description">Notes:</string>
<string name="log_medicine_dialog_description">nom du médicament, quantité, type, notes …:</string>
<string name="log_notes_dialog_qty_hint">Quantité (ou vide)</string>
<string name="log_notes_dialog_note_hint">Notes ...</string>
<string name="dialog_event_detail_title">Détails de l\'entrée</string>
<string name="dialog_event_detail_close_button">OK</string>
<string name="dialog_event_detail_delete_button">Supprimer</string>
<string name="dialog_add_logbook_title">Ajouter un journal</string>
<string name="dialog_add_logbook_logbookname">👶 Nom du journal</string>
<string name="dialog_add_logbook_message">Renseignez un nom pour identifier ce journal. Ce nom apparaîtra en haut de l\'écran et, si vous utilisez WebDAV, il figurera également dans le nom du fichier enreigstré.</string>
<string name="dialog_add_logbook_message_intro">Bienvenue ! Pour utiliser cette application, vous devez créer au moins un carnet de bord. Vous voudrez probablement l\'appeler avec le nom de votre enfant.</string>
<string name="default_logbook_name">👶 Mon premier carnet de bord</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>

View File

@@ -1,7 +1,6 @@
<resources>
<string name="app_name">LunaTracker</string>
<string name="title">🌜 LunaTracker 🌛</string>
<string name="log_an_event">Logga un evento:</string>
<string name="logbook">Diario di bordo</string>
<string name="log_bottle_dialog_title">Biberon</string>
@@ -94,4 +93,93 @@
<string name="default_logbook_name">👶 Il mio primo 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>

View File

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

View File

@@ -1,7 +1,6 @@
<resources>
<string name="app_name">LunaTracker</string>
<string name="title">🌜 LunaTracker 🌛</string>
<string name="log_an_event">Log an event:</string>
<string name="logbook">Logged events</string>
<string name="log_bottle_dialog_title">Baby bottle</string>
@@ -13,6 +12,9 @@
<string name="log_temperature_dialog_title">Temperature</string>
<string name="log_temperature_dialog_description">Insert the temperature</string>
<string name="log_puke_dialog_title">Puke</string>
<string name="log_puke_dialog_description">Select the amount</string>
<string name="event_bottle_type" translatable="false">🍼</string>
<string name="event_food_type" translatable="false">🥣</string>
<string name="event_scale_type" translatable="false">⚖️</string>
@@ -26,6 +28,9 @@
<string name="event_note_type" translatable="false">📝</string>
<string name="event_temperature_type" translatable="false">🌡️</string>
<string name="event_colic_type" translatable="false">💨</string>
<string name="event_puke_type" translatable="false">🤮</string>
<string name="event_bath_type" translatable="false">🛁</string>
<string name="event_sleep_type" translatable="false">🌙</string>
<string name="event_unknown_type" translatable="false">\?</string>
<string name="event_bottle_desc">Baby bottle</string>
@@ -41,6 +46,9 @@
<string name="event_note_desc">Note</string>
<string name="event_temperature_desc">Temperature</string>
<string name="event_colic_desc">Gaseous colic</string>
<string name="event_puke_desc">Puke</string>
<string name="event_bath_desc">Bath</string>
<string name="event_sleep_desc">Sleep</string>
<string name="event_unknown_desc"></string>
<string name="overflow_event_scale">⚖️ Weight</string>
@@ -49,6 +57,8 @@
<string name="overflow_event_note">📝 Note</string>
<string name="overflow_event_temperature">🌡️ Temperature</string>
<string name="overflow_event_colic">💨 Gaseous colic</string>
<string name="overflow_event_puke">🤮 Puke</string>
<string name="overflow_event_bath">🛁 Bath</string>
<string name="toast_event_added">Event logged</string>
<string name="toast_logbook_saved">Logbook saved</string>
@@ -56,10 +66,20 @@
<string name="toast_integer_error">Invalid value. Insert an integer.</string>
<string name="now">now</string>
<string name="hour_ago">hour</string>
<string name="hours_ago">hours</string>
<string name="second_ago">sec</string>
<string name="seconds_ago">secs</string>
<string name="minute_ago">min</string>
<string name="minutes_ago">mins</string>
<string name="hour_ago">hour</string>
<string name="hours_ago">hours</string>
<string name="day_ago">day</string>
<string name="days_ago">days</string>
<string name="year_ago">year</string>
<string name="years_ago">years</string>
<string name="amount_little">Little</string>
<string name="amount_normal">Normal</string>
<string name="amount_plenty">Plenty</string>
<string name="no_connection">No connection</string>
<string name="no_connection_explain">Unable to reach WebDAV service</string>
@@ -67,12 +87,14 @@
<string name="no_connection_retry">Retry</string>
<string name="settings_title">Settings</string>
<string name="settings_signature">Signature</string>
<string name="settings_signature_desc">Attach a signature to each event you create and for others to see. Useful if multiple people add events.</string>
<string name="settings_storage">Choose where to save data</string>
<string name="settings_storage_local">On device</string>
<string name="settings_storage_local_desc">Most privacy-friendly solution: data doesn\'t leave your device</string>
<string name="settings_storage_dav">On a WebDAV server</string>
<string name="settings_storage_dav_desc">You can use any WebDAV service (i.e. Nextcloud, Owncloud, Dropbox, Box…) to save the data. In this way you can syncronize it between more devices, i.e. the dad\'s, the mom\'s, the grandma\'s… You will need the WebDAV url, you can find it in your provider\'s documentation (i.e. in Nextcloud web is in the Files app settings)</string>
<string name="settings_storage_dav_url">WebDAV url</string>
<string name="settings_storage_dav_desc">You can use any WebDAV service (i.e. Nextcloud, Owncloud, Dropbox, Box…) to save the data. In this way, you can syncronize it between more devices, i.e., the dad\'s, the mom\'s, the grandma\'s… You will need the WebDAV URL, you can find it in your provider\'s documentation (i.e. in Nextcloud web is in the Files app settings)</string>
<string name="settings_storage_dav_url">WebDAV URL</string>
<string name="settings_storage_dav_url_hint">https://</string>
<string name="settings_storage_dav_user">Username</string>
<string name="settings_storage_dav_pass">Password</string>
@@ -81,14 +103,16 @@
<string name="settings_webdav_error_server_offline">WebDAV server is currently unavailable</string>
<string name="settings_webdav_error_generic">Error while trying to access WebDAV:</string>
<string name="settings_webdav_creation_error_generic">Unable to save a file on the WebDAV server:</string>
<string name="settings_webdav_creation_ok">Successfully connected with WebDAV server</string>
<string name="settings_json_error">There\'s a save file on the server, but is corrupted or unreadable. Please delete it </string>
<string name="settings_webdav_creation_ok">Successfully connected with the WebDAV server</string>
<string name="settings_no_breastfeeding">No Breastfeeding</string>
<string name="settings_no_breastfeeding_desc">Hide the Breastfeeding buttons for when they are not needed.</string>
<string name="settings_json_error">There\'s a save file on the server, but it is corrupted or unreadable. Please delete it </string>
<string name="settings_generic_error">Error: </string>
<string name="settings_webdav_upload_error">Error while uploading local logbook %1$s to webdav: %2$s</string>
<string name="trim_logbook_dialog_title">Your logbook is pretty big!</string>
<string name="trim_logbook_dialog_message_local">Your logbook file is growing a lot. We suggest trimming the oldest events to avoid crashes.</string>
<string name="trim_logbook_dialog_message_dav">Your logbook file is growing a lot. We suggest trimming the oldest events to avoid crashes. If you want to preserve all the events, please backup the "lunatracker_logbook.json" file on the WebDAV server or rename it to start a new logbook keeping the old one.</string>
<string name="trim_logbook_dialog_message_dav">Your logbook file is growing a lot. We suggest trimming the oldest events to avoid crashes. If you want to preserve all the events, please back up the "lunatracker_logbook.json" file on the WebDAV server or rename it to start a new logbook, keeping the old one.</string>
<string name="trim_logbook_dialog_button_ok">Trim it now</string>
<string name="trim_logbook_dialog_button_cancel">Remind me later</string>
@@ -106,17 +130,113 @@
<string name="measurement_unit_temperature_base_imperial" translatable="false">°F</string>
<string name="measurement_unit_temperature_base_metric" translatable="false">°C</string>
<string name="row_luna_event_description">Description</string>
<string name="row_luna_event_quantity">Qty</string>
<string name="row_luna_event_time">Time</string>
<string name="dialog_event_detail_title">Event detail</string>
<string name="dialog_event_detail_datetime_icon" translatable="false">🕒 %s1</string>
<string name="dialog_event_detail_datetime_icon" translatable="false">🕒 %s</string>
<string name="dialog_event_detail_close_button">OK</string>
<string name="dialog_event_detail_delete_button">Delete</string>
<string name="dialog_event_detail_quantity">Quantity</string>
<string name="dialog_event_detail_notes">Notes</string>
<string name="dialog_event_detail_signature">by %s</string>
<string name="dialog_add_logbook_title">Add logbook</string>
<string name="dialog_add_logbook_logbookname">👶 Logbook name</string>
<string name="dialog_add_logbook_message">Write a name to identify this logbook. This name will appear on top of the screen and, if you use WebDAV, will be in the save file name as well.</string>
<string name="dialog_add_logbook_message_intro">Welcome! To use this app you need to create at least one logbook. You would probably want to call it with your child\'s name.</string>
<string name="dialog_add_logbook_message_intro">Welcome! To use this app, you need to create at least one logbook. You would probably want to call it with your child\'s name.</string>
<string name="default_logbook_name">👶 My first logbook</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>

View File

@@ -1,11 +1,12 @@
Parenting can be tough. You get home from the hospital, exhausted, with this little fragile unknown thingy that has no user manual and a single way to let you know something's not ok: crying.
You have to react fast, even if it's 4 AM and you have no idea why is crying.
You have to react fast, even if it's 4 AM and you have no idea of the reason behind the cries.
This app is meant to log all the relevant events (diaper change, breastfeeding, baby bottle feeding...), so you can always remember when has been done last time. It supports syncing the data between different devices (using your WebDAV server for the best privacy) so that when the baby needs attentions, the dad can step in leaving the mom sleeping peacefully.
This app is meant to log all the relevant events (diaper change, breastfeeding, baby bottle feeding, etc.), so you can always remember what was done last time and when. It supports syncing the data between different devices (using your WebDAV server for the best privacy) so that when the baby needs attention, the dad can step in, leaving the mom sleeping peacefully.
Dedicated to my daughter Luna.
A HUGE thanks to all our contributors. See https://git.ichibi.eu/penguin86/luna-tracker/src/branch/master/README.md
NOTE: the content on this app is for informational or educational purposes only and does not substitute professional medical advice or consultations with healthcare professionals.
NOTE: The content on this app is for informational or educational purposes only and does not substitute professional medical advice or consultations with healthcare professionals.
Feature graphic ("Baby and baby milk bottle. Baby feeding."): © Vyacheslav Argenberg / http://www.vascoplanet.com/, CC BY 4.0 <https://creativecommons.org/licenses/by/4.0>, via Wikimedia Commons

View File

@@ -0,0 +1 @@
Première version alpha : carnet de bord, synchronisation.

View File

@@ -0,0 +1,2 @@
Prise en charge de plusieurs enfants
Correction de l'interface sur les appareils avec une grande taille de police.

View File

@@ -0,0 +1,15 @@
Être parent n'est pas toujours facile. Vous rentrez de la maternité, épuisé, avec cette petite créature inconnue et fragile qui n'a pas de mode d'emploi et une seule façon de vous faire savoir que quelque chose ne va pas : les pleurs.
Vous devez réagir rapidement, même s'il est 4 heures du matin et que vous n'avez aucune idée de la raison de ses pleurs.
Cette application est destinée à enregistrer tous les événements importants (changement de couche, allaitement, biberon...), afin que vous puissiez toujours vous souvenir de ce qui a été fait la dernière fois (et quand).
Elle permet de synchroniser les données entre différents appareils (en utilisant votre serveur WebDAV pour une meilleure confidentialité). Ainsi, lorsque le bébé a besoin d'attention, le papa peut intervenir et laisser la maman dormir tranquillement.
Dédié à ma fille Luna.
Un grand merci à tous nos contributeurs. Voir https://git.ichibi.eu/penguin86/luna-tracker/src/branch/master/README.md
REMARQUE : le contenu de cette application est fourni à titre informatif ou éducatif uniquement et ne remplace pas les conseils médicaux professionnels ou les consultations avec des professionnels de la santé.
Crédit image ("Baby and baby milk bottle. Baby feeding."): © Vyacheslav Argenberg / http://www.vascoplanet.com/, CC BY 4.0 <https://creativecommons.org/licenses/by/4.0>, via Wikimedia Commons

View File

@@ -0,0 +1 @@
LunaTracker est une application de suivi des nouveau-nés.

View File

@@ -0,0 +1 @@
🌜 LunaTracker 🌛

View File

@@ -6,6 +6,7 @@ Quest'app è pensata per memorizzare tutti gli eventi del bambino (cambio di pan
Dedicato a mia figlia Luna.
Un grandissimo ringraziamento a tutti i contributors! Vedi https://git.ichibi.eu/penguin86/luna-tracker/src/branch/master/README.md
NOTA: il contenuto di quest'app ha solo scopo informativo o didattico e non sostituisce il consulto medico professionale o le consulenze con operatori sanitari.
Feature graphic ("Baby and baby milk bottle. Baby feeding."): © Vyacheslav Argenberg / http://www.vascoplanet.com/, CC BY 4.0 <https://creativecommons.org/licenses/by/4.0>, via Wikimedia Commons

View File

@@ -1,5 +1,5 @@
[versions]
agp = "8.7.2"
agp = "8.13.0"
kotlin = "2.0.0"
coreKtx = "1.10.1"
junit = "4.13.2"

View File

@@ -1,6 +1,6 @@
#Sat Nov 02 10:58:51 CET 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists