forked from penguin86/luna-tracker
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
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
@@ -21,6 +22,7 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -29,14 +31,18 @@ import com.google.android.material.slider.Slider
|
||||
import com.thegrizzlylabs.sardineandroid.impl.SardineException
|
||||
import it.danieleverducci.lunatracker.adapters.DaySeparatorDecoration
|
||||
import it.danieleverducci.lunatracker.adapters.LunaEventRecyclerAdapter
|
||||
import it.danieleverducci.lunatracker.entities.ButtonConfig
|
||||
import it.danieleverducci.lunatracker.entities.Logbook
|
||||
import it.danieleverducci.lunatracker.entities.LunaEvent
|
||||
import it.danieleverducci.lunatracker.repository.ButtonConfigRepository
|
||||
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
|
||||
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
|
||||
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
|
||||
import it.danieleverducci.lunatracker.repository.LogbookLoadedListener
|
||||
import it.danieleverducci.lunatracker.repository.LogbookRepository
|
||||
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 kotlinx.coroutines.Runnable
|
||||
import okio.IOException
|
||||
@@ -56,7 +62,7 @@ class MainActivity : AppCompatActivity() {
|
||||
var logbook: Logbook? = null
|
||||
var pauseLogbookUpdate = false
|
||||
lateinit var progressIndicator: LinearProgressIndicator
|
||||
lateinit var buttonsContainer: ViewGroup
|
||||
lateinit var buttonsContainer: FlexboxLayout
|
||||
lateinit var recyclerView: RecyclerView
|
||||
lateinit var handler: Handler
|
||||
var signature = ""
|
||||
@@ -97,43 +103,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
// Set listeners
|
||||
findViewById<View>(R.id.logbooks_add_button).setOnClickListener { showAddLogbookDialog(true) }
|
||||
findViewById<View>(R.id.button_bottle).setOnClickListener { askBabyBottleContent() }
|
||||
findViewById<View>(R.id.button_food).setOnClickListener { askNotes(LunaEvent(LunaEvent.TYPE_FOOD)) }
|
||||
findViewById<View>(R.id.button_nipple_left).setOnClickListener {
|
||||
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.logbooks_edit_button).setOnClickListener { showEditLogbookDialog() }
|
||||
findViewById<View>(R.id.button_no_connection_settings).setOnClickListener {
|
||||
showSettings()
|
||||
}
|
||||
@@ -207,11 +177,8 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
signature = settingsRepository.loadSignature()
|
||||
|
||||
val noBreastfeeding = settingsRepository.loadNoBreastfeeding()
|
||||
findViewById<View>(R.id.layout_nipples).visibility = when (noBreastfeeding) {
|
||||
true -> View.GONE
|
||||
false -> View.VISIBLE
|
||||
}
|
||||
// Render buttons based on configuration
|
||||
renderButtons()
|
||||
|
||||
// Update list dates
|
||||
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() {
|
||||
val d = AlertDialog.Builder(this)
|
||||
d.setTitle(R.string.trim_logbook_dialog_title)
|
||||
@@ -780,6 +975,114 @@ class MainActivity : AppCompatActivity() {
|
||||
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() {
|
||||
setLoading(true)
|
||||
logbookRepo?.listLogbooks(this, object: LogbookListObtainedListener {
|
||||
|
||||
@@ -1,354 +1,61 @@
|
||||
package it.danieleverducci.lunatracker
|
||||
|
||||
import android.net.Uri
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.RadioButton
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import com.thegrizzlylabs.sardineandroid.impl.SardineException
|
||||
import it.danieleverducci.lunatracker.entities.Logbook
|
||||
import it.danieleverducci.lunatracker.entities.LunaEvent
|
||||
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
|
||||
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
|
||||
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
|
||||
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
|
||||
import okio.IOException
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
open class SettingsActivity : AppCompatActivity() {
|
||||
protected lateinit var settingsRepository: LocalSettingsRepository
|
||||
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
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
// Activity Result Launchers for Export/Import
|
||||
private val exportLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/json")
|
||||
) { uri -> uri?.let { exportLogbookToUri(it) } }
|
||||
private lateinit var settingsRepository: LocalSettingsRepository
|
||||
private lateinit var textViewStorageStatus: TextView
|
||||
|
||||
private val importLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri -> uri?.let { importLogbookFromUri(it) } }
|
||||
private val storageSettingsLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { _ ->
|
||||
updateStorageStatus()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_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)
|
||||
progressIndicator = findViewById(R.id.progress_indicator)
|
||||
switchNoBreastfeeding = findViewById(R.id.switch_no_breastfeeding)
|
||||
textViewSignature = findViewById(R.id.settings_signature)
|
||||
textViewStorageStatus = findViewById(R.id.settings_storage_status)
|
||||
|
||||
findViewById<View>(R.id.settings_save).setOnClickListener({
|
||||
validateAndSave()
|
||||
})
|
||||
findViewById<View>(R.id.settings_cancel).setOnClickListener({
|
||||
findViewById<View>(R.id.settings_storage).setOnClickListener {
|
||||
storageSettingsLauncher.launch(Intent(this, StorageSettingsActivity::class.java))
|
||||
}
|
||||
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()
|
||||
})
|
||||
findViewById<View>(R.id.settings_export).setOnClickListener({
|
||||
startExport()
|
||||
})
|
||||
findViewById<View>(R.id.settings_import).setOnClickListener({
|
||||
startImport()
|
||||
})
|
||||
}
|
||||
|
||||
settingsRepository = LocalSettingsRepository(this)
|
||||
loadSettings()
|
||||
}
|
||||
|
||||
fun loadSettings() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateStorageStatus()
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
updateStorageStatus()
|
||||
}
|
||||
|
||||
private fun updateStorageStatus() {
|
||||
val dataRepo = settingsRepository.loadDataRepository()
|
||||
val webDavCredentials = settingsRepository.loadWebdavCredentials()
|
||||
val noBreastfeeding = settingsRepository.loadNoBreastfeeding()
|
||||
val signature = settingsRepository.loadSignature()
|
||||
|
||||
when (dataRepo) {
|
||||
LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> radioDataLocal.isChecked = true
|
||||
LocalSettingsRepository.DATA_REPO.WEBDAV -> radioDataWebDAV.isChecked = true
|
||||
}
|
||||
|
||||
textViewSignature.setText(signature)
|
||||
switchNoBreastfeeding.isChecked = noBreastfeeding
|
||||
|
||||
if (webDavCredentials != null) {
|
||||
textViewWebDAVUrl.text = webDavCredentials[0]
|
||||
textViewWebDAVUser.text = webDavCredentials[1]
|
||||
textViewWebDAVPass.text = webDavCredentials[2]
|
||||
textViewStorageStatus.text = when (dataRepo) {
|
||||
LocalSettingsRepository.DATA_REPO.WEBDAV -> getString(R.string.settings_storage_status_webdav)
|
||||
else -> getString(R.string.settings_storage_status_local)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,38 @@ class FileLogbookRepository: LogbookRepository {
|
||||
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 {
|
||||
return "$FILE_NAME_START${if (name.isNotEmpty()) "_" else ""}${name}$FILE_NAME_END"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class LocalSettingsRepository(val context: Context) {
|
||||
const val SHARED_PREFS_DAV_USER = "webdav_user"
|
||||
const val SHARED_PREFS_DAV_PASS = "webdav_password"
|
||||
const val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding"
|
||||
const val SHARED_PREFS_NO_BOTTLE = "no_bottle"
|
||||
const val SHARED_PREFS_SIGNATURE = "signature"
|
||||
const val SHARED_PREFS_BF_TIMER_START = "bf_timer_start"
|
||||
const val SHARED_PREFS_BF_TIMER_TYPE = "bf_timer_type"
|
||||
@@ -50,6 +51,14 @@ class LocalSettingsRepository(val context: Context) {
|
||||
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) {
|
||||
sharedPreferences.edit(commit = true) {
|
||||
putString(
|
||||
|
||||
@@ -13,6 +13,8 @@ interface LogbookRepository {
|
||||
fun loadLogbook(context: Context, name: String = "", listener: LogbookLoadedListener)
|
||||
fun saveLogbook(context: Context,logbook: Logbook, listener: LogbookSavedListener)
|
||||
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 {
|
||||
@@ -36,4 +38,18 @@ interface LogbookListObtainedListener {
|
||||
fun onIOError(error: IOException)
|
||||
fun onWebDAVError(error: SardineException)
|
||||
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)
|
||||
}
|
||||
@@ -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 {
|
||||
val fileName = "${FILE_NAME_START}${if (name.isNotEmpty()) "_" else ""}${name}${FILE_NAME_END}"
|
||||
Log.d(TAG, fileName)
|
||||
|
||||
Reference in New Issue
Block a user