14 Commits

Author SHA1 Message Date
3e8af97757 Add configurable buttons, separate settings screens and backup activity
- Add ButtonConfigActivity for customizing main screen buttons with
  drag-and-drop reordering and individual size options (S/M/L)
- Move storage settings to separate StorageSettingsActivity
- Move signature setting to storage settings (relevant for WebDAV sync)
- Move data backup to separate BackupActivity with export/import
- Make "more" overflow button configurable in size
- Simplify SettingsActivity to 3 navigation buttons
- Add logbook rename/delete functionality
- Improve S/M/L button contrast with visible borders
2026-01-17 21:37:11 +01:00
6a995d6561 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-17 21:37:05 +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: #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
59 changed files with 5269 additions and 564 deletions

View File

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

View File

@@ -19,6 +19,10 @@ android {
} }
buildTypes { buildTypes {
debug {
applicationIdSuffix = ".theo"
resValue("string", "app_name", "Theotracker")
}
release { release {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
@@ -52,6 +56,7 @@ dependencies {
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.recyclerview) implementation(libs.androidx.recyclerview)
implementation("com.github.thegrizzlylabs:sardine-android:v0.9") implementation("com.github.thegrizzlylabs:sardine-android:v0.9")
implementation("com.google.android.flexbox:flexbox:3.0.0")
implementation(libs.material) implementation(libs.material)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View File

@@ -30,6 +30,22 @@
android:name=".SettingsActivity" android:name=".SettingsActivity"
android:label="@string/settings_title" android:label="@string/settings_title"
android:theme="@style/Theme.LunaTracker"/> android:theme="@style/Theme.LunaTracker"/>
<activity
android:name=".StatisticsActivity"
android:label="@string/statistics_title"
android:theme="@style/Theme.LunaTracker"/>
<activity
android:name=".ButtonConfigActivity"
android:label="@string/button_config_title"
android:theme="@style/Theme.LunaTracker"/>
<activity
android:name=".StorageSettingsActivity"
android:label="@string/storage_settings_title"
android:theme="@style/Theme.LunaTracker"/>
<activity
android:name=".BackupActivity"
android:label="@string/backup_title"
android:theme="@style/Theme.LunaTracker"/>
</application> </application>
</manifest> </manifest>

View File

@@ -0,0 +1,177 @@
package it.danieleverducci.lunatracker
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.progressindicator.LinearProgressIndicator
import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import org.json.JSONArray
import org.json.JSONObject
/**
* Activity for backup functionality (export/import logbooks).
*/
class BackupActivity : AppCompatActivity() {
private lateinit var progressIndicator: LinearProgressIndicator
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)
setContentView(R.layout.activity_backup)
progressIndicator = findViewById(R.id.progress_indicator)
findViewById<View>(R.id.backup_export).setOnClickListener {
startExport()
}
findViewById<View>(R.id.backup_import).setOnClickListener {
startImport()
}
findViewById<View>(R.id.backup_close).setOnClickListener {
finish()
}
}
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 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,71 @@
package it.danieleverducci.lunatracker
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import it.danieleverducci.lunatracker.adapters.ButtonConfigAdapter
import it.danieleverducci.lunatracker.adapters.ButtonConfigItemTouchHelperCallback
import it.danieleverducci.lunatracker.repository.ButtonConfigRepository
/**
* Activity for configuring which buttons appear on the main screen
* and in what order.
*/
class ButtonConfigActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: ButtonConfigAdapter
private lateinit var itemTouchHelper: ItemTouchHelper
private lateinit var repository: ButtonConfigRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_button_config)
repository = ButtonConfigRepository(this)
setupRecyclerView()
setupButtons()
}
private fun setupRecyclerView() {
recyclerView = findViewById(R.id.button_list)
recyclerView.layoutManager = LinearLayoutManager(this)
// Load current configuration
val configs = repository.loadConfigs().toMutableList()
adapter = ButtonConfigAdapter(configs) { viewHolder ->
itemTouchHelper.startDrag(viewHolder)
}
recyclerView.adapter = adapter
// Setup drag-and-drop
val callback = ButtonConfigItemTouchHelperCallback(adapter)
itemTouchHelper = ItemTouchHelper(callback)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
private fun setupButtons() {
findViewById<Button>(R.id.btn_save).setOnClickListener {
saveConfiguration()
}
findViewById<Button>(R.id.btn_cancel).setOnClickListener {
finish()
}
}
private fun saveConfiguration() {
val configs = adapter.getConfigs()
repository.saveConfigs(configs)
Toast.makeText(this, R.string.button_config_saved, Toast.LENGTH_SHORT).show()
finish()
}
}

View File

@@ -4,12 +4,14 @@ import android.app.DatePickerDialog
import android.app.TimePickerDialog import android.app.TimePickerDialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.EditText import android.widget.EditText
@@ -20,44 +22,50 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.flexbox.FlexboxLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import com.thegrizzlylabs.sardineandroid.impl.SardineException import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.adapters.DaySeparatorDecoration
import it.danieleverducci.lunatracker.adapters.LunaEventRecyclerAdapter import it.danieleverducci.lunatracker.adapters.LunaEventRecyclerAdapter
import it.danieleverducci.lunatracker.entities.ButtonConfig
import it.danieleverducci.lunatracker.entities.Logbook import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.repository.ButtonConfigRepository
import it.danieleverducci.lunatracker.repository.FileLogbookRepository import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.LogbookLoadedListener import it.danieleverducci.lunatracker.repository.LogbookLoadedListener
import it.danieleverducci.lunatracker.repository.LogbookRepository import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.LogbookSavedListener import it.danieleverducci.lunatracker.repository.LogbookSavedListener
import it.danieleverducci.lunatracker.repository.LogbookRenamedListener
import it.danieleverducci.lunatracker.repository.LogbookDeletedListener
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import kotlinx.coroutines.Runnable import kotlinx.coroutines.Runnable
import okio.IOException import okio.IOException
import org.json.JSONException import org.json.JSONException
import utils.DateUtils import utils.DateUtils
import utils.NumericUtils import utils.NumericUtils
import java.text.DateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
companion object { companion object {
val TAG = "MainActivity" const val TAG = "MainActivity"
val UPDATE_EVERY_SECS: Long = 30 const val UPDATE_EVERY_SECS: Long = 30
val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false const val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false
} }
var logbook: Logbook? = null var logbook: Logbook? = null
var pauseLogbookUpdate = false var pauseLogbookUpdate = false
lateinit var progressIndicator: LinearProgressIndicator lateinit var progressIndicator: LinearProgressIndicator
lateinit var buttonsContainer: ViewGroup lateinit var buttonsContainer: FlexboxLayout
lateinit var recyclerView: RecyclerView lateinit var recyclerView: RecyclerView
lateinit var handler: Handler lateinit var handler: Handler
var signature = ""
var savingEvent = false var savingEvent = false
val updateListRunnable: Runnable = Runnable { val updateListRunnable: Runnable = Runnable {
if (logbook != null && !pauseLogbookUpdate) if (logbook != null && !pauseLogbookUpdate)
@@ -67,6 +75,19 @@ class MainActivity : AppCompatActivity() {
var logbookRepo: LogbookRepository? = null var logbookRepo: LogbookRepository? = null
var showingOverflowPopupWindow = false var showingOverflowPopupWindow = false
// Breastfeeding timer state
var bfTimerStartTime: Long = 0
var bfTimerType: String? = null
var bfTimerDialog: AlertDialog? = null
var bfTimerHandler: Handler? = null
var bfTimerRunnable: Runnable? = null
// Sleep timer state
var sleepTimerStartTime: Long = 0
var sleepTimerDialog: AlertDialog? = null
var sleepTimerHandler: Handler? = null
var sleepTimerRunnable: Runnable? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -82,60 +103,39 @@ class MainActivity : AppCompatActivity() {
// Set listeners // Set listeners
findViewById<View>(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog(true) } findViewById<View>(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog(true) }
findViewById<View>(R.id.button_bottle).setOnClickListener { askBabyBottleContent() } findViewById<View>(R.id.logbooks_edit_button).setOnClickListener { showEditLogbookDialog() }
findViewById<View>(R.id.button_food).setOnClickListener { askNotes(LunaEvent(LunaEvent.TYPE_FOOD)) } findViewById<View>(R.id.button_no_connection_settings).setOnClickListener {
findViewById<View>(R.id.button_nipple_left).setOnClickListener { logEvent( showSettings()
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_change_poo).setOnClickListener { logEvent(
LunaEvent(
LunaEvent.TYPE_DIAPERCHANGE_POO
)
) }
findViewById<View>(R.id.button_change_pee).setOnClickListener { logEvent(
LunaEvent(
LunaEvent.TYPE_DIAPERCHANGE_PEE
)
) }
val moreButton = findViewById<View>(R.id.button_more)
moreButton.setOnClickListener {
showOverflowPopupWindow(moreButton)
} }
findViewById<View>(R.id.button_no_connection_settings).setOnClickListener({ findViewById<View>(R.id.button_settings).setOnClickListener {
showSettings() showSettings()
}) }
findViewById<View>(R.id.button_settings).setOnClickListener({ findViewById<View>(R.id.button_statistics).setOnClickListener {
showSettings() showStatistics()
}) }
findViewById<View>(R.id.button_no_connection_retry).setOnClickListener({ findViewById<View>(R.id.button_no_connection_retry).setOnClickListener {
// This may happen at start, when logbook is still null: better ask the logbook list // This may happen at start, when logbook is still null: better ask the logbook list
loadLogbookList() loadLogbookList()
}) }
findViewById<View>(R.id.button_sync).setOnClickListener({ findViewById<View>(R.id.button_sync).setOnClickListener {
loadLogbookList() loadLogbookList()
}) }
} }
private fun setListAdapter(items: ArrayList<LunaEvent>) { private fun setListAdapter(items: ArrayList<LunaEvent>) {
val adapter = LunaEventRecyclerAdapter(this, items) val adapter = LunaEventRecyclerAdapter(this, items)
adapter.onItemClickListener = object: LunaEventRecyclerAdapter.OnItemClickListener{ adapter.onItemClickListener = object: LunaEventRecyclerAdapter.OnItemClickListener {
override fun onItemClick(event: LunaEvent) { override fun onItemClick(event: LunaEvent) {
showEventDetailDialog(event, items) showEventDetailDialog(event, items)
} }
} }
recyclerView.adapter = adapter recyclerView.adapter = adapter
// Tages-Trenner hinzufügen
while (recyclerView.itemDecorationCount > 0) {
recyclerView.removeItemDecorationAt(0)
}
recyclerView.addItemDecoration(DaySeparatorDecoration(this, items))
} }
fun showSettings() { fun showSettings() {
@@ -143,6 +143,12 @@ class MainActivity : AppCompatActivity() {
startActivity(i) startActivity(i)
} }
fun showStatistics() {
val i = Intent(this, StatisticsActivity::class.java)
i.putExtra(StatisticsActivity.EXTRA_LOGBOOK_NAME, logbook?.name ?: "")
startActivity(i)
}
fun showLogbook() { fun showLogbook() {
// Show logbook // Show logbook
if (logbook == null) if (logbook == null)
@@ -169,15 +175,20 @@ class MainActivity : AppCompatActivity() {
logbookRepo = FileLogbookRepository() logbookRepo = FileLogbookRepository()
} }
val noBreastfeeding = settingsRepository.loadNoBreastfeeding() signature = settingsRepository.loadSignature()
findViewById<View>(R.id.layout_nipples).visibility = when (noBreastfeeding) {
true -> View.GONE // Render buttons based on configuration
false -> View.VISIBLE renderButtons()
}
// Update list dates // Update list dates
recyclerView.adapter?.notifyDataSetChanged() recyclerView.adapter?.notifyDataSetChanged()
// Check for ongoing breastfeeding timer
restoreBreastfeedingTimerIfNeeded()
// Check for ongoing sleep timer
restoreSleepTimerIfNeeded()
if (logbook != null) { if (logbook != null) {
// Already running: reload data for currently selected logbook // Already running: reload data for currently selected logbook
loadLogbook(logbook!!.name) loadLogbook(logbook!!.name)
@@ -190,6 +201,14 @@ class MainActivity : AppCompatActivity() {
override fun onStop() { override fun onStop() {
handler.removeCallbacks(updateListRunnable) handler.removeCallbacks(updateListRunnable)
// Clean up breastfeeding timer UI (state is preserved in SharedPreferences)
bfTimerRunnable?.let { bfTimerHandler?.removeCallbacks(it) }
bfTimerDialog?.dismiss()
// Clean up sleep timer UI (state is preserved in SharedPreferences)
sleepTimerRunnable?.let { sleepTimerHandler?.removeCallbacks(it) }
sleepTimerDialog?.dismiss()
super.onStop() super.onStop()
} }
@@ -260,6 +279,26 @@ class MainActivity : AppCompatActivity() {
alertDialog.show() alertDialog.show()
} }
fun askPukeValue() {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.puke_dialog, null)
d.setTitle(R.string.log_puke_dialog_title)
d.setMessage(R.string.log_puke_dialog_description)
d.setView(dialogView)
val spinner = dialogView.findViewById<Spinner>(R.id.dialog_puke_value)
spinner.adapter = ArrayAdapter.createFromResource(this, R.array.AmountLabels, android.R.layout.simple_spinner_dropdown_item)
spinner.setSelection(1)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val pos = spinner.selectedItemPosition
logEvent(LunaEvent(LunaEvent.TYPE_PUKE, pos))
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
val alertDialog = d.create()
alertDialog.show()
}
fun askNotes(lunaEvent: LunaEvent) { fun askNotes(lunaEvent: LunaEvent) {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_notes, null) val dialogView = layoutInflater.inflate(R.layout.dialog_notes, null)
@@ -289,6 +328,458 @@ class MainActivity : AppCompatActivity() {
alertDialog.show() alertDialog.show()
} }
fun startBreastfeedingTimer(eventType: String) {
// Check if timer already running
if (bfTimerType != null) {
Toast.makeText(this, R.string.breastfeeding_timer_already_running, Toast.LENGTH_SHORT).show()
return
}
// Save timer state
bfTimerStartTime = System.currentTimeMillis()
bfTimerType = eventType
saveBreastfeedingTimerState()
// Show timer dialog
showBreastfeedingTimerDialog(eventType)
}
fun showBreastfeedingTimerDialog(eventType: String) {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.breastfeeding_timer_dialog, null)
d.setTitle(R.string.breastfeeding_timer_title)
d.setView(dialogView)
d.setCancelable(false)
val timerDisplay = dialogView.findViewById<TextView>(R.id.breastfeeding_timer_display)
val sideEmoji = dialogView.findViewById<TextView>(R.id.breastfeeding_side_emoji)
sideEmoji.text = LunaEvent(eventType).getTypeEmoji(this)
// Set up timer updates
bfTimerHandler = Handler(mainLooper)
bfTimerRunnable = object : Runnable {
override fun run() {
val elapsed = (System.currentTimeMillis() - bfTimerStartTime) / 1000
val minutes = elapsed / 60
val seconds = elapsed % 60
timerDisplay.text = String.format("%02d:%02d", minutes, seconds)
bfTimerHandler?.postDelayed(this, 1000)
}
}
bfTimerHandler?.post(bfTimerRunnable!!)
d.setPositiveButton(R.string.breastfeeding_timer_stop) { _, _ ->
stopBreastfeedingTimer()
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
cancelBreastfeedingTimer()
dialogInterface.dismiss()
}
bfTimerDialog = d.create()
bfTimerDialog?.show()
}
fun stopBreastfeedingTimer() {
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
val durationMillis = System.currentTimeMillis() - bfTimerStartTime
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
val eventType = bfTimerType
clearBreastfeedingTimerState()
if (eventType != null) {
logEvent(LunaEvent(eventType, durationMinutes))
}
}
fun cancelBreastfeedingTimer() {
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
clearBreastfeedingTimerState()
}
fun askBreastfeedingDuration(eventType: String) {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
d.setTitle(R.string.breastfeeding_duration_title)
d.setMessage(R.string.breastfeeding_duration_description)
d.setView(dialogView)
val numberPicker = dialogView.findViewById<NumberPicker>(R.id.breastfeeding_duration_picker)
numberPicker.minValue = 1
numberPicker.maxValue = 60
numberPicker.value = 15 // Default 15 minutes
numberPicker.wrapSelectorWheel = false
d.setPositiveButton(android.R.string.ok) { _, _ ->
logEvent(LunaEvent(eventType, numberPicker.value))
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
dialogInterface.dismiss()
}
d.create().show()
}
fun saveBreastfeedingTimerState() {
LocalSettingsRepository(this).saveBreastfeedingTimer(bfTimerStartTime, bfTimerType ?: "")
}
fun clearBreastfeedingTimerState() {
bfTimerStartTime = 0
bfTimerType = null
bfTimerDialog = null
LocalSettingsRepository(this).clearBreastfeedingTimer()
}
fun restoreBreastfeedingTimerIfNeeded() {
val timerState = LocalSettingsRepository(this).loadBreastfeedingTimer()
if (timerState != null && timerState.first > 0 && timerState.second.isNotEmpty()) {
bfTimerStartTime = timerState.first
bfTimerType = timerState.second
showBreastfeedingTimerDialog(timerState.second)
}
}
// Sleep timer methods
fun startSleepTimer() {
// Check if timer already running
if (sleepTimerStartTime > 0) {
Toast.makeText(this, R.string.sleep_timer_already_running, Toast.LENGTH_SHORT).show()
return
}
// Save timer state
sleepTimerStartTime = System.currentTimeMillis()
saveSleepTimerState()
// Show timer dialog
showSleepTimerDialog()
}
fun showSleepTimerDialog() {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.sleep_timer_dialog, null)
d.setTitle(R.string.sleep_timer_title)
d.setView(dialogView)
d.setCancelable(false)
val timerDisplay = dialogView.findViewById<TextView>(R.id.sleep_timer_display)
// Set up timer updates
sleepTimerHandler = Handler(mainLooper)
sleepTimerRunnable = object : Runnable {
override fun run() {
val elapsed = (System.currentTimeMillis() - sleepTimerStartTime) / 1000
val hours = elapsed / 3600
val minutes = (elapsed % 3600) / 60
val seconds = elapsed % 60
timerDisplay.text = if (hours > 0) {
String.format("%d:%02d:%02d", hours, minutes, seconds)
} else {
String.format("%02d:%02d", minutes, seconds)
}
sleepTimerHandler?.postDelayed(this, 1000)
}
}
sleepTimerHandler?.post(sleepTimerRunnable!!)
d.setPositiveButton(R.string.sleep_timer_stop) { _, _ ->
stopSleepTimer()
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
cancelSleepTimer()
dialogInterface.dismiss()
}
sleepTimerDialog = d.create()
sleepTimerDialog?.show()
}
fun stopSleepTimer() {
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
val durationMillis = System.currentTimeMillis() - sleepTimerStartTime
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
clearSleepTimerState()
logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, durationMinutes))
}
fun cancelSleepTimer() {
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
clearSleepTimerState()
}
fun askSleepDuration() {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.sleep_duration_dialog, null)
d.setTitle(R.string.sleep_duration_title)
d.setMessage(R.string.sleep_duration_description)
d.setView(dialogView)
val numberPicker = dialogView.findViewById<NumberPicker>(R.id.sleep_duration_picker)
numberPicker.minValue = 1
numberPicker.maxValue = 180 // Up to 3 hours
numberPicker.value = 30 // Default 30 minutes
numberPicker.wrapSelectorWheel = false
d.setPositiveButton(android.R.string.ok) { _, _ ->
logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, numberPicker.value))
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
dialogInterface.dismiss()
}
d.create().show()
}
fun saveSleepTimerState() {
LocalSettingsRepository(this).saveSleepTimer(sleepTimerStartTime)
}
fun clearSleepTimerState() {
sleepTimerStartTime = 0
sleepTimerDialog = null
LocalSettingsRepository(this).clearSleepTimer()
}
fun restoreSleepTimerIfNeeded() {
val startTime = LocalSettingsRepository(this).loadSleepTimer()
if (startTime > 0) {
sleepTimerStartTime = startTime
showSleepTimerDialog()
}
}
/**
* Renders buttons dynamically based on the saved configuration.
*/
fun renderButtons() {
buttonsContainer.removeAllViews()
val buttonConfigRepo = ButtonConfigRepository(this)
val allConfigs = buttonConfigRepo.loadConfigs()
val visibleConfigs = allConfigs.filter { it.visible }.sortedBy { it.order }
val hiddenConfigs = allConfigs.filter { !it.visible }
val marginPx = resources.getDimensionPixelSize(R.dimen.button_margin)
for (config in visibleConfigs) {
// Get size-specific dimensions for this button
val (textSizeRes, paddingRes, minWidthRes) = when (config.size) {
ButtonConfigRepository.SIZE_SMALL -> Triple(
R.dimen.button_text_size_small,
R.dimen.button_padding_small,
R.dimen.button_min_width_small
)
ButtonConfigRepository.SIZE_LARGE -> Triple(
R.dimen.button_text_size_large,
R.dimen.button_padding_large,
R.dimen.button_min_width_large
)
else -> Triple(
R.dimen.button_text_size_medium,
R.dimen.button_padding_medium,
R.dimen.button_min_width_medium
)
}
val textSizePx = resources.getDimension(textSizeRes)
val paddingPx = resources.getDimensionPixelSize(paddingRes)
val minWidthPx = resources.getDimensionPixelSize(minWidthRes)
val button = TextView(this).apply {
text = getString(config.iconResId)
setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, textSizePx)
gravity = android.view.Gravity.CENTER
background = ContextCompat.getDrawable(this@MainActivity, R.drawable.button_background)
val lp = FlexboxLayout.LayoutParams(
FlexboxLayout.LayoutParams.WRAP_CONTENT,
FlexboxLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(marginPx, marginPx, marginPx, marginPx)
flexGrow = 1f
minWidth = minWidthPx
}
layoutParams = lp
setPadding(paddingPx, paddingPx, paddingPx, paddingPx)
// Set click listener based on button type
setOnClickListener { handleButtonClick(config.id) }
// Set long click listener for breastfeeding and sleep buttons
if (config.id in listOf(
ButtonConfig.ID_BREASTFEEDING_LEFT,
ButtonConfig.ID_BREASTFEEDING_BOTH,
ButtonConfig.ID_BREASTFEEDING_RIGHT
)) {
setOnLongClickListener {
askBreastfeedingDuration(ButtonConfig.getEventType(config.id))
true
}
} else if (config.id == ButtonConfig.ID_SLEEP) {
setOnLongClickListener {
askSleepDuration()
true
}
}
}
buttonsContainer.addView(button)
}
// Add "More" button if there are hidden buttons (excluding MORE itself)
val hiddenConfigsWithoutMore = hiddenConfigs.filter { it.id != ButtonConfig.ID_MORE }
if (hiddenConfigsWithoutMore.isNotEmpty()) {
// Get size configuration for the More button
val moreConfig = allConfigs.find { it.id == ButtonConfig.ID_MORE }
val moreSize = moreConfig?.size ?: ButtonConfigRepository.SIZE_MEDIUM
val (moreTextSizeRes, morePaddingRes, moreMinWidthRes) = when (moreSize) {
ButtonConfigRepository.SIZE_SMALL -> Triple(
R.dimen.button_text_size_small,
R.dimen.button_padding_small,
R.dimen.button_min_width_small
)
ButtonConfigRepository.SIZE_LARGE -> Triple(
R.dimen.button_text_size_large,
R.dimen.button_padding_large,
R.dimen.button_min_width_large
)
else -> Triple(
R.dimen.button_text_size_medium,
R.dimen.button_padding_medium,
R.dimen.button_min_width_medium
)
}
val textSizePx = resources.getDimension(moreTextSizeRes)
val paddingPx = resources.getDimensionPixelSize(morePaddingRes)
val minWidthPx = resources.getDimensionPixelSize(moreMinWidthRes)
val moreButton = TextView(this).apply {
text = getString(R.string.event_more_type)
setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, textSizePx)
gravity = android.view.Gravity.CENTER
background = ContextCompat.getDrawable(this@MainActivity, R.drawable.button_background)
val lp = FlexboxLayout.LayoutParams(
FlexboxLayout.LayoutParams.WRAP_CONTENT,
FlexboxLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(marginPx, marginPx, marginPx, marginPx)
flexGrow = 1f
minWidth = minWidthPx
}
layoutParams = lp
setPadding(paddingPx, paddingPx, paddingPx, paddingPx)
setOnClickListener { showMoreButtonsPopup(it, hiddenConfigsWithoutMore) }
}
buttonsContainer.addView(moreButton)
}
}
/**
* Shows a popup with all hidden buttons.
*/
private fun showMoreButtonsPopup(anchor: View, hiddenConfigs: List<ButtonConfig>) {
if (showingOverflowPopupWindow)
return
val popupView = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(16, 16, 16, 16)
setBackgroundColor(ContextCompat.getColor(this@MainActivity, R.color.transparent))
}
hiddenConfigs.forEach { config ->
val item = TextView(this).apply {
text = "${getString(config.iconResId)} ${getString(config.labelResId)}"
textSize = 18f
setPadding(20, 20, 20, 20)
background = ContextCompat.getDrawable(this@MainActivity, R.drawable.dropdown_list_item_background)
val itemLp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
itemLp.topMargin = 8
layoutParams = itemLp
}
popupView.addView(item)
}
val popupWindow = PopupWindow(
popupView,
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
true
).apply {
isOutsideTouchable = true
setOnDismissListener {
Handler(mainLooper).postDelayed({
showingOverflowPopupWindow = false
}, 500)
}
}
// Set click listeners after popup is created
for (i in 0 until popupView.childCount) {
val item = popupView.getChildAt(i) as TextView
val config = hiddenConfigs[i]
item.setOnClickListener {
handleButtonClick(config.id)
popupWindow.dismiss()
}
// Long click for breastfeeding and sleep buttons
if (config.id in listOf(
ButtonConfig.ID_BREASTFEEDING_LEFT,
ButtonConfig.ID_BREASTFEEDING_BOTH,
ButtonConfig.ID_BREASTFEEDING_RIGHT
)) {
item.setOnLongClickListener {
askBreastfeedingDuration(ButtonConfig.getEventType(config.id))
popupWindow.dismiss()
true
}
} else if (config.id == ButtonConfig.ID_SLEEP) {
item.setOnLongClickListener {
askSleepDuration()
popupWindow.dismiss()
true
}
}
}
popupWindow.showAsDropDown(anchor)
showingOverflowPopupWindow = true
}
/**
* Handles a button click based on the button ID.
*/
private fun handleButtonClick(buttonId: String) {
when (buttonId) {
ButtonConfig.ID_BOTTLE -> askBabyBottleContent()
ButtonConfig.ID_BREASTFEEDING_LEFT -> startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE)
ButtonConfig.ID_BREASTFEEDING_BOTH -> startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE)
ButtonConfig.ID_BREASTFEEDING_RIGHT -> startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE)
ButtonConfig.ID_FOOD -> askNotes(LunaEvent(LunaEvent.TYPE_FOOD))
ButtonConfig.ID_DIAPER_POO -> logEvent(LunaEvent(LunaEvent.TYPE_DIAPERCHANGE_POO))
ButtonConfig.ID_DIAPER_PEE -> logEvent(LunaEvent(LunaEvent.TYPE_DIAPERCHANGE_PEE))
ButtonConfig.ID_SLEEP -> startSleepTimer()
ButtonConfig.ID_MEDICINE -> askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE))
ButtonConfig.ID_TEMPERATURE -> askTemperatureValue()
ButtonConfig.ID_NOTE -> askNotes(LunaEvent(LunaEvent.TYPE_NOTE))
ButtonConfig.ID_PUKE -> askPukeValue()
ButtonConfig.ID_COLIC -> logEvent(LunaEvent(LunaEvent.TYPE_COLIC))
ButtonConfig.ID_WEIGHT -> askWeightValue()
ButtonConfig.ID_BATH -> logEvent(LunaEvent(LunaEvent.TYPE_BATH))
ButtonConfig.ID_ENEMA -> logEvent(LunaEvent(LunaEvent.TYPE_ENEMA))
}
}
fun askToTrimLogbook() { fun askToTrimLogbook() {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
d.setTitle(R.string.trim_logbook_dialog_title) d.setTitle(R.string.trim_logbook_dialog_title)
@@ -340,7 +831,6 @@ class MainActivity : AppCompatActivity() {
fun showEventDetailDialog(event: LunaEvent, items: ArrayList<LunaEvent>) { fun showEventDetailDialog(event: LunaEvent, items: ArrayList<LunaEvent>) {
// Do not update list while the detail is shown, to avoid changing the object below while it is changed by the user // Do not update list while the detail is shown, to avoid changing the object below while it is changed by the user
pauseLogbookUpdate = true pauseLogbookUpdate = true
val dateFormat = DateFormat.getDateTimeInstance()
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
d.setTitle(R.string.dialog_event_detail_title) d.setTitle(R.string.dialog_event_detail_title)
val dialogView = layoutInflater.inflate(R.layout.dialog_event_detail, null) val dialogView = layoutInflater.inflate(R.layout.dialog_event_detail, null)
@@ -352,8 +842,9 @@ class MainActivity : AppCompatActivity() {
val currentDateTime = Calendar.getInstance() val currentDateTime = Calendar.getInstance()
currentDateTime.time = Date(event.time * 1000) currentDateTime.time = Date(event.time * 1000)
val dateTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_date) val dateTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_date)
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), dateFormat.format(currentDateTime.time)) dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), DateUtils.formatDateTime(event.time))
dateTextView.setOnClickListener { dateTextView.setOnClickListener {
// Show datetime picker // Show datetime picker
val startYear = currentDateTime.get(Calendar.YEAR) val startYear = currentDateTime.get(Calendar.YEAR)
@@ -366,11 +857,9 @@ class MainActivity : AppCompatActivity() {
TimePickerDialog(this, { _, hour, minute -> TimePickerDialog(this, { _, hour, minute ->
val pickedDateTime = Calendar.getInstance() val pickedDateTime = Calendar.getInstance()
pickedDateTime.set(year, month, day, hour, minute) pickedDateTime.set(year, month, day, hour, minute)
currentDateTime.time = pickedDateTime.time
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), dateFormat.format(currentDateTime.time))
// Save event and move it to the right position in the logbook // Save event and move it to the right position in the logbook
event.time = currentDateTime.time.time / 1000 // Seconds since epoch event.time = pickedDateTime.time.time / 1000 // Seconds since epoch
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), DateUtils.formatDateTime(event.time))
logbook?.sort() logbook?.sort()
recyclerView.adapter?.notifyDataSetChanged() recyclerView.adapter?.notifyDataSetChanged()
saveLogbook() saveLogbook()
@@ -378,6 +867,44 @@ class MainActivity : AppCompatActivity() {
}, startYear, startMonth, startDay).show() }, startYear, startMonth, startDay).show()
} }
// Make quantity editable for breastfeeding and sleep events
val quantityTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity)
val isBreastfeeding = event.type in listOf(
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE,
LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
)
val isSleep = event.type == LunaEvent.TYPE_SLEEP
if ((isBreastfeeding || isSleep) && event.quantity > 0) {
quantityTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_edit, 0)
quantityTextView.compoundDrawableTintList = ColorStateList.valueOf(getColor(R.color.accent))
quantityTextView.setOnClickListener {
val pickerDialog = AlertDialog.Builder(this@MainActivity)
val pickerView = if (isSleep) {
layoutInflater.inflate(R.layout.sleep_duration_dialog, null)
} else {
layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
}
val picker = pickerView.findViewById<NumberPicker>(
if (isSleep) R.id.sleep_duration_picker else R.id.breastfeeding_duration_picker
)
picker.minValue = 1
picker.maxValue = if (isSleep) 180 else 60
picker.value = if (event.quantity > 0) Math.min(event.quantity, picker.maxValue) else if (isSleep) 30 else 15
pickerDialog.setTitle(if (isSleep) R.string.sleep_duration_title else R.string.breastfeeding_duration_title)
pickerDialog.setView(pickerView)
pickerDialog.setPositiveButton(android.R.string.ok) { _, _ ->
event.quantity = picker.value
quantityTextView.text = NumericUtils(this@MainActivity).formatEventQuantity(event)
recyclerView.adapter?.notifyDataSetChanged()
saveLogbook()
}
pickerDialog.setNegativeButton(android.R.string.cancel, null)
pickerDialog.show()
}
}
d.setView(dialogView) d.setView(dialogView)
d.setPositiveButton(R.string.dialog_event_detail_close_button) { dialogInterface, i -> dialogInterface.dismiss() } d.setPositiveButton(R.string.dialog_event_detail_close_button) { dialogInterface, i -> dialogInterface.dismiss() }
d.setNeutralButton(R.string.dialog_event_detail_delete_button) { dialogInterface, i -> deleteEvent(event) } d.setNeutralButton(R.string.dialog_event_detail_delete_button) { dialogInterface, i -> deleteEvent(event) }
@@ -389,6 +916,13 @@ class MainActivity : AppCompatActivity() {
pauseLogbookUpdate = false pauseLogbookUpdate = false
}) })
// show optional signature
if (event.signature.isNotEmpty()) {
val signatureTextEdit = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_signature)
signatureTextEdit.text = String.format(getString(R.string.dialog_event_detail_signature), event.signature)
signatureTextEdit.visibility = View.VISIBLE
}
// create next/previous links to events of the same type // create next/previous links to events of the same type
val previousTextView = dialogView.findViewById<TextView>(R.id.dialog_event_previous) val previousTextView = dialogView.findViewById<TextView>(R.id.dialog_event_previous)
@@ -441,6 +975,114 @@ class MainActivity : AppCompatActivity() {
alertDialog.show() alertDialog.show()
} }
fun showEditLogbookDialog() {
if (logbook == null) return
val d = AlertDialog.Builder(this)
d.setTitle(R.string.edit_logbook_title)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_logbook, null)
val logbookNameEditText = dialogView.findViewById<EditText>(R.id.dialog_edit_logbook_name)
logbookNameEditText.setText(logbook?.name ?: "")
val deleteButton = dialogView.findViewById<View>(R.id.dialog_edit_logbook_delete)
deleteButton.setOnClickListener {
showDeleteLogbookConfirmDialog()
}
d.setView(dialogView)
d.setPositiveButton(R.string.rename_logbook) { dialogInterface, i ->
val newName = logbookNameEditText.text.toString().trim()
if (newName.isNotEmpty() && newName != logbook?.name) {
renameLogbook(logbook?.name ?: "", newName)
}
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
d.create().show()
}
fun showDeleteLogbookConfirmDialog() {
val currentName = logbook?.name ?: return
val displayName = currentName.ifEmpty { getString(R.string.default_logbook_name) }
AlertDialog.Builder(this)
.setTitle(R.string.delete_logbook)
.setMessage(getString(R.string.delete_logbook_confirm, displayName))
.setPositiveButton(R.string.delete_logbook) { _, _ ->
deleteLogbook(currentName)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
fun renameLogbook(oldName: String, newName: String) {
setLoading(true)
logbookRepo?.renameLogbook(this, oldName, newName, object : LogbookRenamedListener {
override fun onLogbookRenamed() {
runOnUiThread {
setLoading(false)
Toast.makeText(this@MainActivity, R.string.logbook_renamed, Toast.LENGTH_SHORT).show()
loadLogbookList()
}
}
override fun onIOError(error: IOException) {
runOnUiThread {
setLoading(false)
Toast.makeText(this@MainActivity, R.string.logbook_rename_error, Toast.LENGTH_SHORT).show()
}
}
override fun onWebDAVError(error: SardineException) {
runOnUiThread {
setLoading(false)
Toast.makeText(this@MainActivity, R.string.logbook_rename_error, Toast.LENGTH_SHORT).show()
}
}
override fun onError(error: Exception) {
runOnUiThread {
setLoading(false)
Toast.makeText(this@MainActivity, R.string.logbook_rename_error, Toast.LENGTH_SHORT).show()
}
}
})
}
fun deleteLogbook(name: String) {
setLoading(true)
logbookRepo?.deleteLogbook(this, name, object : LogbookDeletedListener {
override fun onLogbookDeleted() {
runOnUiThread {
setLoading(false)
Toast.makeText(this@MainActivity, R.string.logbook_deleted, Toast.LENGTH_SHORT).show()
logbook = null
loadLogbookList()
}
}
override fun onIOError(error: IOException) {
runOnUiThread {
setLoading(false)
Toast.makeText(this@MainActivity, R.string.logbook_delete_error, Toast.LENGTH_SHORT).show()
}
}
override fun onWebDAVError(error: SardineException) {
runOnUiThread {
setLoading(false)
Toast.makeText(this@MainActivity, R.string.logbook_delete_error, Toast.LENGTH_SHORT).show()
}
}
override fun onError(error: Exception) {
runOnUiThread {
setLoading(false)
Toast.makeText(this@MainActivity, R.string.logbook_delete_error, Toast.LENGTH_SHORT).show()
}
}
})
}
fun loadLogbookList() { fun loadLogbookList() {
setLoading(true) setLoading(true)
logbookRepo?.listLogbooks(this, object: LogbookListObtainedListener { logbookRepo?.listLogbooks(this, object: LogbookListObtainedListener {
@@ -638,6 +1280,8 @@ class MainActivity : AppCompatActivity() {
fun logEvent(event: LunaEvent) { fun logEvent(event: LunaEvent) {
savingEvent(true) savingEvent(true)
event.signature = signature
setLoading(true) setLoading(true)
logbook?.logs?.add(0, event) logbook?.logs?.add(0, event)
recyclerView.adapter?.notifyItemInserted(0) recyclerView.adapter?.notifyItemInserted(0)
@@ -771,6 +1415,15 @@ class MainActivity : AppCompatActivity() {
isOutsideTouchable = true isOutsideTouchable = true
val inflater = LayoutInflater.from(anchor.context) val inflater = LayoutInflater.from(anchor.context)
contentView = inflater.inflate(R.layout.more_events_popup, null) contentView = inflater.inflate(R.layout.more_events_popup, null)
contentView.findViewById<View>(R.id.button_sleep).setOnClickListener {
startSleepTimer()
dismiss()
}
contentView.findViewById<View>(R.id.button_sleep).setOnLongClickListener {
askSleepDuration()
dismiss()
true
}
contentView.findViewById<View>(R.id.button_medicine).setOnClickListener { contentView.findViewById<View>(R.id.button_medicine).setOnClickListener {
askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE)) askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE))
dismiss() dismiss()
@@ -787,6 +1440,10 @@ class MainActivity : AppCompatActivity() {
askTemperatureValue() askTemperatureValue()
dismiss() dismiss()
}) })
contentView.findViewById<View>(R.id.button_puke).setOnClickListener({
askPukeValue()
dismiss()
})
contentView.findViewById<View>(R.id.button_colic).setOnClickListener({ contentView.findViewById<View>(R.id.button_colic).setOnClickListener({
logEvent( logEvent(
LunaEvent(LunaEvent.TYPE_COLIC) LunaEvent(LunaEvent.TYPE_COLIC)
@@ -797,6 +1454,12 @@ class MainActivity : AppCompatActivity() {
askWeightValue() askWeightValue()
dismiss() dismiss()
}) })
contentView.findViewById<View>(R.id.button_bath).setOnClickListener({
logEvent(
LunaEvent(LunaEvent.TYPE_BATH)
)
dismiss()
})
}.also { popupWindow -> }.also { popupWindow ->
popupWindow.setOnDismissListener({ popupWindow.setOnDismissListener({
Handler(mainLooper).postDelayed({ Handler(mainLooper).postDelayed({

View File

@@ -1,195 +1,61 @@
package it.danieleverducci.lunatracker package it.danieleverducci.lunatracker
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.RadioButton
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.switchmaterial.SwitchMaterial
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException
open class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
protected lateinit var settingsRepository: LocalSettingsRepository
protected lateinit var radioDataLocal: RadioButton private lateinit var settingsRepository: LocalSettingsRepository
protected lateinit var radioDataWebDAV: RadioButton private lateinit var textViewStorageStatus: TextView
protected lateinit var textViewWebDAVUrl: TextView
protected lateinit var textViewWebDAVUser: TextView private val storageSettingsLauncher = registerForActivityResult(
protected lateinit var textViewWebDAVPass: TextView ActivityResultContracts.StartActivityForResult()
protected lateinit var progressIndicator: LinearProgressIndicator ) { _ ->
protected lateinit var switchNoBreastfeeding: SwitchMaterial updateStorageStatus()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings) setContentView(R.layout.activity_settings)
radioDataLocal = findViewById(R.id.settings_data_local) textViewStorageStatus = findViewById(R.id.settings_storage_status)
radioDataWebDAV = findViewById(R.id.settings_data_webdav)
textViewWebDAVUrl = findViewById(R.id.settings_data_webdav_url)
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)
findViewById<View>(R.id.settings_save).setOnClickListener({ findViewById<View>(R.id.settings_storage).setOnClickListener {
validateAndSave() storageSettingsLauncher.launch(Intent(this, StorageSettingsActivity::class.java))
}) }
findViewById<View>(R.id.settings_cancel).setOnClickListener({ findViewById<View>(R.id.settings_button_config).setOnClickListener {
startActivity(Intent(this, ButtonConfigActivity::class.java))
}
findViewById<View>(R.id.settings_backup).setOnClickListener {
startActivity(Intent(this, BackupActivity::class.java))
}
findViewById<View>(R.id.settings_close).setOnClickListener {
finish() finish()
}) }
settingsRepository = LocalSettingsRepository(this) settingsRepository = LocalSettingsRepository(this)
loadSettings() loadSettings()
} }
fun loadSettings() { override fun onResume() {
super.onResume()
updateStorageStatus()
}
private fun loadSettings() {
updateStorageStatus()
}
private fun updateStorageStatus() {
val dataRepo = settingsRepository.loadDataRepository() val dataRepo = settingsRepository.loadDataRepository()
val webDavCredentials = settingsRepository.loadWebdavCredentials() textViewStorageStatus.text = when (dataRepo) {
val noBreastfeeding = settingsRepository.loadNoBreastfeeding() LocalSettingsRepository.DATA_REPO.WEBDAV -> getString(R.string.settings_storage_status_webdav)
else -> getString(R.string.settings_storage_status_local)
when (dataRepo) {
LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> radioDataLocal.isChecked = true
LocalSettingsRepository.DATA_REPO.WEBDAV -> radioDataWebDAV.isChecked = true
}
switchNoBreastfeeding.isChecked = noBreastfeeding
if (webDavCredentials != null) {
textViewWebDAVUrl.setText(webDavCredentials[0])
textViewWebDAVUser.setText(webDavCredentials[1])
textViewWebDAVPass.setText(webDavCredentials[2])
} }
} }
}
fun validateAndSave() {
if (radioDataLocal.isChecked) {
// No validation required, just save
saveSettings()
return
}
// Try to connect to WebDAV and check if the save file already exists
val webDAVLogbookRepo = WebDAVLogbookRepository(
textViewWebDAVUrl.text.toString(),
textViewWebDAVUser.text.toString(),
textViewWebDAVPass.text.toString()
)
progressIndicator.visibility = View.VISIBLE
webDAVLogbookRepo.listLogbooks(this, object: LogbookListObtainedListener{
override fun onLogbookListObtained(logbooksNames: ArrayList<String>) {
if (logbooksNames.isEmpty()) {
// TODO: Ask the user if he wants to upload the local ones or to create a new one
copyLocalLogbooksToWebdav(webDAVLogbookRepo, object: OnCopyLocalLogbooksToWebdavFinishedListener {
override fun onCopyLocalLogbooksToWebdavFinished(errors: String?) {
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
if (errors == null) {
saveSettings()
Toast.makeText(this@SettingsActivity, R.string.settings_webdav_creation_ok, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@SettingsActivity, errors, Toast.LENGTH_SHORT).show()
}
})
}
})
} else {
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
saveSettings()
Toast.makeText(this@SettingsActivity, R.string.settings_webdav_creation_ok, Toast.LENGTH_SHORT).show()
})
}
}
override fun onIOError(error: IOException) {
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(this@SettingsActivity, getString(R.string.settings_network_error) + error.toString(), Toast.LENGTH_SHORT).show()
})
}
override fun onWebDAVError(error: SardineException) {
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
if(error.toString().contains("401")) {
Toast.makeText(this@SettingsActivity, getString(R.string.settings_webdav_error_denied), Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@SettingsActivity, getString(R.string.settings_webdav_error_generic) + error.toString(), Toast.LENGTH_SHORT).show()
}
})
}
override fun onError(error: Exception) {
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(this@SettingsActivity, getString(R.string.settings_generic_error) + error.toString(), Toast.LENGTH_SHORT).show()
})
}
})
/*webDAVLogbookRepo.createLogbook(this, LogbookRepository.DEFAULT_LOGBOOK_NAME, object: WebDAVLogbookRepository.LogbookCreatedListener{
override fun onJSONError(error: JSONException) {
runOnUiThread({
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(this@SettingsActivity, getString(R.string.settings_json_error) + error.toString(), Toast.LENGTH_SHORT).show()
})
}
})*/
}
fun saveSettings() {
settingsRepository.saveDataRepository(
if (radioDataWebDAV.isChecked) LocalSettingsRepository.DATA_REPO.WEBDAV
else LocalSettingsRepository.DATA_REPO.LOCAL_FILE
)
settingsRepository.saveNoBreastfeeding(switchNoBreastfeeding.isChecked)
settingsRepository.saveWebdavCredentials(
textViewWebDAVUrl.text.toString(),
textViewWebDAVUser.text.toString(),
textViewWebDAVPass.text.toString()
)
finish()
}
/**
* Copies the local logbooks to webdav.
* @return success
*/
private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) {
Thread(Runnable {
var errors = StringBuilder()
val fileLogbookRepo = FileLogbookRepository()
val logbooks = fileLogbookRepo.getAllLogbooks(this)
for (logbook in logbooks) {
// Copy only if does not already exist
val error = webDAVLogbookRepository.uploadLogbookIfNotExists(this, logbook.name)
if (error != null) {
if (errors.isNotEmpty())
errors.append("\n")
errors.append(String.format(getString(R.string.settings_webdav_upload_error), logbook.name, error))
}
}
listener.onCopyLocalLogbooksToWebdavFinished(
if (errors.isEmpty()) null else errors.toString()
)
}).start()
}
private interface OnCopyLocalLogbooksToWebdavFinishedListener {
fun onCopyLocalLogbooksToWebdavFinished(errors: String?)
}
}

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,178 @@
package it.danieleverducci.lunatracker
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.RadioButton
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException
/**
* Activity for configuring storage settings (Local/WebDAV).
*/
class StorageSettingsActivity : AppCompatActivity() {
private lateinit var settingsRepository: LocalSettingsRepository
private lateinit var radioDataLocal: RadioButton
private lateinit var radioDataWebDAV: RadioButton
private lateinit var textViewWebDAVUrl: EditText
private lateinit var textViewWebDAVUser: EditText
private lateinit var textViewWebDAVPass: EditText
private lateinit var textViewSignature: EditText
private lateinit var progressIndicator: LinearProgressIndicator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_storage_settings)
radioDataLocal = findViewById(R.id.settings_data_local)
radioDataWebDAV = findViewById(R.id.settings_data_webdav)
textViewWebDAVUrl = findViewById(R.id.settings_data_webdav_url)
textViewWebDAVUser = findViewById(R.id.settings_data_webdav_user)
textViewWebDAVPass = findViewById(R.id.settings_data_webdav_pass)
textViewSignature = findViewById(R.id.settings_signature)
progressIndicator = findViewById(R.id.progress_indicator)
findViewById<View>(R.id.settings_save).setOnClickListener {
validateAndSave()
}
findViewById<View>(R.id.settings_cancel).setOnClickListener {
finish()
}
settingsRepository = LocalSettingsRepository(this)
loadSettings()
}
private fun loadSettings() {
val dataRepo = settingsRepository.loadDataRepository()
val webDavCredentials = settingsRepository.loadWebdavCredentials()
when (dataRepo) {
LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> radioDataLocal.isChecked = true
LocalSettingsRepository.DATA_REPO.WEBDAV -> radioDataWebDAV.isChecked = true
}
if (webDavCredentials != null) {
textViewWebDAVUrl.setText(webDavCredentials[0])
textViewWebDAVUser.setText(webDavCredentials[1])
textViewWebDAVPass.setText(webDavCredentials[2])
}
textViewSignature.setText(settingsRepository.loadSignature())
}
private fun validateAndSave() {
if (radioDataLocal.isChecked) {
saveSettings()
return
}
// Try to connect to WebDAV and check if the save file already exists
val webDAVLogbookRepo = WebDAVLogbookRepository(
textViewWebDAVUrl.text.toString(),
textViewWebDAVUser.text.toString(),
textViewWebDAVPass.text.toString()
)
progressIndicator.visibility = View.VISIBLE
webDAVLogbookRepo.listLogbooks(this, object : LogbookListObtainedListener {
override fun onLogbookListObtained(logbooksNames: ArrayList<String>) {
if (logbooksNames.isEmpty()) {
copyLocalLogbooksToWebdav(webDAVLogbookRepo, object : OnCopyLocalLogbooksToWebdavFinishedListener {
override fun onCopyLocalLogbooksToWebdavFinished(errors: String?) {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
if (errors == null) {
saveSettings()
Toast.makeText(this@StorageSettingsActivity, R.string.settings_webdav_creation_ok, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@StorageSettingsActivity, errors, Toast.LENGTH_SHORT).show()
}
}
}
})
} else {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
saveSettings()
Toast.makeText(this@StorageSettingsActivity, R.string.settings_webdav_creation_ok, Toast.LENGTH_SHORT).show()
}
}
}
override fun onIOError(error: IOException) {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(this@StorageSettingsActivity, getString(R.string.settings_network_error) + error.toString(), Toast.LENGTH_SHORT).show()
}
}
override fun onWebDAVError(error: SardineException) {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
if (error.toString().contains("401")) {
Toast.makeText(this@StorageSettingsActivity, getString(R.string.settings_webdav_error_denied), Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@StorageSettingsActivity, getString(R.string.settings_webdav_error_generic) + error.toString(), Toast.LENGTH_SHORT).show()
}
}
}
override fun onError(error: Exception) {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(this@StorageSettingsActivity, getString(R.string.settings_generic_error) + error.toString(), Toast.LENGTH_SHORT).show()
}
}
})
}
private fun saveSettings() {
settingsRepository.saveDataRepository(
if (radioDataWebDAV.isChecked) LocalSettingsRepository.DATA_REPO.WEBDAV
else LocalSettingsRepository.DATA_REPO.LOCAL_FILE
)
settingsRepository.saveWebdavCredentials(
textViewWebDAVUrl.text.toString(),
textViewWebDAVUser.text.toString(),
textViewWebDAVPass.text.toString()
)
settingsRepository.saveSignature(textViewSignature.text.toString())
setResult(RESULT_OK)
finish()
}
private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) {
Thread {
val errors = StringBuilder()
val fileLogbookRepo = FileLogbookRepository()
val logbooks = fileLogbookRepo.getAllLogbooks(this)
for (logbook in logbooks) {
val error = webDAVLogbookRepository.uploadLogbookIfNotExists(this, logbook.name)
if (error != null) {
if (errors.isNotEmpty())
errors.append("\n")
errors.append(String.format(getString(R.string.settings_webdav_upload_error), logbook.name, error))
}
}
listener.onCopyLocalLogbooksToWebdavFinished(
if (errors.isEmpty()) null else errors.toString()
)
}.start()
}
private interface OnCopyLocalLogbooksToWebdavFinishedListener {
fun onCopyLocalLogbooksToWebdavFinished(errors: String?)
}
}

