3 Commits

Author SHA1 Message Date
cbd18cd891 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-11 21:31:49 +01:00
b6110c2cbb 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-08 09:33:36 +01:00
dccc89a8e2 Add CLAUDE.md and .claude/ to gitignore 2026-01-08 09:33:04 +01:00
29 changed files with 1982 additions and 715 deletions

2
.gitignore vendored
View File

@@ -107,3 +107,5 @@ app/release/output-metadata.json
# Other # Other
app/src/main/java/it/danieleverducci/lunatracker/TemporaryHardcodedCredentials.kt app/src/main/java/it/danieleverducci/lunatracker/TemporaryHardcodedCredentials.kt
.kotlin/sessions/* .kotlin/sessions/*
CLAUDE.md
.claude/

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

@@ -34,6 +34,18 @@
android:name=".StatisticsActivity" android:name=".StatisticsActivity"
android:label="@string/statistics_title" android:label="@string/statistics_title"
android:theme="@style/Theme.LunaTracker"/> 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

@@ -11,6 +11,7 @@ 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
@@ -21,6 +22,7 @@ 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
@@ -29,14 +31,18 @@ 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.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
@@ -56,7 +62,7 @@ class MainActivity : AppCompatActivity() {
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 signature = ""
@@ -97,43 +103,7 @@ 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_nipple_left).setOnClickListener {
startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE)
}
findViewById<View>(R.id.button_nipple_left).setOnLongClickListener {
askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE)
true
}
findViewById<View>(R.id.button_nipple_both).setOnClickListener {
startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE)
}
findViewById<View>(R.id.button_nipple_both).setOnLongClickListener {
askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE)
true
}
findViewById<View>(R.id.button_nipple_right).setOnClickListener {
startBreastfeedingTimer(LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE)
}
findViewById<View>(R.id.button_nipple_right).setOnLongClickListener {
askBreastfeedingDuration(LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE)
true
}
findViewById<View>(R.id.button_change_poo).setOnClickListener { logEvent(
LunaEvent(
LunaEvent.TYPE_DIAPERCHANGE_POO
)
) }
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_no_connection_settings).setOnClickListener {
showSettings() showSettings()
} }
@@ -207,11 +177,8 @@ class MainActivity : AppCompatActivity() {
signature = settingsRepository.loadSignature() signature = settingsRepository.loadSignature()
val noBreastfeeding = settingsRepository.loadNoBreastfeeding() // Render buttons based on configuration
findViewById<View>(R.id.layout_nipples).visibility = when (noBreastfeeding) { renderButtons()
true -> View.GONE
false -> View.VISIBLE
}
// Update list dates // Update list dates
recyclerView.adapter?.notifyDataSetChanged() recyclerView.adapter?.notifyDataSetChanged()
@@ -585,6 +552,234 @@ class MainActivity : AppCompatActivity() {
} }
} }
/**
* 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)
@@ -695,18 +890,12 @@ class MainActivity : AppCompatActivity() {
) )
picker.minValue = 1 picker.minValue = 1
picker.maxValue = if (isSleep) 180 else 60 picker.maxValue = if (isSleep) 180 else 60
val oldQuantity = event.quantity
picker.value = if (event.quantity > 0) Math.min(event.quantity, picker.maxValue) else if (isSleep) 30 else 15 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.setTitle(if (isSleep) R.string.sleep_duration_title else R.string.breastfeeding_duration_title)
pickerDialog.setView(pickerView) pickerDialog.setView(pickerView)
pickerDialog.setPositiveButton(android.R.string.ok) { _, _ -> pickerDialog.setPositiveButton(android.R.string.ok) { _, _ ->
val newQuantity = picker.value event.quantity = picker.value
if (newQuantity != oldQuantity) {
// Adjust end time based on duration change (duration reduced = end time earlier)
event.time = event.time - (oldQuantity - newQuantity) * 60L
event.quantity = newQuantity
}
quantityTextView.text = NumericUtils(this@MainActivity).formatEventQuantity(event) quantityTextView.text = NumericUtils(this@MainActivity).formatEventQuantity(event)
recyclerView.adapter?.notifyDataSetChanged() recyclerView.adapter?.notifyDataSetChanged()
saveLogbook() saveLogbook()
@@ -786,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 {

View File

@@ -1,354 +1,61 @@
package it.danieleverducci.lunatracker package it.danieleverducci.lunatracker
import android.net.Uri import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.EditText
import android.widget.RadioButton
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts 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.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException
import org.json.JSONArray
import org.json.JSONObject
open class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
protected lateinit var settingsRepository: LocalSettingsRepository
protected lateinit var radioDataLocal: RadioButton
protected lateinit var radioDataWebDAV: RadioButton
protected lateinit var textViewWebDAVUrl: TextView
protected lateinit var textViewWebDAVUser: TextView
protected lateinit var textViewWebDAVPass: TextView
protected lateinit var progressIndicator: LinearProgressIndicator
protected lateinit var switchNoBreastfeeding: SwitchMaterial
protected lateinit var textViewSignature: EditText
// Activity Result Launchers for Export/Import private lateinit var settingsRepository: LocalSettingsRepository
private val exportLauncher = registerForActivityResult( private lateinit var textViewStorageStatus: TextView
ActivityResultContracts.CreateDocument("application/json")
) { uri -> uri?.let { exportLogbookToUri(it) } }
private val importLauncher = registerForActivityResult( private val storageSettingsLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument() ActivityResultContracts.StartActivityForResult()
) { uri -> uri?.let { importLogbookFromUri(it) } } ) { _ ->
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)
textViewSignature = findViewById(R.id.settings_signature)
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()
}) }
findViewById<View>(R.id.settings_export).setOnClickListener({
startExport()
})
findViewById<View>(R.id.settings_import).setOnClickListener({
startImport()
})
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)
val signature = settingsRepository.loadSignature() 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
}
textViewSignature.setText(signature)
switchNoBreastfeeding.isChecked = noBreastfeeding
if (webDavCredentials != null) {
textViewWebDAVUrl.text = webDavCredentials[0]
textViewWebDAVUser.text = webDavCredentials[1]
textViewWebDAVPass.text = 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.saveSignature(textViewSignature.text.toString())
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 {
val 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?)
}
// Export/Import functionality
private fun startExport() {
val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.US)
.format(java.util.Date())
exportLauncher.launch("lunatracker_backup_$timestamp.json")
}
private fun startImport() {
importLauncher.launch(arrayOf("application/json"))
}
private fun exportLogbookToUri(uri: Uri) {
progressIndicator.visibility = View.VISIBLE
Thread {
try {
val fileLogbookRepo = FileLogbookRepository()
val logbooks = fileLogbookRepo.getAllLogbooks(this)
val json = JSONObject().apply {
put("version", 1)
put("app", "LunaTracker")
put("exported_at", System.currentTimeMillis())
put("logbooks", JSONArray().apply {
logbooks.forEach { logbook ->
put(JSONObject().apply {
put("name", logbook.name)
put("events", JSONArray().apply {
logbook.logs.forEach { event ->
put(event.toJson())
}
})
})
}
})
}
contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(json.toString(2).toByteArray(Charsets.UTF_8))
}
val eventCount = logbooks.sumOf { it.logs.size }
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.export_success, eventCount),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.export_error) + e.message,
Toast.LENGTH_SHORT
).show()
}
}
}.start()
}
private fun importLogbookFromUri(uri: Uri) {
progressIndicator.visibility = View.VISIBLE
Thread {
try {
val jsonString = contentResolver.openInputStream(uri)?.bufferedReader()?.readText()
?: throw Exception("Could not read file")
val json = JSONObject(jsonString)
val version = json.optInt("version", 1)
val fileLogbookRepo = FileLogbookRepository()
var totalEvents = 0
if (json.has("logbooks")) {
// New format with multiple logbooks
val logbooksArray = json.getJSONArray("logbooks")
for (i in 0 until logbooksArray.length()) {
val logbookJson = logbooksArray.getJSONObject(i)
val name = logbookJson.optString("name", "")
val eventsArray = logbookJson.getJSONArray("events")
val logbook = Logbook(name)
for (j in 0 until eventsArray.length()) {
try {
logbook.logs.add(LunaEvent(eventsArray.getJSONObject(j)))
totalEvents++
} catch (e: IllegalArgumentException) {
// Skip invalid events
}
}
fileLogbookRepo.saveLogbook(this, logbook)
}
} else if (json.has("events")) {
// Old format with single logbook
val name = json.optString("logbook_name", "")
val eventsArray = json.getJSONArray("events")
val logbook = Logbook(name)
for (i in 0 until eventsArray.length()) {
try {
logbook.logs.add(LunaEvent(eventsArray.getJSONObject(i)))
totalEvents++
} catch (e: IllegalArgumentException) {
// Skip invalid events
}
}
fileLogbookRepo.saveLogbook(this, logbook)
}
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.import_success, totalEvents),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
runOnUiThread {
progressIndicator.visibility = View.INVISIBLE
Toast.makeText(
this,
getString(R.string.import_error) + ": " + e.message,
Toast.LENGTH_LONG
).show()
}
}
}.start()
}
} }

View File

@@ -0,0 +1,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,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

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

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

@@ -14,6 +14,7 @@ class LocalSettingsRepository(val context: Context) {
const val SHARED_PREFS_DAV_USER = "webdav_user" const val SHARED_PREFS_DAV_USER = "webdav_user"
const val SHARED_PREFS_DAV_PASS = "webdav_password" const val SHARED_PREFS_DAV_PASS = "webdav_password"
const val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding" const val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding"
const val SHARED_PREFS_NO_BOTTLE = "no_bottle"
const val SHARED_PREFS_SIGNATURE = "signature" const val SHARED_PREFS_SIGNATURE = "signature"
const val SHARED_PREFS_BF_TIMER_START = "bf_timer_start" const val SHARED_PREFS_BF_TIMER_START = "bf_timer_start"
const val SHARED_PREFS_BF_TIMER_TYPE = "bf_timer_type" const val SHARED_PREFS_BF_TIMER_TYPE = "bf_timer_type"
@@ -50,6 +51,14 @@ class LocalSettingsRepository(val context: Context) {
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) {
sharedPreferences.edit(commit = true) { sharedPreferences.edit(commit = true) {
putString( putString(

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 {
@@ -37,3 +39,17 @@ interface LogbookListObtainedListener {
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

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

@@ -73,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"
@@ -88,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,12 +1,7 @@
<?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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="20dp"> android:padding="20dp">
@@ -17,224 +12,71 @@
android:textSize="28sp" android:textSize="28sp"
android:textColor="@color/accent"/> android:textColor="@color/accent"/>
<TextView <!-- Storage Settings Button -->
<Button
android:id="@+id/settings_storage"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textStyle="bold"
android:layout_marginTop="20dp"
android:text="@string/settings_storage"/>
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton android:id="@+id/settings_data_local"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
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:layout_marginTop="20dp"
android:text="@string/settings_storage_dav"/> android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@string/storage_settings_title"/>
<TextView <TextView
android:id="@+id/settings_storage_status"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:text="@string/settings_storage_dav_desc"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_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>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/settings_signature" />
<EditText
android:id="@+id/settings_signature"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:inputType="textEmailAddress" android:text="@string/settings_storage_status_local"/>
android:background="@drawable/textview_background"/>
<!-- Button Configuration -->
<Button
android:id="@+id/settings_button_config"
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"/>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:text="@string/settings_signature_desc"/> android:text="@string/button_config_settings_desc"/>
<LinearLayout <!-- Data Backup Button -->
<Button
android:id="@+id/settings_backup"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_marginTop="20dp"
android:layout_marginTop="20dp"> android:background="@drawable/button_background"
android:textColor="@color/accent"
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/settings_no_breastfeeding" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_no_breastfeeding"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_weight="1" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="5dp"
android:text="@string/settings_no_breastfeeding_desc"/>
<!-- Data Backup Section -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:layout_marginTop="30dp"
android:text="@string/settings_backup_title"/> android:text="@string/settings_backup_title"/>
<Button
android:id="@+id/settings_export"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@string/settings_export"/>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:text="@string/settings_export_desc"/> android:layout_marginTop="5dp"
android:text="@string/settings_backup_desc"/>
<!-- Spacer to push close button to bottom -->
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button <Button
android:id="@+id/settings_import" android:id="@+id/settings_close"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@string/settings_import"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:text="@string/settings_import_desc"/>
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="20dp" 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:background="@drawable/button_background"
android:textColor="@color/accent" android:textColor="@color/accent"
android:text="@android:string/cancel"/> android:text="@string/close"/>
<Button </LinearLayout>
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,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,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

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

@@ -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>
@@ -51,7 +52,12 @@
<string name="no_connection_retry">Erneut versuchen</string> <string name="no_connection_retry">Erneut versuchen</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_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>
@@ -126,6 +132,7 @@
<string name="settings_signature">Signatur</string> <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_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_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 --> <!-- Event-Detail-Dialog -->
<string name="dialog_event_detail_quantity">Menge</string> <string name="dialog_event_detail_quantity">Menge</string>
@@ -203,8 +210,23 @@
<string name="stats_weight_format">%.2f kg</string> <string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</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 --> <!-- Export/Import -->
<string name="backup_title">Datensicherung</string>
<string name="settings_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">Logbook exportieren</string>
<string name="settings_export_desc">Alle Ereignisse als JSON-Datei speichern</string> <string name="settings_export_desc">Alle Ereignisse als JSON-Datei speichern</string>
<string name="settings_import">Logbook importieren</string> <string name="settings_import">Logbook importieren</string>
@@ -214,4 +236,16 @@
<string name="import_success">%d Ereignisse importiert</string> <string name="import_success">%d Ereignisse importiert</string>
<string name="import_error">Import fehlgeschlagen</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>
@@ -171,8 +178,23 @@
<string name="stats_weight_format">%.2f kg</string> <string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</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 --> <!-- 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_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">Exporter le journal</string>
<string name="settings_export_desc">Enregistrer tous les événements en fichier JSON</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">Importer un journal</string>
@@ -182,4 +204,16 @@
<string name="import_success">%d événements importés</string> <string name="import_success">%d événements importés</string>
<string name="import_error">Échec de l\'import</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>
@@ -171,8 +178,23 @@
<string name="stats_weight_format">%.2f kg</string> <string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</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 --> <!-- Export/Import -->
<string name="backup_title">Backup dati</string>
<string name="settings_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">Esporta diario</string>
<string name="settings_export_desc">Salva tutti gli eventi come file JSON</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">Importa diario</string>
@@ -182,4 +204,16 @@
<string name="import_success">%d eventi importati</string> <string name="import_success">%d eventi importati</string>
<string name="import_error">Importazione fallita</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,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

@@ -31,6 +31,7 @@
<string name="event_puke_type" translatable="false">🤮</string> <string name="event_puke_type" translatable="false">🤮</string>
<string name="event_bath_type" translatable="false">🛁</string> <string name="event_bath_type" translatable="false">🛁</string>
<string name="event_sleep_type" translatable="false">🌙</string> <string name="event_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>
@@ -49,6 +50,7 @@
<string name="event_puke_desc">Puke</string> <string name="event_puke_desc">Puke</string>
<string name="event_bath_desc">Bath</string> <string name="event_bath_desc">Bath</string>
<string name="event_sleep_desc">Sleep</string> <string name="event_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>
@@ -87,6 +89,10 @@
<string name="no_connection_retry">Retry</string> <string name="no_connection_retry">Retry</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">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_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>
@@ -106,6 +112,8 @@
<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">No Breastfeeding</string>
<string name="settings_no_breastfeeding_desc">Hide the Breastfeeding buttons for when they are not needed.</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>
@@ -228,8 +236,23 @@
<string name="stats_weight_format">%.2f kg</string> <string name="stats_weight_format">%.2f kg</string>
<string name="stats_temperature_format">%.1f °C</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 --> <!-- Export/Import -->
<string name="backup_title">Data Backup</string>
<string name="settings_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">Export Logbook</string>
<string name="settings_export_desc">Save all events as JSON file</string> <string name="settings_export_desc">Save all events as JSON file</string>
<string name="settings_import">Import Logbook</string> <string name="settings_import">Import Logbook</string>
@@ -239,4 +262,16 @@
<string name="import_success">Imported %d events</string> <string name="import_success">Imported %d events</string>
<string name="import_error">Import failed</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>