View File

@@ -0,0 +1,170 @@
package it.danieleverducci.lunatracker.adapters
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import it.danieleverducci.lunatracker.R
import it.danieleverducci.lunatracker.entities.ButtonConfig
import it.danieleverducci.lunatracker.repository.ButtonConfigRepository
import java.util.Collections
/**
* Adapter for the button configuration list with drag-and-drop reordering.
*/
class ButtonConfigAdapter(
private val configs: MutableList<ButtonConfig>,
private val onStartDrag: (RecyclerView.ViewHolder) -> Unit
) : RecyclerView.Adapter<ButtonConfigAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_button_config, parent, false)
return ViewHolder(view)
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val config = configs[position]
val context = holder.itemView.context
holder.icon.text = context.getString(config.iconResId)
holder.label.text = context.getString(config.labelResId)
// Hide checkbox for MORE button (it's shown dynamically when there are hidden buttons)
if (config.id == ButtonConfig.ID_MORE) {
holder.checkbox.visibility = View.INVISIBLE
} else {
holder.checkbox.visibility = View.VISIBLE
holder.checkbox.isChecked = config.visible
}
holder.checkbox.setOnCheckedChangeListener { _, isChecked ->
config.visible = isChecked
}
// Start drag when touching the drag handle
holder.dragHandle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
onStartDrag(holder)
}
false
}
// Size buttons
updateSizeButtons(holder, config.size)
holder.sizeSmall.setOnClickListener {
config.size = ButtonConfigRepository.SIZE_SMALL
updateSizeButtons(holder, config.size)
}
holder.sizeMedium.setOnClickListener {
config.size = ButtonConfigRepository.SIZE_MEDIUM
updateSizeButtons(holder, config.size)
}
holder.sizeLarge.setOnClickListener {
config.size = ButtonConfigRepository.SIZE_LARGE
updateSizeButtons(holder, config.size)
}
}
private fun updateSizeButtons(holder: ViewHolder, size: Int) {
holder.sizeSmall.isSelected = (size == ButtonConfigRepository.SIZE_SMALL)
holder.sizeMedium.isSelected = (size == ButtonConfigRepository.SIZE_MEDIUM)
holder.sizeLarge.isSelected = (size == ButtonConfigRepository.SIZE_LARGE)
}
override fun getItemCount(): Int = configs.size
/**
* Moves an item from one position to another.
* Called by ItemTouchHelper during drag.
*/
fun moveItem(fromPosition: Int, toPosition: Int) {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(configs, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(configs, i, i - 1)
}
}
notifyItemMoved(fromPosition, toPosition)
updateOrders()
}
/**
* Updates the order field of all configs based on their current positions.
*/
private fun updateOrders() {
configs.forEachIndexed { index, config ->
config.order = index
}
}
/**
* Returns the current configuration list.
*/
fun getConfigs(): List<ButtonConfig> = configs.toList()
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val dragHandle: ImageView = itemView.findViewById(R.id.drag_handle)
val checkbox: CheckBox = itemView.findViewById(R.id.checkbox_visible)
val icon: TextView = itemView.findViewById(R.id.icon)
val label: TextView = itemView.findViewById(R.id.label)
val sizeSmall: TextView = itemView.findViewById(R.id.size_small)
val sizeMedium: TextView = itemView.findViewById(R.id.size_medium)
val sizeLarge: TextView = itemView.findViewById(R.id.size_large)
}
}
/**
* ItemTouchHelper.Callback for drag-and-drop reordering.
*/
class ButtonConfigItemTouchHelperCallback(
private val adapter: ButtonConfigAdapter
) : ItemTouchHelper.Callback() {
override fun isLongPressDragEnabled(): Boolean = false // We use drag handle instead
override fun isItemViewSwipeEnabled(): Boolean = false // No swipe to delete
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
adapter.moveItem(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// Not used
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
viewHolder?.itemView?.alpha = 0.7f
}
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.alpha = 1.0f
}
}

View File

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

View File

@@ -53,7 +53,7 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.textColor)) holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.textColor))
// Contents // Contents
holder.type.text = item.getTypeEmoji(context) holder.type.text = item.getTypeEmoji(context)
holder.description.text = when(item.type) { holder.description.text = when (item.type) {
LunaEvent.TYPE_MEDICINE -> item.notes LunaEvent.TYPE_MEDICINE -> item.notes
LunaEvent.TYPE_NOTE -> item.notes LunaEvent.TYPE_NOTE -> item.notes
LunaEvent.TYPE_CUSTOM -> item.notes LunaEvent.TYPE_CUSTOM -> item.notes

View File

@@ -0,0 +1,90 @@
package it.danieleverducci.lunatracker.entities
import it.danieleverducci.lunatracker.R
import it.danieleverducci.lunatracker.repository.ButtonConfigRepository
/**
* Represents a button configuration for the main screen.
* Users can configure which buttons are visible, in what order, and at what size.
*/
data class ButtonConfig(
val id: String, // Button identifier (matches LunaEvent.TYPE_*)
val iconResId: Int, // R.string.event_*_type (emoji)
val labelResId: Int, // R.string.event_*_desc (description)
var visible: Boolean, // Show on main screen?
var order: Int, // Display order (0 = first)
var size: Int = ButtonConfigRepository.SIZE_MEDIUM // Button size (S/M/L)
) {
companion object {
// Button IDs matching LunaEvent types
const val ID_BOTTLE = "bottle"
const val ID_BREASTFEEDING_LEFT = "breastfeeding_left"
const val ID_BREASTFEEDING_BOTH = "breastfeeding_both"
const val ID_BREASTFEEDING_RIGHT = "breastfeeding_right"
const val ID_FOOD = "food"
const val ID_DIAPER_POO = "diaper_poo"
const val ID_DIAPER_PEE = "diaper_pee"
const val ID_SLEEP = "sleep"
const val ID_MEDICINE = "medicine"
const val ID_TEMPERATURE = "temperature"
const val ID_NOTE = "note"
const val ID_PUKE = "puke"
const val ID_COLIC = "colic"
const val ID_WEIGHT = "weight"
const val ID_BATH = "bath"
const val ID_ENEMA = "enema"
const val ID_MORE = "more"
/**
* Returns the default button configuration.
* First 7 buttons are visible by default, rest are hidden.
*/
fun getDefaultConfigs(): List<ButtonConfig> {
return listOf(
ButtonConfig(ID_BOTTLE, R.string.event_bottle_type, R.string.event_bottle_desc, true, 0),
ButtonConfig(ID_BREASTFEEDING_LEFT, R.string.event_breastfeeding_left_type, R.string.event_breastfeeding_left_desc, true, 1),
ButtonConfig(ID_BREASTFEEDING_BOTH, R.string.event_breastfeeding_both_type, R.string.event_breastfeeding_both_desc, true, 2),
ButtonConfig(ID_BREASTFEEDING_RIGHT, R.string.event_breastfeeding_right_type, R.string.event_breastfeeding_right_desc, true, 3),
ButtonConfig(ID_FOOD, R.string.event_food_type, R.string.event_food_desc, true, 4),
ButtonConfig(ID_DIAPER_POO, R.string.event_diaperchange_poo_type, R.string.event_diaperchange_poo_desc, true, 5),
ButtonConfig(ID_DIAPER_PEE, R.string.event_diaperchange_pee_type, R.string.event_diaperchange_pee_desc, true, 6),
ButtonConfig(ID_SLEEP, R.string.event_sleep_type, R.string.event_sleep_desc, false, 7),
ButtonConfig(ID_MEDICINE, R.string.event_medicine_type, R.string.event_medicine_desc, false, 8),
ButtonConfig(ID_TEMPERATURE, R.string.event_temperature_type, R.string.event_temperature_desc, false, 9),
ButtonConfig(ID_NOTE, R.string.event_note_type, R.string.event_note_desc, false, 10),
ButtonConfig(ID_PUKE, R.string.event_puke_type, R.string.event_puke_desc, false, 11),
ButtonConfig(ID_COLIC, R.string.event_colic_type, R.string.event_colic_desc, false, 12),
ButtonConfig(ID_WEIGHT, R.string.event_scale_type, R.string.event_scale_desc, false, 13),
ButtonConfig(ID_BATH, R.string.event_bath_type, R.string.event_bath_desc, false, 14),
ButtonConfig(ID_ENEMA, R.string.event_enema_type, R.string.event_enema_desc, false, 15),
// More button - always last, visibility controlled dynamically
ButtonConfig(ID_MORE, R.string.event_more_type, R.string.event_more_desc, false, 16)
)
}
/**
* Maps button ID to LunaEvent type constant.
*/
fun getEventType(buttonId: String): String {
return when (buttonId) {
ID_BOTTLE -> LunaEvent.TYPE_BABY_BOTTLE
ID_BREASTFEEDING_LEFT -> LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE
ID_BREASTFEEDING_BOTH -> LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE
ID_BREASTFEEDING_RIGHT -> LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
ID_FOOD -> LunaEvent.TYPE_FOOD
ID_DIAPER_POO -> LunaEvent.TYPE_DIAPERCHANGE_POO
ID_DIAPER_PEE -> LunaEvent.TYPE_DIAPERCHANGE_PEE
ID_SLEEP -> LunaEvent.TYPE_SLEEP
ID_MEDICINE -> LunaEvent.TYPE_MEDICINE
ID_TEMPERATURE -> LunaEvent.TYPE_TEMPERATURE
ID_NOTE -> LunaEvent.TYPE_NOTE
ID_PUKE -> LunaEvent.TYPE_PUKE
ID_COLIC -> LunaEvent.TYPE_COLIC
ID_WEIGHT -> LunaEvent.TYPE_WEIGHT
ID_BATH -> LunaEvent.TYPE_BATH
ID_ENEMA -> LunaEvent.TYPE_ENEMA
else -> throw IllegalArgumentException("Unknown button ID: $buttonId")
}
}
}
}

View File

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

View File

@@ -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

@@ -0,0 +1,111 @@
package it.danieleverducci.lunatracker.repository
import android.content.Context
import android.content.SharedPreferences
import it.danieleverducci.lunatracker.entities.ButtonConfig
import org.json.JSONArray
import org.json.JSONObject
/**
* Repository for saving and loading button configurations.
* Stores the configuration as a JSON array in SharedPreferences.
*/
class ButtonConfigRepository(context: Context) {
companion object {
private const val PREFS_NAME = "button_config"
private const val KEY_CONFIGS = "configs"
// Button size constants
const val SIZE_SMALL = 0
const val SIZE_MEDIUM = 1
const val SIZE_LARGE = 2
}
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
/**
* Saves the button configurations to SharedPreferences.
*/
fun saveConfigs(configs: List<ButtonConfig>) {
val jsonArray = JSONArray()
configs.forEach { config ->
val jsonObject = JSONObject().apply {
put("id", config.id)
put("visible", config.visible)
put("order", config.order)
put("size", config.size)
}
jsonArray.put(jsonObject)
}
sharedPreferences.edit()
.putString(KEY_CONFIGS, jsonArray.toString())
.apply()
}
/**
* Loads the button configurations from SharedPreferences.
* Returns default configuration if no saved config exists.
*/
fun loadConfigs(): List<ButtonConfig> {
val jsonString = sharedPreferences.getString(KEY_CONFIGS, null)
?: return ButtonConfig.getDefaultConfigs()
return try {
val jsonArray = JSONArray(jsonString)
// Triple: visible, order, size
val savedConfigs = mutableMapOf<String, Triple<Boolean, Int, Int>>()
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
val id = jsonObject.getString("id")
val visible = jsonObject.getBoolean("visible")
val order = jsonObject.getInt("order")
val size = jsonObject.optInt("size", SIZE_MEDIUM)
savedConfigs[id] = Triple(visible, order, size)
}
// Merge saved configs with defaults (in case new buttons were added)
val defaultConfigs = ButtonConfig.getDefaultConfigs()
val mergedConfigs = defaultConfigs.map { defaultConfig ->
val saved = savedConfigs[defaultConfig.id]
if (saved != null) {
defaultConfig.copy(visible = saved.first, order = saved.second, size = saved.third)
} else {
defaultConfig
}
}
// Sort by order
mergedConfigs.sortedBy { it.order }
} catch (e: Exception) {
ButtonConfig.getDefaultConfigs()
}
}
/**
* Returns only the visible buttons, sorted by order.
*/
fun loadVisibleConfigs(): List<ButtonConfig> {
return loadConfigs()
.filter { it.visible }
.sortedBy { it.order }
}
/**
* Resets the configuration to defaults.
*/
fun resetToDefaults() {
sharedPreferences.edit()
.remove(KEY_CONFIGS)
.apply()
}
/**
* Checks if a custom configuration has been saved.
*/
fun hasCustomConfig(): Boolean {
return sharedPreferences.contains(KEY_CONFIGS)
}
}

View File

@@ -13,9 +13,9 @@ import java.io.FilenameFilter
class FileLogbookRepository: LogbookRepository { class FileLogbookRepository: LogbookRepository {
companion object { companion object {
val TAG = "FileLogbookRepository" const val TAG = "FileLogbookRepository"
val FILE_NAME_START = "data" const val FILE_NAME_START = "data"
val FILE_NAME_END = ".json" const val FILE_NAME_END = ".json"
} }
override fun loadLogbook(context: Context, name: String, listener: LogbookLoadedListener) { override fun loadLogbook(context: Context, name: String, listener: LogbookLoadedListener) {
@@ -32,7 +32,7 @@ class FileLogbookRepository: LogbookRepository {
fun loadLogbook(context: Context, name: String): Logbook { fun loadLogbook(context: Context, name: String): Logbook {
val logbook = Logbook(name) val logbook = Logbook(name)
val fileName = getFileName(name) val fileName = getFileName(name)
val file = File(context.getFilesDir(), fileName) val file = File(context.filesDir, fileName)
val json = FileInputStream(file).bufferedReader().use { it.readText() } val json = FileInputStream(file).bufferedReader().use { it.readText() }
val ja = JSONArray(json) val ja = JSONArray(json)
for (i in 0 until ja.length()) { for (i in 0 until ja.length()) {
@@ -58,7 +58,7 @@ class FileLogbookRepository: LogbookRepository {
fun saveLogbook(context: Context, logbook: Logbook) { fun saveLogbook(context: Context, logbook: Logbook) {
val fileName = getFileName(logbook.name) val fileName = getFileName(logbook.name)
val file = File(context.getFilesDir(), fileName) val file = File(context.filesDir, fileName)
val ja = JSONArray() val ja = JSONArray()
for (l in logbook.logs) { for (l in logbook.logs) {
ja.put(l.toJson()) ja.put(l.toJson())
@@ -82,7 +82,7 @@ class FileLogbookRepository: LogbookRepository {
} }
private fun listLogbooks(context: Context): ArrayList<String> { private fun listLogbooks(context: Context): ArrayList<String> {
val logbooksFileNames = context.getFilesDir().list(object: FilenameFilter { val logbooksFileNames = context.filesDir.list(object: FilenameFilter {
override fun accept(dir: File?, name: String?): Boolean { override fun accept(dir: File?, name: String?): Boolean {
if (name == null) if (name == null)
return false return false
@@ -105,6 +105,38 @@ class FileLogbookRepository: LogbookRepository {
return logbooksNames return logbooksNames
} }
override fun renameLogbook(
context: Context,
oldName: String,
newName: String,
listener: LogbookRenamedListener
) {
val oldFile = File(context.filesDir, getFileName(oldName))
val newFile = File(context.filesDir, getFileName(newName))
if (newFile.exists()) {
listener.onIOError(okio.IOException("A logbook with this name already exists"))
return
}
if (oldFile.renameTo(newFile)) {
listener.onLogbookRenamed()
} else {
listener.onIOError(okio.IOException("Failed to rename logbook"))
}
}
override fun deleteLogbook(
context: Context,
name: String,
listener: LogbookDeletedListener
) {
val file = File(context.filesDir, getFileName(name))
if (file.delete()) {
listener.onLogbookDeleted()
} else {
listener.onIOError(okio.IOException("Failed to delete logbook"))
}
}
private fun getFileName(name: String): String { private fun getFileName(name: String): String {
return "$FILE_NAME_START${if (name.isNotEmpty()) "_" else ""}${name}$FILE_NAME_END" return "$FILE_NAME_START${if (name.isNotEmpty()) "_" else ""}${name}$FILE_NAME_END"
} }

View File

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

View File

@@ -13,6 +13,8 @@ interface LogbookRepository {
fun loadLogbook(context: Context, name: String = "", listener: LogbookLoadedListener) fun loadLogbook(context: Context, name: String = "", listener: LogbookLoadedListener)
fun saveLogbook(context: Context,logbook: Logbook, listener: LogbookSavedListener) fun saveLogbook(context: Context,logbook: Logbook, listener: LogbookSavedListener)
fun listLogbooks(context: Context, listener: LogbookListObtainedListener) fun listLogbooks(context: Context, listener: LogbookListObtainedListener)
fun renameLogbook(context: Context, oldName: String, newName: String, listener: LogbookRenamedListener)
fun deleteLogbook(context: Context, name: String, listener: LogbookDeletedListener)
} }
interface LogbookLoadedListener { interface LogbookLoadedListener {
@@ -36,4 +38,18 @@ interface LogbookListObtainedListener {
fun onIOError(error: IOException) fun onIOError(error: IOException)
fun onWebDAVError(error: SardineException) fun onWebDAVError(error: SardineException)
fun onError(error: Exception) fun onError(error: Exception)
}
interface LogbookRenamedListener {
fun onLogbookRenamed()
fun onIOError(error: IOException)
fun onWebDAVError(error: SardineException)
fun onError(error: Exception)
}
interface LogbookDeletedListener {
fun onLogbookDeleted()
fun onIOError(error: IOException)
fun onWebDAVError(error: SardineException)
fun onError(error: Exception)
} }

View File

@@ -192,6 +192,62 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
} }
} }
override fun renameLogbook(
context: Context,
oldName: String,
newName: String,
listener: LogbookRenamedListener
) {
Thread(Runnable {
try {
val oldUrl = getUrl(oldName)
val newUrl = getUrl(newName)
// Check if target already exists
try {
sardine.get(newUrl).close()
listener.onIOError(IOException("A logbook with this name already exists"))
return@Runnable
} catch (e: SardineException) {
// 404 is expected - file doesn't exist, we can proceed
if (!e.toString().contains("404")) {
throw e
}
}
sardine.move(oldUrl, newUrl)
listener.onLogbookRenamed()
} catch (e: SardineException) {
Log.e(TAG, e.toString())
listener.onWebDAVError(e)
} catch (e: IOException) {
Log.e(TAG, e.toString())
listener.onIOError(e)
} catch (e: Exception) {
listener.onError(e)
}
}).start()
}
override fun deleteLogbook(
context: Context,
name: String,
listener: LogbookDeletedListener
) {
Thread(Runnable {
try {
sardine.delete(getUrl(name))
listener.onLogbookDeleted()
} catch (e: SardineException) {
Log.e(TAG, e.toString())
listener.onWebDAVError(e)
} catch (e: IOException) {
Log.e(TAG, e.toString())
listener.onIOError(e)
} catch (e: Exception) {
listener.onError(e)
}
}).start()
}
private fun getUrl(name: String): String { private fun getUrl(name: String): String {
val fileName = "${FILE_NAME_START}${if (name.isNotEmpty()) "_" else ""}${name}${FILE_NAME_END}" val fileName = "${FILE_NAME_START}${if (name.isNotEmpty()) "_" else ""}${name}${FILE_NAME_END}"
Log.d(TAG, fileName) Log.d(TAG, fileName)

View File

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

View File

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

View File

@@ -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,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#888888"
android:pathData="M11,18c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2 0.9,-2 2,-2 2,0.9 2,2zM9,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM9,4c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM15,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-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

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="@color/accent"/>
<stroke android:width="2dp" android:color="#B8A050"/>
<corners android:radius="4dp"/>
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#FFFFFF"/>
<stroke android:width="1dp" android:color="#999999"/>
<corners android:radius="4dp"/>
</shape>
</item>
</selector>

View File

@@ -0,0 +1,72 @@
<?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"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_backup_title"
android:textSize="28sp"
android:textColor="@color/accent"/>
<Button
android:id="@+id/backup_export"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
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:layout_marginTop="5dp"
android:text="@string/settings_export_desc"/>
<Button
android:id="@+id/backup_import"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
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:layout_marginTop="5dp"
android:text="@string/settings_import_desc"/>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
app:indicatorColor="@color/accent"
android:layout_marginTop="20dp"
android:visibility="invisible"/>
<Button
android:id="@+id/backup_close"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@android:string/ok"/>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,56 @@
<?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"
android:padding="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/button_config_title"
android:textSize="28sp"
android:textColor="@color/accent"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/button_config_hint"
android:textColor="@android:color/darker_gray"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/button_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="20dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:background="@drawable/button_background"
android:textColor="@android:color/darker_gray"
android:text="@string/button_config_cancel"/>
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@string/button_config_save"/>
</LinearLayout>
</LinearLayout>

View File

@@ -16,6 +16,15 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView
android:id="@+id/button_statistics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_gravity="end"
android:src="@drawable/ic_statistics"
app:tint="@color/grey"/>
<ImageView <ImageView
android:id="@+id/button_settings" android:id="@+id/button_settings"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -64,6 +73,16 @@
</FrameLayout> </FrameLayout>
<ImageView
android:id="@+id/logbooks_edit_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:padding="8dp"
android:src="@drawable/ic_edit"
android:background="@drawable/button_background"
app:tint="@color/accent"/>
<TextView <TextView
android:id="@+id/logbooks_add_button" android:id="@+id/logbooks_add_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -79,121 +98,15 @@
</LinearLayout> </LinearLayout>
<LinearLayout <com.google.android.flexbox.FlexboxLayout
android:id="@+id/buttons_container" android:id="@+id/buttons_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> app:flexWrap="wrap"
app:justifyContent="flex_start"
<LinearLayout app:alignItems="stretch">
android:layout_width="match_parent" <!-- Buttons werden dynamisch hinzugefügt -->
android:layout_height="wrap_content" </com.google.android.flexbox.FlexboxLayout>
android:orientation="horizontal">
<TextView
android:id="@+id/button_bottle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:layout_margin="5dp"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="50sp"
android:text="@string/event_bottle_type"/>
<TextView
android:id="@+id/button_food"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="5dp"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="50sp"
android:text="@string/event_food_type"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
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="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🤱⬅️"/>
<TextView
android:id="@+id/button_nipple_both"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🤱↔️"/>
<TextView
android:id="@+id/button_nipple_right"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🤱➡️️"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/button_change_poo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="2"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🚼 💩"/>
<TextView
android:id="@+id/button_change_pee"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="2"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🚼 💧"/>
<ImageView
android:id="@+id/button_more"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:src="@drawable/ic_more"
app:tint="@android:color/darker_gray"/>
</LinearLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -1,173 +1,82 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"> android:orientation="vertical"
android:padding="20dp">
<LinearLayout <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:text="@string/settings_title"
android:padding="20dp"> android:textSize="28sp"
android:textColor="@color/accent"/>
<TextView <!-- Storage Settings Button -->
android:layout_width="match_parent" <Button
android:layout_height="wrap_content" android:id="@+id/settings_storage"
android:text="@string/settings_title" android:layout_width="match_parent"
android:textSize="28sp" android:layout_height="wrap_content"
android:textColor="@color/accent"/> android:layout_marginTop="20dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@string/storage_settings_title"/>
<TextView <TextView
android:layout_width="match_parent" android:id="@+id/settings_storage_status"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:textStyle="bold" android:layout_height="wrap_content"
android:layout_marginTop="20dp" android:layout_marginStart="30dp"
android:text="@string/settings_storage"/> android:layout_marginTop="5dp"
android:text="@string/settings_storage_status_local"/>
<RadioGroup <!-- Button Configuration -->
android:layout_width="match_parent" <Button
android:layout_height="wrap_content" android:id="@+id/settings_button_config"
android:orientation="vertical"> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@string/button_config_title"/>
<RadioButton android:id="@+id/settings_data_local" <TextView
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="30dp" android:layout_marginStart="30dp"
android:checked="true" android:layout_marginTop="5dp"
android:text="@string/settings_storage_local"/> android:text="@string/button_config_settings_desc"/>
<TextView <!-- Data Backup Button -->
android:layout_width="match_parent" <Button
android:layout_height="wrap_content" android:id="@+id/settings_backup"
android:layout_marginStart="30dp" android:layout_width="match_parent"
android:text="@string/settings_storage_local_desc"/> android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@string/settings_backup_title"/>
<RadioButton android:id="@+id/settings_data_webdav" <TextView
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="20dp" android:layout_marginStart="30dp"
android:text="@string/settings_storage_dav"/> android:layout_marginTop="5dp"
android:text="@string/settings_backup_desc"/>
<TextView <!-- Spacer to push close button to bottom -->
android:layout_width="match_parent" <View
android:layout_height="wrap_content" android:layout_width="match_parent"
android:layout_marginStart="30dp" android:layout_height="0dp"
android:text="@string/settings_storage_dav_desc"/> android:layout_weight="1"/>
<TextView <Button
android:layout_width="match_parent" android:id="@+id/settings_close"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:layout_marginTop="10dp" android:layout_height="wrap_content"
android:layout_marginStart="30dp" android:layout_marginTop="20dp"
android:textStyle="bold" android:background="@drawable/button_background"
android:text="@string/settings_storage_dav_url"/> android:textColor="@color/accent"
android:text="@string/close"/>
<EditText </LinearLayout>
android:id="@+id/settings_data_webdav_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:hint="@string/settings_storage_dav_url_hint"
android:inputType="textUri"
android:background="@drawable/textview_background"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginStart="30dp"
android:textStyle="bold"
android:text="@string/settings_storage_dav_user"/>
<EditText
android:id="@+id/settings_data_webdav_user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:inputType="textEmailAddress"
android:background="@drawable/textview_background"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginStart="30dp"
android:textStyle="bold"
android:text="@string/settings_storage_dav_pass"/>
<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:background="@drawable/textview_background"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
app:indicatorColor="@color/accent"
android:layout_marginTop="20dp"
android:visibility="invisible"/>
</RadioGroup>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="5dp"
android:layout_marginEnd="30dp">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/no_breastfeeding" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_no_breastfeeding"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="horizontal">
<Button
android:id="@+id/settings_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="20dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@android:string/cancel"/>
<Button
android:id="@+id/settings_save"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/accent"
android:background="@drawable/button_background"
android:text="@android:string/ok"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

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,168 @@
<?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"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/storage_settings_title"
android:textSize="28sp"
android:textColor="@color/accent"/>
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="vertical">
<RadioButton android:id="@+id/settings_data_local"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/settings_storage_local"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
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="20dp"
android:text="@string/settings_storage_dav"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
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_marginStart="30dp"
android:textStyle="bold"
android:text="@string/settings_storage_dav_url"/>
<EditText
android:id="@+id/settings_data_webdav_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:hint="@string/settings_storage_dav_url_hint"
android:inputType="textUri"
android:background="@drawable/textview_background"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginStart="30dp"
android:textStyle="bold"
android:text="@string/settings_storage_dav_user"/>
<EditText
android:id="@+id/settings_data_webdav_user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:inputType="textEmailAddress"
android:background="@drawable/textview_background"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginStart="30dp"
android:textStyle="bold"
android:text="@string/settings_storage_dav_pass"/>
<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:background="@drawable/textview_background"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
app:indicatorColor="@color/accent"
android:layout_marginTop="20dp"
android:visibility="invisible"/>
</RadioGroup>
<!-- Signature -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
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:layout_marginTop="30dp"
android:orientation="horizontal">
<Button
android:id="@+id/settings_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="20dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@android:string/cancel"/>
<Button
android:id="@+id/settings_save"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/accent"
android:background="@drawable/button_background"
android:text="@android:string/ok"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">
<EditText
android:id="@+id/dialog_edit_logbook_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="1"
android:inputType="text"
android:hint="@string/edit_logbook_name_hint"
android:background="@drawable/textview_background"/>
<Button
android:id="@+id/dialog_edit_logbook_delete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@drawable/button_background"
android:textColor="#FF5252"
android:text="@string/delete_logbook"/>
</LinearLayout>

View File

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

View File

@@ -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

@@ -0,0 +1,80 @@
<?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="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="12dp"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/drag_handle"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_drag"
android:contentDescription="@string/button_config_drag_hint"/>
<CheckBox
android:id="@+id/checkbox_visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"/>
<TextView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="24sp"/>
<TextView
android:id="@+id/label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:textSize="16sp"/>
<!-- Size buttons S/M/L -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="8dp">
<TextView
android:id="@+id/size_small"
android:layout_width="28dp"
android:layout_height="28dp"
android:text="S"
android:textSize="12sp"
android:textStyle="bold"
android:gravity="center"
android:background="@drawable/size_button_background"/>
<TextView
android:id="@+id/size_medium"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginStart="4dp"
android:text="M"
android:textSize="12sp"
android:textStyle="bold"
android:gravity="center"
android:background="@drawable/size_button_background"/>
<TextView
android:id="@+id/size_large"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginStart="4dp"
android:text="L"
android:textSize="12sp"
android:textStyle="bold"
android:gravity="center"
android:background="@drawable/size_button_background"/>
</LinearLayout>
</LinearLayout>

View File

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

View File

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

View File

@@ -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

@@ -25,6 +25,7 @@
<string name="event_note_desc">Notiz</string> <string name="event_note_desc">Notiz</string>
<string name="event_temperature_desc">Temperatur</string> <string name="event_temperature_desc">Temperatur</string>
<string name="event_colic_desc">Blähungskolik</string> <string name="event_colic_desc">Blähungskolik</string>
<string name="event_more_desc">Mehr</string>
<string name="event_unknown_desc"></string> <string name="event_unknown_desc"></string>
<string name="overflow_event_scale">⚖️ Gewicht</string> <string name="overflow_event_scale">⚖️ Gewicht</string>
@@ -50,9 +51,13 @@
<string name="no_connection_go_to_settings">Einstellungen</string> <string name="no_connection_go_to_settings">Einstellungen</string>
<string name="no_connection_retry">Erneut versuchen</string> <string name="no_connection_retry">Erneut versuchen</string>
<string name="no_breastfeeding">Kein Stillen</string>
<string name="settings_title">Einstellungen</string> <string name="settings_title">Einstellungen</string>
<string name="close">Schließen</string>
<string name="storage_settings_title">Speicherort</string>
<string name="settings_storage_status_local">Aktuell: Lokaler Speicher</string>
<string name="settings_storage_status_webdav">Aktuell: WebDAV</string>
<string name="settings_no_breastfeeding">Kein Stillen</string>
<string name="settings_no_bottle">Kein Fläschchen</string>
<string name="settings_storage">Speicherort für Daten auswählen</string> <string name="settings_storage">Speicherort für Daten auswählen</string>
<string name="settings_storage_local">Auf dem Gerät</string> <string name="settings_storage_local">Auf dem Gerät</string>
<string name="settings_storage_local_desc">Datenschutzfreundlichste Lösung: Deine Daten verlassen dein Gerät nicht</string> <string name="settings_storage_local_desc">Datenschutzfreundlichste Lösung: Deine Daten verlassen dein Gerät nicht</string>
@@ -95,4 +100,152 @@
<string name="default_logbook_name">👶 Mein erstes Logbuch</string> <string name="default_logbook_name">👶 Mein erstes Logbuch</string>
<string name="logbook_created">Neues Logbuch erstellt: </string> <string name="logbook_created">Neues Logbuch erstellt: </string>
<string name="breastfeeding_timer_title">Stillen läuft</string>
<string name="breastfeeding_timer_stop">Stopp</string>
<string name="breastfeeding_timer_hint">Tippe Stopp wenn fertig</string>
<string name="breastfeeding_timer_already_running">Es läuft bereits eine Stillsitzung</string>
<string name="breastfeeding_duration_title">Stilldauer</string>
<string name="breastfeeding_duration_description">Dauer in Minuten eingeben</string>
<!-- Puke/Bath Events -->
<string name="log_puke_dialog_title">Spucken</string>
<string name="log_puke_dialog_description">Menge auswählen</string>
<string name="event_puke_desc">Spucken</string>
<string name="event_bath_desc">Baden</string>
<string name="overflow_event_puke">🤮 Spucken</string>
<string name="overflow_event_bath">🛁 Baden</string>
<!-- Zeitangaben -->
<string name="second_ago">Sek.</string>
<string name="seconds_ago">Sek.</string>
<string name="day_ago">Tag</string>
<string name="days_ago">Tage</string>
<string name="year_ago">Jahr</string>
<string name="years_ago">Jahre</string>
<!-- Mengenangaben -->
<string name="amount_little">Wenig</string>
<string name="amount_normal">Normal</string>
<string name="amount_plenty">Viel</string>
<!-- Signatur-Einstellungen -->
<string name="settings_signature">Signatur</string>
<string name="settings_signature_desc">Füge jedem Event eine Signatur hinzu, die andere sehen können. Nützlich wenn mehrere Personen Events hinzufügen.</string>
<string name="settings_no_breastfeeding_desc">Verstecke die Stillbuttons wenn sie nicht benötigt werden.</string>
<string name="settings_no_bottle_desc">Verstecke den Fläschchen-Button wenn er nicht benötigt wird.</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>
<!-- Button Configuration -->
<string name="button_config_title">Button-Konfiguration</string>
<string name="button_config_hint">Ziehen zum Sortieren, ankreuzen für Hauptbildschirm</string>
<string name="button_config_settings_desc">Anpassen welche Buttons auf dem Hauptbildschirm erscheinen</string>
<string name="button_config_save">Speichern</string>
<string name="button_config_cancel">Abbrechen</string>
<string name="button_config_saved">Button-Konfiguration gespeichert</string>
<string name="button_config_drag_hint">Ziehen zum Sortieren</string>
<string name="button_size_label">Größe:</string>
<string name="button_size_small">S</string>
<string name="button_size_medium">M</string>
<string name="button_size_large">L</string>
<!-- Export/Import -->
<string name="backup_title">Datensicherung</string>
<string name="settings_backup_title">Datensicherung</string>
<string name="settings_backup_desc">Logbook-Daten exportieren oder importieren</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>
<!-- Logbook-Verwaltung -->
<string name="edit_logbook_title">Logbook bearbeiten</string>
<string name="edit_logbook_name_hint">Logbook-Name</string>
<string name="rename_logbook">Umbenennen</string>
<string name="delete_logbook">Logbook löschen</string>
<string name="delete_logbook_confirm">Logbook \"%s\" löschen? Alle Ereignisse gehen verloren. Dies kann nicht rückgängig gemacht werden.</string>
<string name="logbook_renamed">Logbook umbenannt</string>
<string name="logbook_deleted">Logbook gelöscht</string>
<string name="logbook_rename_error">Umbenennen fehlgeschlagen</string>
<string name="logbook_delete_error">Löschen fehlgeschlagen</string>
<string name="logbook_name_exists">Ein Logbook mit diesem Namen existiert bereits</string>
</resources> </resources>

View File

@@ -25,6 +25,7 @@
<string name="event_note_desc">Note</string> <string name="event_note_desc">Note</string>
<string name="event_temperature_desc">Température</string> <string name="event_temperature_desc">Température</string>
<string name="event_colic_desc">Colique gazeuse</string> <string name="event_colic_desc">Colique gazeuse</string>
<string name="event_more_desc">Plus</string>
<string name="event_unknown_desc"></string> <string name="event_unknown_desc"></string>
<string name="overflow_event_scale">⚖️ Poids</string> <string name="overflow_event_scale">⚖️ Poids</string>
@@ -51,6 +52,10 @@
<string name="no_connection_retry">Réessayer</string> <string name="no_connection_retry">Réessayer</string>
<string name="settings_title">Paramètres</string> <string name="settings_title">Paramètres</string>
<string name="close">Fermer</string>
<string name="storage_settings_title">Stockage</string>
<string name="settings_storage_status_local">Actuellement : Stockage local</string>
<string name="settings_storage_status_webdav">Actuellement : WebDAV</string>
<string name="settings_storage">Choisir le lieu de stockage des données</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">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_local_desc">La solution la plus respectueuse de la vie privée : les données ne quittent pas l\'appareil</string>
@@ -69,6 +74,8 @@
<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_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_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="settings_webdav_upload_error">Une erreur est survenue en téléversant le journal local %1$s sur %2$s</string>
<string name="settings_no_bottle">Pas de biberon</string>
<string name="settings_no_bottle_desc">Masquer le bouton biberon s\'il n\'est pas nécessaire.</string>
<string name="trim_logbook_dialog_title">Votre journal grossit !</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_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>
@@ -93,4 +100,120 @@
<string name="default_logbook_name">👶 Mon premier carnet de bord</string> <string name="default_logbook_name">👶 Mon premier carnet de bord</string>
<string name="logbook_created">Journal ajouté: </string> <string name="logbook_created">Journal ajouté: </string>
<string name="breastfeeding_timer_title">Allaitement en cours</string>
<string name="breastfeeding_timer_stop">Arrêter</string>
<string name="breastfeeding_timer_hint">Appuyez sur Arrêter quand terminé</string>
<string name="breastfeeding_timer_already_running">Une session d\'allaitement est déjà en cours</string>
<string name="breastfeeding_duration_title">Durée d\'allaitement</string>
<string name="breastfeeding_duration_description">Entrez la durée en minutes</string>
<!-- Suivi du sommeil -->
<string name="event_sleep_desc">Sommeil</string>
<string name="sleep_timer_title">Bébé dort</string>
<string name="sleep_timer_stop">Réveillé</string>
<string name="sleep_timer_hint">Appuyez quand bébé se réveille</string>
<string name="sleep_timer_already_running">Une session de sommeil est déjà en cours</string>
<string name="sleep_duration_title">Durée du sommeil</string>
<string name="sleep_duration_description">Entrez la durée en minutes</string>
<string name="overflow_event_sleep">🌙 Sommeil</string>
<!-- Statistiques -->
<string name="statistics_title">Statistiques</string>
<string name="stats_tab_today">Aujourd\'hui</string>
<string name="stats_tab_feeding">Alimentation</string>
<string name="stats_tab_diapers">Couches</string>
<string name="stats_tab_sleep">Sommeil</string>
<string name="stats_tab_growth">Croissance</string>
<string name="stats_period_7days">7 jours</string>
<string name="stats_period_14days">14 jours</string>
<string name="stats_period_30days">30 jours</string>
<string name="stats_feeding_title">Alimentation</string>
<string name="stats_sleep_title">Sommeil</string>
<string name="stats_diapers_title">Couches</string>
<string name="stats_health_title">Santé</string>
<string name="stats_today">Aujourd\'hui</string>
<string name="stats_times">%d fois</string>
<string name="stats_count_format">%d× aujourd\'hui</string>
<string name="stats_avg_format">Moy: %s</string>
<string name="stats_daily_intake">Apport quotidien</string>
<string name="stats_bottle_title">Biberon</string>
<string name="stats_breastfeeding_title">Allaitement</string>
<string name="stats_avg_per_day">Moy: %.1f/jour</string>
<string name="stats_avg_ml_per_day">Moy: %.0f ml/jour</string>
<string name="stats_avg_min_per_day">Moy: %.0f min/jour</string>
<string name="stats_feeding_interval">Intervalle: moy. %d min</string>
<string name="stats_avg_duration">Durée moy: %.1f min</string>
<string name="stats_side_distribution">Répartition des côtés</string>
<string name="stats_left">Gauche</string>
<string name="stats_right">Droite</string>
<string name="stats_diapers_per_day">Couches par jour</string>
<string name="stats_poo">Selles</string>
<string name="stats_pee">Urine</string>
<string name="stats_summary">Résumé</string>
<string name="stats_avg_diapers">Moy: %.1f couches/jour</string>
<string name="stats_avg_poo">Selles: %.1f/jour</string>
<string name="stats_avg_pee">Urine: %.1f/jour</string>
<string name="stats_last_poo">Dernières selles: %s</string>
<string name="stats_sleep_per_day">Sommeil par jour</string>
<string name="stats_sleep_analysis">Analyse du sommeil</string>
<string name="stats_avg_sleep">Moy: %.1f heures/jour</string>
<string name="stats_avg_naps">Moy: %.1f siestes/jour</string>
<string name="stats_avg_nap_duration">Sieste moy: %.0f min</string>
<string name="stats_longest_sleep">Plus long: %d min</string>
<string name="stats_no_sleep_data">Aucune donnée de sommeil enregistrée</string>
<string name="stats_hours_format">%.1f h</string>
<string name="stats_weight_curve">Courbe de poids</string>
<string name="stats_weight_summary">Poids</string>
<string name="stats_current_weight">Actuel: %s</string>
<string name="stats_weight_gain_week">7 derniers jours: %+d g</string>
<string name="stats_weight_gain_month">30 derniers jours: %+d g</string>
<string name="stats_no_weight_data">Aucune donnée de poids enregistrée</string>
<string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</string>
<!-- Button Configuration -->
<string name="button_config_title">Configuration des boutons</string>
<string name="button_config_hint">Glisser pour réorganiser, cocher pour afficher sur l\'écran principal</string>
<string name="button_config_settings_desc">Personnaliser les boutons affichés sur l\'écran principal</string>
<string name="button_config_save">Enregistrer</string>
<string name="button_config_cancel">Annuler</string>
<string name="button_config_saved">Configuration des boutons enregistrée</string>
<string name="button_config_drag_hint">Glisser pour réorganiser</string>
<string name="button_size_label">Taille :</string>
<string name="button_size_small">S</string>
<string name="button_size_medium">M</string>
<string name="button_size_large">L</string>
<!-- Export/Import -->
<string name="backup_title">Sauvegarde des données</string>
<string name="settings_backup_title">Sauvegarde des données</string>
<string name="settings_backup_desc">Exporter ou importer les données du journal</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>
<!-- Gestion du journal -->
<string name="edit_logbook_title">Modifier le journal</string>
<string name="edit_logbook_name_hint">Nom du journal</string>
<string name="rename_logbook">Renommer</string>
<string name="delete_logbook">Supprimer le journal</string>
<string name="delete_logbook_confirm">Supprimer le journal \"%s\" ? Tous les événements seront perdus. Cette action est irréversible.</string>
<string name="logbook_renamed">Journal renommé</string>
<string name="logbook_deleted">Journal supprimé</string>
<string name="logbook_rename_error">Échec du renommage</string>
<string name="logbook_delete_error">Échec de la suppression</string>
<string name="logbook_name_exists">Un journal avec ce nom existe déjà</string>
</resources> </resources>

View File

@@ -32,6 +32,7 @@
<string name="event_note_desc">Nota</string> <string name="event_note_desc">Nota</string>
<string name="event_temperature_desc">Temperatura</string> <string name="event_temperature_desc">Temperatura</string>
<string name="event_colic_desc">Colichette</string> <string name="event_colic_desc">Colichette</string>
<string name="event_more_desc">Altro</string>
<string name="event_unknown_desc"></string> <string name="event_unknown_desc"></string>
<string name="toast_event_added">Evento aggiunto</string> <string name="toast_event_added">Evento aggiunto</string>
@@ -51,6 +52,10 @@
<string name="no_connection_retry">Riprova</string> <string name="no_connection_retry">Riprova</string>
<string name="settings_title">Impostazioni</string> <string name="settings_title">Impostazioni</string>
<string name="close">Chiudi</string>
<string name="storage_settings_title">Archiviazione</string>
<string name="settings_storage_status_local">Attualmente: Archiviazione locale</string>
<string name="settings_storage_status_webdav">Attualmente: WebDAV</string>
<string name="settings_storage">Scegli dove l\'app salva i dati</string> <string name="settings_storage">Scegli dove l\'app salva i dati</string>
<string name="settings_storage_local">Sul dispositivo</string> <string name="settings_storage_local">Sul dispositivo</string>
<string name="settings_storage_local_desc">La soluzione più privacy-friendly: i dati non escono mai dal tuo dispositivo</string> <string name="settings_storage_local_desc">La soluzione più privacy-friendly: i dati non escono mai dal tuo dispositivo</string>
@@ -69,6 +74,8 @@
<string name="settings_json_error">Sul server esiste un salvataggio, ma è corrotto o illeggibile. Cancellare il file </string> <string name="settings_json_error">Sul server esiste un salvataggio, ma è corrotto o illeggibile. Cancellare il file </string>
<string name="settings_generic_error">Si è verificato un errore: </string> <string name="settings_generic_error">Si è verificato un errore: </string>
<string name="settings_webdav_upload_error">Errore durante l\'upload del logbook locale %1$s su webdav: %2$s</string> <string name="settings_webdav_upload_error">Errore durante l\'upload del logbook locale %1$s su webdav: %2$s</string>
<string name="settings_no_bottle">Niente biberon</string>
<string name="settings_no_bottle_desc">Nascondi il pulsante biberon quando non è necessario.</string>
<string name="trim_logbook_dialog_title">Il tuo diario è bello grande!</string> <string name="trim_logbook_dialog_title">Il tuo diario è bello grande!</string>
<string name="trim_logbook_dialog_message_local">Il file del tuo diario sta crescendo molto. Ti suggeriamo di cancellare gli eventi più vecchi per evitare problemi di memoria.</string> <string name="trim_logbook_dialog_message_local">Il file del tuo diario sta crescendo molto. Ti suggeriamo di cancellare gli eventi più vecchi per evitare problemi di memoria.</string>
@@ -93,4 +100,120 @@
<string name="default_logbook_name">👶 Il mio primo diario</string> <string name="default_logbook_name">👶 Il mio primo diario</string>
<string name="logbook_created">Creato nuovo diario: </string> <string name="logbook_created">Creato nuovo diario: </string>
<string name="breastfeeding_timer_title">Allattamento in corso</string>
<string name="breastfeeding_timer_stop">Stop</string>
<string name="breastfeeding_timer_hint">Premi Stop quando hai finito</string>
<string name="breastfeeding_timer_already_running">Una sessione di allattamento è già in corso</string>
<string name="breastfeeding_duration_title">Durata allattamento</string>
<string name="breastfeeding_duration_description">Inserisci la durata in minuti</string>
<!-- Tracciamento del sonno -->
<string name="event_sleep_desc">Sonno</string>
<string name="sleep_timer_title">Il bimbo dorme</string>
<string name="sleep_timer_stop">Svegliato</string>
<string name="sleep_timer_hint">Premi quando il bimbo si sveglia</string>
<string name="sleep_timer_already_running">Una sessione di sonno è già in corso</string>
<string name="sleep_duration_title">Durata del sonno</string>
<string name="sleep_duration_description">Inserisci la durata in minuti</string>
<string name="overflow_event_sleep">🌙 Sonno</string>
<!-- Statistiche -->
<string name="statistics_title">Statistiche</string>
<string name="stats_tab_today">Oggi</string>
<string name="stats_tab_feeding">Alimentazione</string>
<string name="stats_tab_diapers">Pannolini</string>
<string name="stats_tab_sleep">Sonno</string>
<string name="stats_tab_growth">Crescita</string>
<string name="stats_period_7days">7 giorni</string>
<string name="stats_period_14days">14 giorni</string>
<string name="stats_period_30days">30 giorni</string>
<string name="stats_feeding_title">Alimentazione</string>
<string name="stats_sleep_title">Sonno</string>
<string name="stats_diapers_title">Pannolini</string>
<string name="stats_health_title">Salute</string>
<string name="stats_today">Oggi</string>
<string name="stats_times">%d volte</string>
<string name="stats_count_format">%d× oggi</string>
<string name="stats_avg_format">Media: %s</string>
<string name="stats_daily_intake">Assunzione giornaliera</string>
<string name="stats_bottle_title">Biberon</string>
<string name="stats_breastfeeding_title">Allattamento</string>
<string name="stats_avg_per_day">Media: %.1f/giorno</string>
<string name="stats_avg_ml_per_day">Media: %.0f ml/giorno</string>
<string name="stats_avg_min_per_day">Media: %.0f min/giorno</string>
<string name="stats_feeding_interval">Intervallo: media %d min</string>
<string name="stats_avg_duration">Durata media: %.1f min</string>
<string name="stats_side_distribution">Distribuzione lati</string>
<string name="stats_left">Sinistra</string>
<string name="stats_right">Destra</string>
<string name="stats_diapers_per_day">Pannolini al giorno</string>
<string name="stats_poo">Cacca</string>
<string name="stats_pee">Pipì</string>
<string name="stats_summary">Riepilogo</string>
<string name="stats_avg_diapers">Media: %.1f pannolini/giorno</string>
<string name="stats_avg_poo">Cacca: %.1f/giorno</string>
<string name="stats_avg_pee">Pipì: %.1f/giorno</string>
<string name="stats_last_poo">Ultima cacca: %s</string>
<string name="stats_sleep_per_day">Sonno al giorno</string>
<string name="stats_sleep_analysis">Analisi del sonno</string>
<string name="stats_avg_sleep">Media: %.1f ore/giorno</string>
<string name="stats_avg_naps">Media: %.1f sonnellini/giorno</string>
<string name="stats_avg_nap_duration">Sonnellino medio: %.0f min</string>
<string name="stats_longest_sleep">Più lungo: %d min</string>
<string name="stats_no_sleep_data">Nessun dato sul sonno registrato</string>
<string name="stats_hours_format">%.1f h</string>
<string name="stats_weight_curve">Curva di peso</string>
<string name="stats_weight_summary">Peso</string>
<string name="stats_current_weight">Attuale: %s</string>
<string name="stats_weight_gain_week">Ultimi 7 giorni: %+d g</string>
<string name="stats_weight_gain_month">Ultimi 30 giorni: %+d g</string>
<string name="stats_no_weight_data">Nessun dato sul peso registrato</string>
<string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</string>
<!-- Button Configuration -->
<string name="button_config_title">Configurazione pulsanti</string>
<string name="button_config_hint">Trascina per riordinare, seleziona per mostrare sulla schermata principale</string>
<string name="button_config_settings_desc">Personalizza quali pulsanti appaiono sulla schermata principale</string>
<string name="button_config_save">Salva</string>
<string name="button_config_cancel">Annulla</string>
<string name="button_config_saved">Configurazione pulsanti salvata</string>
<string name="button_config_drag_hint">Trascina per riordinare</string>
<string name="button_size_label">Dimensione:</string>
<string name="button_size_small">S</string>
<string name="button_size_medium">M</string>
<string name="button_size_large">L</string>
<!-- Export/Import -->
<string name="backup_title">Backup dati</string>
<string name="settings_backup_title">Backup dati</string>
<string name="settings_backup_desc">Esporta o importa i dati del diario</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>
<!-- Gestione diario -->
<string name="edit_logbook_title">Modifica diario</string>
<string name="edit_logbook_name_hint">Nome del diario</string>
<string name="rename_logbook">Rinomina</string>
<string name="delete_logbook">Elimina diario</string>
<string name="delete_logbook_confirm">Eliminare il diario \"%s\"? Tutti gli eventi andranno persi. Questa azione non può essere annullata.</string>
<string name="logbook_renamed">Diario rinominato</string>
<string name="logbook_deleted">Diario eliminato</string>
<string name="logbook_rename_error">Rinomina fallita</string>
<string name="logbook_delete_error">Eliminazione fallita</string>
<string name="logbook_name_exists">Un diario con questo nome esiste già</string>
</resources> </resources>

View File

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

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Button dimensions for dynamic button layout -->
<dimen name="button_margin">5dp</dimen>
<dimen name="button_min_width">80dp</dimen>
<dimen name="button_padding">8dp</dimen>
<!-- Button sizes: Small -->
<dimen name="button_text_size_small">24sp</dimen>
<dimen name="button_padding_small">6dp</dimen>
<dimen name="button_min_width_small">60dp</dimen>
<!-- Button sizes: Medium -->
<dimen name="button_text_size_medium">30sp</dimen>
<dimen name="button_padding_medium">8dp</dimen>
<dimen name="button_min_width_medium">80dp</dimen>
<!-- Button sizes: Large -->
<dimen name="button_text_size_large">40sp</dimen>
<dimen name="button_padding_large">12dp</dimen>
<dimen name="button_min_width_large">100dp</dimen>
</resources>

View File

@@ -12,6 +12,9 @@
<string name="log_temperature_dialog_title">Temperature</string> <string name="log_temperature_dialog_title">Temperature</string>
<string name="log_temperature_dialog_description">Insert the temperature</string> <string name="log_temperature_dialog_description">Insert the temperature</string>
<string name="log_puke_dialog_title">Puke</string>
<string name="log_puke_dialog_description">Select the amount</string>
<string name="event_bottle_type" translatable="false">🍼</string> <string name="event_bottle_type" translatable="false">🍼</string>
<string name="event_food_type" translatable="false">🥣</string> <string name="event_food_type" translatable="false">🥣</string>
<string name="event_scale_type" translatable="false">⚖️</string> <string name="event_scale_type" translatable="false">⚖️</string>
@@ -25,6 +28,10 @@
<string name="event_note_type" translatable="false">📝</string> <string name="event_note_type" translatable="false">📝</string>
<string name="event_temperature_type" translatable="false">🌡️</string> <string name="event_temperature_type" translatable="false">🌡️</string>
<string name="event_colic_type" translatable="false">💨</string> <string name="event_colic_type" translatable="false">💨</string>
<string name="event_puke_type" translatable="false">🤮</string>
<string name="event_bath_type" translatable="false">🛁</string>
<string name="event_sleep_type" translatable="false">🌙</string>
<string name="event_more_type" translatable="false"></string>
<string name="event_unknown_type" translatable="false">\?</string> <string name="event_unknown_type" translatable="false">\?</string>
<string name="event_bottle_desc">Baby bottle</string> <string name="event_bottle_desc">Baby bottle</string>
@@ -40,6 +47,10 @@
<string name="event_note_desc">Note</string> <string name="event_note_desc">Note</string>
<string name="event_temperature_desc">Temperature</string> <string name="event_temperature_desc">Temperature</string>
<string name="event_colic_desc">Gaseous colic</string> <string name="event_colic_desc">Gaseous colic</string>
<string name="event_puke_desc">Puke</string>
<string name="event_bath_desc">Bath</string>
<string name="event_sleep_desc">Sleep</string>
<string name="event_more_desc">More</string>
<string name="event_unknown_desc"></string> <string name="event_unknown_desc"></string>
<string name="overflow_event_scale">⚖️ Weight</string> <string name="overflow_event_scale">⚖️ Weight</string>
@@ -48,6 +59,8 @@
<string name="overflow_event_note">📝 Note</string> <string name="overflow_event_note">📝 Note</string>
<string name="overflow_event_temperature">🌡️ Temperature</string> <string name="overflow_event_temperature">🌡️ Temperature</string>
<string name="overflow_event_colic">💨 Gaseous colic</string> <string name="overflow_event_colic">💨 Gaseous colic</string>
<string name="overflow_event_puke">🤮 Puke</string>
<string name="overflow_event_bath">🛁 Bath</string>
<string name="toast_event_added">Event logged</string> <string name="toast_event_added">Event logged</string>
<string name="toast_logbook_saved">Logbook saved</string> <string name="toast_logbook_saved">Logbook saved</string>
@@ -66,14 +79,22 @@
<string name="year_ago">year</string> <string name="year_ago">year</string>
<string name="years_ago">years</string> <string name="years_ago">years</string>
<string name="amount_little">Little</string>
<string name="amount_normal">Normal</string>
<string name="amount_plenty">Plenty</string>
<string name="no_connection">No connection</string> <string name="no_connection">No connection</string>
<string name="no_connection_explain">Unable to reach WebDAV service</string> <string name="no_connection_explain">Unable to reach WebDAV service</string>
<string name="no_connection_go_to_settings">Settings</string> <string name="no_connection_go_to_settings">Settings</string>
<string name="no_connection_retry">Retry</string> <string name="no_connection_retry">Retry</string>
<string name="no_breastfeeding">No Breastfeeding</string>
<string name="settings_title">Settings</string> <string name="settings_title">Settings</string>
<string name="close">Close</string>
<string name="storage_settings_title">Storage Settings</string>
<string name="settings_storage_status_local">Currently: Local storage</string>
<string name="settings_storage_status_webdav">Currently: WebDAV</string>
<string name="settings_signature">Signature</string>
<string name="settings_signature_desc">Attach a signature to each event you create and for others to see. Useful if multiple people add events.</string>
<string name="settings_storage">Choose where to save data</string> <string name="settings_storage">Choose where to save data</string>
<string name="settings_storage_local">On device</string> <string name="settings_storage_local">On device</string>
<string name="settings_storage_local_desc">Most privacy-friendly solution: data doesn\'t leave your device</string> <string name="settings_storage_local_desc">Most privacy-friendly solution: data doesn\'t leave your device</string>
@@ -89,6 +110,10 @@
<string name="settings_webdav_error_generic">Error while trying to access WebDAV:</string> <string name="settings_webdav_error_generic">Error while trying to access WebDAV:</string>
<string name="settings_webdav_creation_error_generic">Unable to save a file on the WebDAV server:</string> <string name="settings_webdav_creation_error_generic">Unable to save a file on the WebDAV server:</string>
<string name="settings_webdav_creation_ok">Successfully connected with the WebDAV server</string> <string name="settings_webdav_creation_ok">Successfully connected with the WebDAV server</string>
<string name="settings_no_breastfeeding">No Breastfeeding</string>
<string name="settings_no_breastfeeding_desc">Hide the Breastfeeding buttons for when they are not needed.</string>
<string name="settings_no_bottle">No Bottle-Feeding</string>
<string name="settings_no_bottle_desc">Hide the Bottle-Feeding button for when it is not needed.</string>
<string name="settings_json_error">There\'s a save file on the server, but it is corrupted or unreadable. Please delete it </string> <string name="settings_json_error">There\'s a save file on the server, but it is corrupted or unreadable. Please delete it </string>
<string name="settings_generic_error">Error: </string> <string name="settings_generic_error">Error: </string>
<string name="settings_webdav_upload_error">Error while uploading local logbook %1$s to webdav: %2$s</string> <string name="settings_webdav_upload_error">Error while uploading local logbook %1$s to webdav: %2$s</string>
@@ -123,6 +148,7 @@
<string name="dialog_event_detail_delete_button">Delete</string> <string name="dialog_event_detail_delete_button">Delete</string>
<string name="dialog_event_detail_quantity">Quantity</string> <string name="dialog_event_detail_quantity">Quantity</string>
<string name="dialog_event_detail_notes">Notes</string> <string name="dialog_event_detail_notes">Notes</string>
<string name="dialog_event_detail_signature">by %s</string>
<string name="dialog_add_logbook_title">Add logbook</string> <string name="dialog_add_logbook_title">Add logbook</string>
<string name="dialog_add_logbook_logbookname">👶 Logbook name</string> <string name="dialog_add_logbook_logbookname">👶 Logbook name</string>
@@ -132,4 +158,120 @@
<string name="default_logbook_name">👶 My first logbook</string> <string name="default_logbook_name">👶 My first logbook</string>
<string name="logbook_created">New logbook created: </string> <string name="logbook_created">New logbook created: </string>
<string name="breastfeeding_timer_title">Breastfeeding in progress</string>
<string name="breastfeeding_timer_stop">Stop</string>
<string name="breastfeeding_timer_hint">Tap Stop when finished</string>
<string name="breastfeeding_timer_already_running">A breastfeeding session is already in progress</string>
<string name="breastfeeding_duration_title">Breastfeeding duration</string>
<string name="breastfeeding_duration_description">Enter the duration in minutes</string>
<string name="measurement_unit_time_minutes" translatable="false">min</string>
<!-- Sleep tracking -->
<string name="sleep_timer_title">Baby is sleeping</string>
<string name="sleep_timer_stop">Woke up</string>
<string name="sleep_timer_hint">Tap when baby wakes up</string>
<string name="sleep_timer_already_running">A sleep session is already in progress</string>
<string name="sleep_duration_title">Sleep duration</string>
<string name="sleep_duration_description">Enter the duration in minutes</string>
<string name="overflow_event_sleep">🌙 Sleep</string>
<!-- Statistics -->
<string name="statistics_title">Statistics</string>
<string name="stats_tab_today">Today</string>
<string name="stats_tab_feeding">Feeding</string>
<string name="stats_tab_diapers">Diapers</string>
<string name="stats_tab_sleep">Sleep</string>
<string name="stats_tab_growth">Growth</string>
<string name="stats_period_7days">7 days</string>
<string name="stats_period_14days">14 days</string>
<string name="stats_period_30days">30 days</string>
<string name="stats_feeding_title">Feeding</string>
<string name="stats_sleep_title">Sleep</string>
<string name="stats_diapers_title">Diapers</string>
<string name="stats_health_title">Health</string>
<string name="stats_today">Today</string>
<string name="stats_times">%d times</string>
<string name="stats_count_format">%d× today</string>
<string name="stats_avg_format">Avg: %s</string>
<string name="stats_daily_intake">Daily intake</string>
<string name="stats_bottle_title">Bottle</string>
<string name="stats_breastfeeding_title">Breastfeeding</string>
<string name="stats_avg_per_day">Avg: %.1f/day</string>
<string name="stats_avg_ml_per_day">Avg: %.0f ml/day</string>
<string name="stats_avg_min_per_day">Avg: %.0f min/day</string>
<string name="stats_feeding_interval">Feeding interval: avg. %d min</string>
<string name="stats_avg_duration">Avg duration: %.1f min</string>
<string name="stats_side_distribution">Side distribution</string>
<string name="stats_left">Left</string>
<string name="stats_right">Right</string>
<string name="stats_diapers_per_day">Diapers per day</string>
<string name="stats_poo">Poo</string>
<string name="stats_pee">Pee</string>
<string name="stats_summary">Summary</string>
<string name="stats_avg_diapers">Avg: %.1f diapers/day</string>
<string name="stats_avg_poo">Poo: %.1f/day</string>
<string name="stats_avg_pee">Pee: %.1f/day</string>
<string name="stats_last_poo">Last poo: %s</string>
<string name="stats_sleep_per_day">Sleep per day</string>
<string name="stats_sleep_analysis">Sleep analysis</string>
<string name="stats_avg_sleep">Avg: %.1f hours/day</string>
<string name="stats_avg_naps">Avg: %.1f naps/day</string>
<string name="stats_avg_nap_duration">Avg nap: %.0f min</string>
<string name="stats_longest_sleep">Longest: %d min</string>
<string name="stats_no_sleep_data">No sleep data recorded yet</string>
<string name="stats_hours_format">%.1f h</string>
<string name="stats_weight_curve">Weight curve</string>
<string name="stats_weight_summary">Weight</string>
<string name="stats_current_weight">Current: %s</string>
<string name="stats_weight_gain_week">Last 7 days: %+d g</string>
<string name="stats_weight_gain_month">Last 30 days: %+d g</string>
<string name="stats_no_weight_data">No weight data recorded yet</string>
<string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</string>
<!-- Button Configuration -->
<string name="button_config_title">Button Configuration</string>
<string name="button_config_hint">Drag to reorder, check to show on main screen</string>
<string name="button_config_settings_desc">Customize which buttons appear on the main screen</string>
<string name="button_config_save">Save</string>
<string name="button_config_cancel">Cancel</string>
<string name="button_config_saved">Button configuration saved</string>
<string name="button_config_drag_hint">Drag to reorder</string>
<string name="button_size_label">Size:</string>
<string name="button_size_small">S</string>
<string name="button_size_medium">M</string>
<string name="button_size_large">L</string>
<!-- Export/Import -->
<string name="backup_title">Data Backup</string>
<string name="settings_backup_title">Data Backup</string>
<string name="settings_backup_desc">Export or import logbook data</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>
<!-- Logbook management -->
<string name="edit_logbook_title">Edit Logbook</string>
<string name="edit_logbook_name_hint">Logbook name</string>
<string name="rename_logbook">Rename</string>
<string name="delete_logbook">Delete Logbook</string>
<string name="delete_logbook_confirm">Delete logbook \"%s\"? All events will be lost. This cannot be undone.</string>
<string name="logbook_renamed">Logbook renamed</string>
<string name="logbook_deleted">Logbook deleted</string>
<string name="logbook_rename_error">Failed to rename logbook</string>
<string name="logbook_delete_error">Failed to delete logbook</string>
<string name="logbook_name_exists">A logbook with this name already exists</string>
</resources> </resources>