30 Commits
v0.3 ... master

Author SHA1 Message Date
1de99e89e5 added french language translation and corrected a few english typos 2025-07-01 16:58:51 +02:00
05c34178a4 Bumped version 2025-04-28 08:48:23 +02:00
0d3be20e1e Logbook event date edit 2025-04-23 08:58:32 +02:00
4c5c7bcf1a Merge branch 'master' into develop 2025-04-19 08:53:43 +02:00
07393faf41 Added food event 2025-04-19 08:51:16 +02:00
043a1c0283 Bumped version, changelog 2025-02-04 08:30:56 +01:00
9fddd37fe9 WIP editable date in event 2025-01-21 18:42:47 +01:00
efdb8f584a Avoid reloading logbooks list when the app is resumed, for a faster and flicker-less screen update 2025-01-20 07:57:46 +01:00
1511764497 Restored detail button string 2025-01-19 09:53:08 +01:00
744bfef62b Better error management, check Webdav instance unavailable, fixed crash on refresh when webdav instance was unavailable 2025-01-19 09:47:14 +01:00
34ca6c1cd6 Missing translation 2025-01-19 09:35:30 +01:00
b4f47ea6bb Implemented event deletion 2025-01-19 09:16:49 +01:00
99743288c5 Implemented transition from local to webdav logbooks and error management 2025-01-18 11:01:43 +01:00
03ec28f8ef Onboarding, fix in logbook list from file repo 2025-01-18 09:50:50 +01:00
ac9f74dbd7 Fix dropdown not updated on logbook creation, refinements 2025-01-18 09:09:44 +01:00
36f52234a3 Fixed bug loading always default logbook, indipendently from which one is selected 2025-01-18 08:56:12 +01:00
e23ab77274 Dropdown style, restored add logbook 2025-01-18 08:48:43 +01:00
e3aceaf133 Reimplemented logbook selector as spinner 2025-01-18 08:14:00 +01:00
0494a11538 Optimized space 2025-01-18 07:44:11 +01:00
ecbde64ca1 Working ugly selector 2025-01-18 07:36:31 +01:00
32fbeac079 WIP implementing multiple children (working, with some issues) 2025-01-17 08:55:37 +01:00
0b0fd8f5af Refactoring FileLogbookRepository 2025-01-15 08:08:26 +01:00
fe5da015cb Support for multiple logbook files 2025-01-13 08:16:06 +01:00
b1e26f8a96 Added Temperature and Gaseous colic events, removed custom event stub 2025-01-09 08:27:36 +01:00
48017f70f6 Fix typo in fastlane and readme 2025-01-08 07:49:10 +01:00
524a121149 Dismiss overflow popup on click outside, avoid reopening on overflow button click 2025-01-08 07:46:49 +01:00
e1388f9bad Consistency check 2025-01-08 07:35:02 +01:00
90761afef2 Changed fastlane description 2024-12-16 08:27:01 +01:00
b02a6c9aad Check on weight and red color on weight loss, fixed layout on small screen or screens with high font size 2024-12-11 08:43:14 +01:00
0d331d90ff Settings style 2024-12-03 08:48:49 +01:00
39 changed files with 1112 additions and 202 deletions

1
.gitignore vendored
View File

@ -106,3 +106,4 @@ app/release/output-metadata.json
# Other
app/src/main/java/it/danieleverducci/lunatracker/TemporaryHardcodedCredentials.kt
.kotlin/sessions/*

View File

@ -3,7 +3,7 @@
LunaTracker is a newborn baby tracking app.
Parenting can be tough. You get home from the hospital, exhausted, with this little fragile unknown thingy that has no user manual and a single way to let you know something's not ok: crying.
You have to react fast, event if it's 4 AM and you have no idea why is crying.
You have to react fast, even if it's 4 AM and you have no idea why is crying.
This app is meant to log all the relevant events (diaper change, breastfeeding, baby bottle feeding...), so you can always remember when has been done last time. It supports syncing the data between different devices (using your WebDAV server for the best privacy) so that when the baby needs attentions, the dad can step in leaving the mom sleeping peacefully.

View File

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

View File

@ -1,5 +1,8 @@
package it.danieleverducci.lunatracker
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Handler
@ -7,22 +10,28 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.NumberPicker
import android.widget.PopupWindow
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.slider.Slider
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.adapters.LunaEventRecyclerAdapter
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.LogbookLoadedListener
import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.LogbookSavedListener
@ -30,40 +39,37 @@ import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import kotlinx.coroutines.Runnable
import okio.IOException
import org.json.JSONException
import utils.DateUtils
import utils.NumericUtils
import java.text.DateFormat
import java.util.Calendar
import java.util.Date
class MainActivity : AppCompatActivity() {
companion object {
val TAG = "MainActivity"
val UPDATE_EVERY_SECS: Long = 30
val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false
}
lateinit var logbook: Logbook
lateinit var adapter: LunaEventRecyclerAdapter
var logbook: Logbook? = null
var pauseLogbookUpdate = false
lateinit var progressIndicator: LinearProgressIndicator
lateinit var buttonsContainer: ViewGroup
lateinit var recyclerView: RecyclerView
lateinit var handler: Handler
var savingEvent = false
val updateListRunnable: Runnable = Runnable {
loadLogbook()
if (logbook != null && !pauseLogbookUpdate)
loadLogbook(logbook!!.name)
handler.postDelayed(updateListRunnable, 1000*60)
}
var logbookRepo: LogbookRepository? = null
var showingOverflowPopupWindow = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handler = Handler(mainLooper)
adapter = LunaEventRecyclerAdapter(this)
adapter.onItemClickListener = object: LunaEventRecyclerAdapter.OnItemClickListener{
override fun onItemClick(event: LunaEvent) {
showEventDetailDialog(event)
}
}
// Show view
setContentView(R.layout.activity_main)
@ -72,11 +78,11 @@ class MainActivity : AppCompatActivity() {
buttonsContainer = findViewById<ViewGroup>(R.id.buttons_container)
recyclerView = findViewById<RecyclerView>(R.id.list_events)
recyclerView.setLayoutManager(LinearLayoutManager(applicationContext))
recyclerView.adapter = adapter
// 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_scale).setOnClickListener { askWeightValue() }
findViewById<View>(R.id.button_food).setOnClickListener { askNotes(LunaEvent(LunaEvent.TYPE_FOOD)) }
findViewById<View>(R.id.button_nipple_left).setOnClickListener { logEvent(
LunaEvent(
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE
@ -113,13 +119,24 @@ class MainActivity : AppCompatActivity() {
showSettings()
})
findViewById<View>(R.id.button_no_connection_retry).setOnClickListener({
loadLogbook()
// This may happen at start, when logbook is still null: better ask the logbook list
loadLogbookList()
})
findViewById<View>(R.id.button_sync).setOnClickListener({
loadLogbook()
loadLogbookList()
})
}
private fun setListAdapter(items: ArrayList<LunaEvent>) {
val adapter = LunaEventRecyclerAdapter(this, items)
adapter.onItemClickListener = object: LunaEventRecyclerAdapter.OnItemClickListener{
override fun onItemClick(event: LunaEvent) {
showEventDetailDialog(event)
}
}
recyclerView.adapter = adapter
}
fun showSettings() {
val i = Intent(this, SettingsActivity::class.java)
startActivity(i)
@ -127,9 +144,10 @@ class MainActivity : AppCompatActivity() {
fun showLogbook() {
// Show logbook
adapter.items.clear()
adapter.items.addAll(logbook.logs)
adapter.notifyDataSetChanged()
if (logbook == null)
Log.w(TAG, "showLogbook(): logbook is null!")
setListAdapter(logbook?.logs ?: arrayListOf())
}
override fun onStart() {
@ -151,10 +169,15 @@ class MainActivity : AppCompatActivity() {
}
// Update list dates
adapter.notifyDataSetChanged()
recyclerView.adapter?.notifyDataSetChanged()
// Reload data
loadLogbook()
if (logbook != null) {
// Already running: reload data for currently selected logbook
loadLogbook(logbook!!.name)
} else {
// First start: load logbook list
loadLogbookList()
}
}
override fun onStop() {
@ -206,16 +229,35 @@ class MainActivity : AppCompatActivity() {
alertDialog.show()
}
fun askTemperatureValue() {
// Show number picker dialog
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.temperature_dialog, null)
d.setTitle(R.string.log_temperature_dialog_title)
d.setMessage(R.string.log_temperature_dialog_description)
d.setView(dialogView)
val tempSlider = dialogView.findViewById<Slider>(R.id.dialog_temperature_value)
val range = NumericUtils(this).getValidEventQuantityRange(LunaEvent.TYPE_TEMPERATURE)!!
tempSlider.valueFrom = range.first.toFloat()
tempSlider.valueTo = range.second.toFloat()
tempSlider.value = range.third.toFloat()
val tempTextView = dialogView.findViewById<TextView>(R.id.dialog_temperature_display)
tempTextView.text = range.third.toString()
tempSlider.addOnChangeListener({s, v, b -> tempTextView.text = v.toString()})
d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val temperature = (tempSlider.value * 10).toInt() // In tenth of a grade
logEvent(LunaEvent(LunaEvent.TYPE_TEMPERATURE, temperature))
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
val alertDialog = d.create()
alertDialog.show()
}
fun askNotes(lunaEvent: LunaEvent) {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_notes, null)
d.setTitle(lunaEvent.getTypeDescription(this))
d.setMessage(
when (lunaEvent.type){
LunaEvent.TYPE_MEDICINE -> R.string.log_medicine_dialog_description
else -> R.string.log_notes_dialog_description
}
)
d.setMessage(lunaEvent.getDialogMessage(this))
d.setView(dialogView)
val notesET = dialogView.findViewById<EditText>(R.id.notes_edittext)
val qtyET = dialogView.findViewById<EditText>(R.id.notes_qty_edittext)
@ -250,7 +292,7 @@ class MainActivity : AppCompatActivity() {
}
)
d.setPositiveButton(R.string.trim_logbook_dialog_button_ok) { dialogInterface, i ->
logbook.trim()
logbook?.trim()
saveLogbook()
}
d.setNegativeButton(R.string.trim_logbook_dialog_button_cancel) { dialogInterface, i ->
@ -261,24 +303,200 @@ class MainActivity : AppCompatActivity() {
}
fun showEventDetailDialog(event: LunaEvent) {
// Do not update list while the detail is shown, to avoid changing the object below while it is changed by the user
pauseLogbookUpdate = true
val dateFormat = DateFormat.getDateTimeInstance();
val d = AlertDialog.Builder(this)
d.setTitle(R.string.dialog_event_detail_title)
val dialogView = layoutInflater.inflate(R.layout.dialog_event_detail, null)
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_emoji).setText(event.getTypeEmoji(this))
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_description).setText(event.getTypeDescription(this))
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_date).setText(dateFormat.format(Date(event.time * 1000)))
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity).setText(
NumericUtils(this).formatEventQuantity(event)
)
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_notes).setText(event.notes)
val currentDateTime = Calendar.getInstance()
currentDateTime.time = Date(event.time * 1000)
val dateTextView = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_date)
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), dateFormat.format(currentDateTime.time))
dateTextView.setOnClickListener({
// Show datetime picker
val startYear = currentDateTime.get(Calendar.YEAR)
val startMonth = currentDateTime.get(Calendar.MONTH)
val startDay = currentDateTime.get(Calendar.DAY_OF_MONTH)
val startHour = currentDateTime.get(Calendar.HOUR_OF_DAY)
val startMinute = currentDateTime.get(Calendar.MINUTE)
DatePickerDialog(this, DatePickerDialog.OnDateSetListener { _, year, month, day ->
TimePickerDialog(this, TimePickerDialog.OnTimeSetListener { _, hour, minute ->
val pickedDateTime = Calendar.getInstance()
pickedDateTime.set(year, month, day, hour, minute)
currentDateTime.time = pickedDateTime.time
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), dateFormat.format(currentDateTime.time))
// Save event and move it to the right position in the logbook
event.time = currentDateTime.time.time / 1000 // Seconds since epoch
logbook?.sort()
recyclerView.adapter?.notifyDataSetChanged()
saveLogbook()
}, startHour, startMinute, false).show()
}, startYear, startMonth, startDay).show()
})
d.setView(dialogView)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> dialogInterface.dismiss() }
d.setPositiveButton(R.string.dialog_event_detail_close_button) { dialogInterface, i -> dialogInterface.dismiss() }
d.setNeutralButton(R.string.dialog_event_detail_delete_button) { dialogInterface, i -> deleteEvent(event) }
val alertDialog = d.create()
alertDialog.show()
alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL).setTextColor(ContextCompat.getColor(this, R.color.danger))
alertDialog.setOnDismissListener({
// Resume logbook update
pauseLogbookUpdate = false
})
}
fun showAddLogbookDialog(requestedByUser: Boolean) {
val d = AlertDialog.Builder(this)
d.setTitle(R.string.dialog_add_logbook_title)
val dialogView = layoutInflater.inflate(R.layout.dialog_add_logbook, null)
dialogView.findViewById<TextView>(R.id.dialog_add_logbook_message).text = getString(
if (requestedByUser) R.string.dialog_add_logbook_message else R.string.dialog_add_logbook_message_intro
)
val logbookNameEditText = dialogView.findViewById<EditText>(R.id.dialog_add_logbook_logbookname)
d.setView(dialogView)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> addLogbook(logbookNameEditText.text.toString()) }
if (requestedByUser) {
d.setCancelable(true)
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
} else {
d.setCancelable(false)
}
val alertDialog = d.create()
alertDialog.show()
}
fun loadLogbook() {
fun loadLogbookList() {
setLoading(true)
logbookRepo?.listLogbooks(this, object: LogbookListObtainedListener {
override fun onLogbookListObtained(logbooksNames: ArrayList<String>) {
runOnUiThread({
if (logbooksNames.isEmpty()) {
// First run, no logbook: create one
showAddLogbookDialog(false)
return@runOnUiThread
}
// Show logbooks dropdown
val spinner = findViewById<Spinner>(R.id.logbooks_spinner)
val sAdapter = ArrayAdapter<String>(this@MainActivity, android.R.layout.simple_spinner_item)
sAdapter.setDropDownViewResource(R.layout.row_logbook_spinner)
for (ln in logbooksNames) {
sAdapter.add(
if (ln.isEmpty()) getString(R.string.default_logbook_name) else ln
)
}
spinner.adapter = sAdapter
spinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
// Changed logbook: empty list
setListAdapter(arrayListOf())
// Load logbook
loadLogbook(logbooksNames.get(position))
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
})
}
override fun onIOError(error: IOException) {
Log.e(TAG, "Unable to load logbooks list (IOError): $error")
runOnUiThread({
setLoading(false)
onRepoError(getString(R.string.settings_network_error) + error.toString())
})
}
override fun onWebDAVError(error: SardineException) {
Log.e(TAG, "Unable to load logbooks list (SardineException): $error")
runOnUiThread({
setLoading(false)
onRepoError(
if(error.toString().contains("401")) {
getString(R.string.settings_webdav_error_denied)
} else if(error.toString().contains("503")) {
getString(R.string.settings_webdav_error_server_offline)
} else {
getString(R.string.settings_webdav_error_generic) + error.toString()
}
)
})
}
override fun onError(error: Exception) {
Log.e(TAG, "Unable to load logbooks list: $error")
runOnUiThread({
setLoading(false)
onRepoError(getString(R.string.settings_generic_error) + error.toString())
})
}
})
}
fun addLogbook(logbookName: String) {
val newLogbook = Logbook(logbookName)
setLoading(true)
logbookRepo?.saveLogbook(this, newLogbook, object: LogbookSavedListener{
override fun onLogbookSaved() {
Log.d(TAG, "Logbook $logbookName created")
runOnUiThread({
setLoading(false)
loadLogbookList()
Toast.makeText(this@MainActivity, getString(R.string.logbook_created) + logbookName, Toast.LENGTH_SHORT).show()
})
}
override fun onIOError(error: IOException) {
runOnUiThread({
onRepoError(getString(R.string.settings_network_error) + error.toString())
})
}
override fun onWebDAVError(error: SardineException) {
runOnUiThread({
onRepoError(
if(error.toString().contains("401")) {
getString(R.string.settings_webdav_error_denied)
} else if(error.toString().contains("503")) {
getString(R.string.settings_webdav_error_server_offline)
} else {
getString(R.string.settings_webdav_error_generic) + error.toString()
}
)
})
}
override fun onJSONError(error: JSONException) {
runOnUiThread({
onRepoError(getString(R.string.settings_json_error) + error.toString())
})
}
override fun onError(error: Exception) {
runOnUiThread({
onRepoError(getString(R.string.settings_generic_error) + error.toString())
})
}
})
}
fun loadLogbook(name: String) {
if (savingEvent)
return
@ -288,13 +506,22 @@ class MainActivity : AppCompatActivity() {
// Load data
setLoading(true)
logbookRepo?.loadLogbook(this, object: LogbookLoadedListener{
logbookRepo?.loadLogbook(this, name, object: LogbookLoadedListener{
override fun onLogbookLoaded(lb: Logbook) {
runOnUiThread({
setLoading(false)
findViewById<View>(R.id.no_connection_screen).visibility = View.GONE
logbook = lb
showLogbook()
if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) {
for (e in logbook?.logs ?: listOf()) {
val em = e.getTypeEmoji(this@MainActivity)
if (em == getString(R.string.event_unknown_type)) {
Log.e(TAG, "UNKNOWN: ${e.type}")
}
}
}
})
}
@ -311,6 +538,8 @@ class MainActivity : AppCompatActivity() {
onRepoError(
if(error.toString().contains("401")) {
getString(R.string.settings_webdav_error_denied)
} else if(error.toString().contains("503")) {
getString(R.string.settings_webdav_error_server_offline)
} else {
getString(R.string.settings_webdav_error_generic) + error.toString()
}
@ -345,26 +574,40 @@ class MainActivity : AppCompatActivity() {
fun logEvent(event: LunaEvent) {
savingEvent(true)
adapter.items.add(0, event)
adapter.notifyItemInserted(0)
recyclerView.smoothScrollToPosition(0)
setLoading(true)
logbook.logs.add(0, event)
logbook?.logs?.add(0, event)
recyclerView.adapter?.notifyItemInserted(0)
recyclerView.smoothScrollToPosition(0)
saveLogbook(event)
// Check logbook size to avoid OOM errors
if (logbook.isTooBig()) {
if (logbook?.isTooBig() == true) {
askToTrimLogbook()
}
}
fun deleteEvent(event: LunaEvent) {
// Update view
savingEvent(true)
// Update data
setLoading(true)
logbook?.logs?.remove(event)
recyclerView.adapter?.notifyDataSetChanged()
saveLogbook()
}
/**
* Saves the logbook. If saving while adding an event, please specify the event so in case
* of error can be removed from the list.
*/
fun saveLogbook(lastEventAdded: LunaEvent? = null) {
logbookRepo?.saveLogbook(this, logbook, object: LogbookSavedListener{
if (logbook == null) {
Log.e(TAG, "Trying to save logbook, but logbook is null!")
return
}
logbookRepo?.saveLogbook(this, logbook!!, object: LogbookSavedListener{
override fun onLogbookSaved() {
Log.d(TAG, "Logbook saved")
runOnUiThread({
@ -397,6 +640,8 @@ class MainActivity : AppCompatActivity() {
onRepoError(
if(error.toString().contains("401")) {
getString(R.string.settings_webdav_error_denied)
} else if(error.toString().contains("503")) {
getString(R.string.settings_webdav_error_server_offline)
} else {
getString(R.string.settings_webdav_error_generic) + error.toString()
}
@ -432,8 +677,7 @@ class MainActivity : AppCompatActivity() {
setLoading(false)
Toast.makeText(this@MainActivity, R.string.toast_event_add_error, Toast.LENGTH_SHORT).show()
adapter.items.remove(event)
adapter.notifyDataSetChanged()
recyclerView.adapter?.notifyDataSetChanged()
savingEvent(false)
})
}
@ -457,6 +701,9 @@ class MainActivity : AppCompatActivity() {
}
private fun showOverflowPopupWindow(anchor: View) {
if (showingOverflowPopupWindow)
return
PopupWindow(anchor.context).apply {
isOutsideTouchable = true
val inflater = LayoutInflater.from(anchor.context)
@ -473,12 +720,28 @@ class MainActivity : AppCompatActivity() {
askNotes(LunaEvent(LunaEvent.TYPE_NOTE))
dismiss()
})
contentView.findViewById<View>(R.id.button_custom).setOnClickListener({
Toast.makeText(anchor.context, "TODO: Implement custom events", Toast.LENGTH_SHORT).show()
contentView.findViewById<View>(R.id.button_temperature).setOnClickListener({
askTemperatureValue()
dismiss()
})
contentView.findViewById<View>(R.id.button_colic).setOnClickListener({
logEvent(
LunaEvent(LunaEvent.TYPE_COLIC)
)
dismiss()
})
contentView.findViewById<View>(R.id.button_scale).setOnClickListener({
askWeightValue()
dismiss()
})
}.also { popupWindow ->
popupWindow.setOnDismissListener({
Handler(mainLooper).postDelayed({
showingOverflowPopupWindow = false
}, 500)
})
popupWindow.showAsDropDown(anchor)
showingOverflowPopupWindow = true
}
}
}

View File

@ -8,7 +8,10 @@ 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.LogbookRepository
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException
import org.json.JSONException
@ -72,14 +75,35 @@ open class SettingsActivity : AppCompatActivity() {
textViewWebDAVPass.text.toString()
)
progressIndicator.visibility = View.VISIBLE
webDAVLogbookRepo.createLogbook(this, object: WebDAVLogbookRepository.LogbookCreatedListener{
override fun onLogbookCreated() {
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({
@ -99,6 +123,16 @@ open class SettingsActivity : AppCompatActivity() {
})
}
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
@ -106,14 +140,8 @@ open class SettingsActivity : AppCompatActivity() {
})
}
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()
})
}
})
})*/
}
fun saveSettings() {
@ -129,4 +157,32 @@ open class SettingsActivity : AppCompatActivity() {
finish()
}
/**
* Copies the local logbooks to webdav.
* @return success
*/
private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) {
Thread(Runnable {
var errors = StringBuilder()
val fileLogbookRepo = FileLogbookRepository()
val logbooks = fileLogbookRepo.getAllLogbooks(this)
for (logbook in logbooks) {
// Copy only if does not already exist
val error = webDAVLogbookRepository.uploadLogbookIfNotExists(this, logbook.name)
if (error != null) {
if (errors.isNotEmpty())
errors.append("\n")
errors.append(String.format(getString(R.string.settings_webdav_upload_error), logbook.name, error))
}
}
listener.onCopyLocalLogbooksToWebdavFinished(
if (errors.isEmpty()) null else errors.toString()
)
}).start()
}
private interface OnCopyLocalLogbooksToWebdavFinishedListener {
fun onCopyLocalLogbooksToWebdavFinished(errors: String?)
}
}

View File

@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.R
@ -13,21 +14,30 @@ import utils.NumericUtils
class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.LunaEventVH> {
private val context: Context
val items = ArrayList<LunaEvent>()
private val items: ArrayList<LunaEvent>
val numericUtils: NumericUtils
var onItemClickListener: OnItemClickListener? = null
val layoutRes: Int
constructor(context: Context) {
constructor(context: Context, items: ArrayList<LunaEvent>) {
this.context = context
this.items = items
this.numericUtils = NumericUtils(context)
val fontScale = context.resources.configuration.fontScale
val screenSize = context.resources.configuration.screenWidthDp
this.layoutRes =
if(fontScale > 1.2 || screenSize < 320 || (fontScale > 1 && screenSize < 400))
R.layout.row_luna_event_vertical
else
R.layout.row_luna_event
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): LunaEventVH {
val view = LayoutInflater.from(context).inflate(R.layout.row_luna_event, parent, false)
val view = LayoutInflater.from(context).inflate(layoutRes, parent, false)
return LunaEventVH(view)
}
@ -40,6 +50,7 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
holder.root.setBackgroundResource(
if (position % 2 == 0) R.color.list_background_even else R.color.list_background_odd
)
holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.textColor))
// Contents
holder.type.text = item.getTypeEmoji(context)
holder.description.text = when(item.type) {
@ -49,7 +60,23 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
else -> item.getTypeDescription(context)
}
holder.time.text = DateUtils.formatTimeAgo(context, item.time)
holder.quantity.text = numericUtils.formatEventQuantity(item)
var quantityText = numericUtils.formatEventQuantity(item)
// if the event is weight, show difference with the last one
if (item.type == LunaEvent.TYPE_WEIGHT) {
val lastWeight = getPreviousWeightEvent(position)
if (lastWeight != null) {
val differenceInWeight = item.quantity - lastWeight.quantity
val sign = if (differenceInWeight > 0) "+" else ""
quantityText += "\n($sign$differenceInWeight)"
if (differenceInWeight < 0) {
holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.danger))
}
}
}
holder.quantity.text = quantityText
// Listeners
if (onItemClickListener != null) {
holder.root.setOnClickListener({
@ -62,6 +89,18 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
return items.size
}
private fun getPreviousWeightEvent(startFromPosition: Int): LunaEvent? {
if (startFromPosition == items.size - 1)
return null
for (pos in startFromPosition + 1 until items.size) {
val item = items.get(pos)
if (item.type != LunaEvent.TYPE_WEIGHT)
continue
return item
}
return null
}
class LunaEventVH: RecyclerView.ViewHolder {
val root: View
val type: TextView

View File

@ -1,6 +1,6 @@
package it.danieleverducci.lunatracker.entities
class Logbook {
class Logbook(val name: String) {
companion object {
val MAX_SAFE_LOGBOOK_SIZE = 30000
}
@ -16,4 +16,8 @@ class Logbook {
fun trim() {
logs.subList(MAX_SAFE_LOGBOOK_SIZE/2, logs.size).clear()
}
fun sort() {
logs.sortDescending()
}
}

View File

@ -11,7 +11,7 @@ import java.util.Date
* allow expandability and backwards compatibility (if a field is added in a
* release, it is simply ignored by previous ones).
*/
class LunaEvent {
class LunaEvent: Comparable<LunaEvent> {
companion object {
val TYPE_BABY_BOTTLE = "BABY_BOTTLE"
@ -25,6 +25,9 @@ class LunaEvent {
val TYPE_ENEMA = "ENEMA"
val TYPE_NOTE = "NOTE"
val TYPE_CUSTOM = "CUSTOM"
val TYPE_COLIC = "COLIC"
val TYPE_TEMPERATURE = "TEMPERATURE"
val TYPE_FOOD = "FOOD"
}
private val jo: JSONObject
@ -84,6 +87,9 @@ class LunaEvent {
TYPE_MEDICINE -> R.string.event_medicine_type
TYPE_ENEMA -> R.string.event_enema_type
TYPE_NOTE -> R.string.event_note_type
TYPE_TEMPERATURE -> R.string.event_temperature_type
TYPE_COLIC -> R.string.event_colic_type
TYPE_FOOD -> R.string.event_food_type
else -> R.string.event_unknown_type
}
)
@ -102,11 +108,21 @@ class LunaEvent {
TYPE_MEDICINE -> R.string.event_medicine_desc
TYPE_ENEMA -> R.string.event_enema_desc
TYPE_NOTE -> R.string.event_note_desc
TYPE_TEMPERATURE -> R.string.event_temperature_desc
TYPE_COLIC -> R.string.event_colic_desc
TYPE_FOOD -> R.string.event_food_desc
else -> R.string.event_unknown_desc
}
)
}
fun getDialogMessage(context: Context): String? {
return when(type) {
TYPE_MEDICINE -> context.getString(R.string.log_medicine_dialog_description)
else -> null
}
}
fun toJson(): JSONObject {
return jo
}
@ -114,4 +130,8 @@ class LunaEvent {
override fun toString(): String {
return "${type} qty: $quantity time: ${Date(time * 1000)}"
}
override fun compareTo(other: LunaEvent): Int {
return (this.time - other.time).toInt()
}
}

View File

@ -9,26 +9,30 @@ import org.json.JSONException
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FilenameFilter
class FileLogbookRepository: LogbookRepository {
companion object {
val TAG = "FileLogbookRepository"
val FILE_NAME_START = "data"
val FILE_NAME_END = ".json"
}
override fun loadLogbook(context: Context, listener: LogbookLoadedListener) {
override fun loadLogbook(context: Context, name: String, listener: LogbookLoadedListener) {
try {
listener.onLogbookLoaded(loadLogbook(context))
listener.onLogbookLoaded(loadLogbook(context, name))
} catch (e: FileNotFoundException) {
Log.d(TAG, "No logbook file found, create one")
val newLogbook = Logbook()
val newLogbook = Logbook(name)
saveLogbook(context, newLogbook)
listener.onLogbookLoaded(newLogbook)
}
}
fun loadLogbook(context: Context): Logbook {
val logbook = Logbook()
val file = File(context.getFilesDir(), "data.json")
fun loadLogbook(context: Context, name: String): Logbook {
val logbook = Logbook(name)
val fileName = getFileName(name)
val file = File(context.getFilesDir(), fileName)
val json = FileInputStream(file).bufferedReader().use { it.readText() }
val ja = JSONArray(json)
for (i in 0 until ja.length()) {
@ -53,11 +57,55 @@ class FileLogbookRepository: LogbookRepository {
}
fun saveLogbook(context: Context, logbook: Logbook) {
val file = File(context.getFilesDir(), "data.json")
val fileName = getFileName(logbook.name)
val file = File(context.getFilesDir(), fileName)
val ja = JSONArray()
for (l in logbook.logs) {
ja.put(l.toJson())
}
file.writeText(ja.toString())
}
override fun listLogbooks(
context: Context,
listener: LogbookListObtainedListener
) {
listener.onLogbookListObtained(listLogbooks(context))
}
fun getAllLogbooks(context: Context): List<Logbook> {
val logbooks = arrayListOf<Logbook>()
for (logbookName in listLogbooks(context)) {
logbooks.add(loadLogbook(context, logbookName))
}
return logbooks
}
private fun listLogbooks(context: Context): ArrayList<String> {
val logbooksFileNames = context.getFilesDir().list(object: FilenameFilter {
override fun accept(dir: File?, name: String?): Boolean {
if (name == null)
return false
if (name.startsWith(FILE_NAME_START) && name.endsWith(FILE_NAME_END))
return true
return false
}
})
if (logbooksFileNames == null || logbooksFileNames.isEmpty()) {
return arrayListOf()
}
val logbooksNames = arrayListOf<String>()
logbooksFileNames.forEach { it ->
logbooksNames.add(
it.replace("${FILE_NAME_START}_", "").replace(FILE_NAME_START, "").replace(FILE_NAME_END, "")
)
}
return logbooksNames
}
private fun getFileName(name: String): String {
return "$FILE_NAME_START${if (name.isNotEmpty()) "_" else ""}${name}$FILE_NAME_END"
}
}

View File

@ -7,8 +7,12 @@ import okio.IOException
import org.json.JSONException
interface LogbookRepository {
fun loadLogbook(context: Context, listener: LogbookLoadedListener)
companion object {
val DEFAULT_LOGBOOK_NAME = "" // For compatibility with older app versions
}
fun loadLogbook(context: Context, name: String = "", listener: LogbookLoadedListener)
fun saveLogbook(context: Context,logbook: Logbook, listener: LogbookSavedListener)
fun listLogbooks(context: Context, listener: LogbookListObtainedListener)
}
interface LogbookLoadedListener {
@ -26,3 +30,10 @@ interface LogbookSavedListener {
fun onJSONError(error: JSONException)
fun onError(error: Exception)
}
interface LogbookListObtainedListener {
fun onLogbookListObtained(logbooksNames: ArrayList<String>)
fun onIOError(error: IOException)
fun onWebDAVError(error: SardineException)
fun onError(error: Exception)
}

View File

@ -2,6 +2,7 @@ package it.danieleverducci.lunatracker.repository
import android.content.Context
import android.util.Log
import com.thegrizzlylabs.sardineandroid.DavResource
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.entities.Logbook
@ -14,11 +15,13 @@ import java.io.FileNotFoundException
import java.io.IOException
import java.net.SocketTimeoutException
import kotlin.io.bufferedReader
import kotlin.text.replace
class WebDAVLogbookRepository(val webDavURL: String, val username: String, val password: String): LogbookRepository {
companion object {
val TAG = "LogbookRepository"
val FILE_NAME = "lunatracker_logbook.json"
val FILE_NAME_START = "lunatracker_logbook"
val FILE_NAME_END = ".json"
}
val sardine: OkHttpSardine = OkHttpSardine()
@ -29,10 +32,10 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
)
}
override fun loadLogbook(context: Context, listener: LogbookLoadedListener) {
override fun loadLogbook(context: Context, name: String, listener: LogbookLoadedListener) {
Thread(Runnable {
try {
val logbook = loadLogbook(context)
val logbook = loadLogbook(name)
listener.onLogbookLoaded(logbook)
} catch (e: SardineException) {
Log.e(TAG, e.toString())
@ -52,12 +55,12 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start()
}
private fun loadLogbook(context: Context): Logbook {
val inputStream = sardine.get("$webDavURL/$FILE_NAME")
private fun loadLogbook(name: String,): Logbook {
val inputStream = sardine.get(getUrl(name))
val json = inputStream.bufferedReader().use(BufferedReader::readText)
inputStream.close()
val ja = JSONArray(json)
val logbook = Logbook()
val logbook = Logbook(name)
for (i in 0 until ja.length()) {
try {
val evt: LunaEvent = LunaEvent(ja.getJSONObject(i))
@ -95,6 +98,40 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start()
}
override fun listLogbooks(
context: Context,
listener: LogbookListObtainedListener
) {
Thread(Runnable {
try {
val logbooksNames = arrayListOf<String>()
for (dr: DavResource in sardine.list(webDavURL)){
if(!dr.name.startsWith(FILE_NAME_START))
continue
if(!dr.name.endsWith(FILE_NAME_END))
continue
logbooksNames.add(
dr.name.replace("${FILE_NAME_START}_", "")
.replace(FILE_NAME_START, "")
.replace(FILE_NAME_END, "")
)
}
listener.onLogbookListObtained(logbooksNames)
} catch (e: SardineException) {
Log.e(TAG, e.toString())
listener.onWebDAVError(e)
} catch (e: IOException) {
Log.e(TAG, e.toString())
listener.onIOError(e)
} catch (e: SocketTimeoutException) {
Log.e(TAG, e.toString())
listener.onIOError(e)
} catch (e: Exception) {
listener.onError(e)
}
}).start()
}
private fun saveLogbook(context: Context, logbook: Logbook) {
// Lock logbook on WebDAV to avoid concurrent changes
//sardine.lock(getUrl())
@ -108,64 +145,56 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
for (l in logbook.logs) {
ja.put(l.toJson())
}
sardine.put(getUrl(), ja.toString().toByteArray())
sardine.put(getUrl(logbook.name), ja.toString().toByteArray())
}
/**
* Connect to server and check if a logbook already exists.
* If it does not exist, try to upload the local one (or create a new one).
* If it does not exist, try to upload the local one.
* @return error, or null if no error
*/
fun createLogbook(context: Context, listener: LogbookCreatedListener) {
Thread(Runnable {
fun uploadLogbookIfNotExists(context: Context, name: String): String? {
val flr = FileLogbookRepository()
try {
loadLogbook(context)
listener.onLogbookCreated()
loadLogbook(name)
Log.d(TAG, "Logbook file $name already exist on the webDav share: will not overwrite it")
return null
} catch (e: SardineException) {
if (e.toString().contains("404")) {
// Connection successful, but no existing save. Upload the local one.
// Connection successful, but logbook does not exist. Upload the local one.
try {
val flr = FileLogbookRepository()
val logbook = flr.loadLogbook(context)
val logbook = flr.loadLogbook(context, name)
saveLogbook(context, logbook)
Log.d(TAG, "Local logbook file found, uploaded")
listener.onLogbookCreated()
Log.d(TAG, "Local logbook file $name found, uploaded")
return null
} catch (e: FileNotFoundException) {
Log.d(TAG, "No local logbook file found, uploading empty file")
saveLogbook(context, Logbook())
listener.onLogbookCreated()
Log.e(TAG, "No local logbook file found, this should not happen!")
return "No local logbook file found, app is in inconsistent state, please delete and reinstall it"
} catch (e: SardineException) {
Log.e(TAG, "Unable to upload logbook: $e")
listener.onWebDAVError(e)
return e.toString()
}
} else {
Log.e(TAG, e.toString())
listener.onWebDAVError(e)
return e.toString()
}
} catch (e: IOException) {
Log.e(TAG, e.toString())
listener.onIOError(e)
return e.toString()
} catch (e: SocketTimeoutException) {
Log.e(TAG, e.toString())
listener.onIOError(e)
return e.toString()
} catch (e: JSONException) {
Log.e(TAG, e.toString())
listener.onJSONError(e)
return e.toString()
} catch (e: Exception) {
listener.onError(e)
return e.toString()
}
}).start()
}
private fun getUrl(): String {
return "$webDavURL/$FILE_NAME"
}
interface LogbookCreatedListener {
fun onLogbookCreated()
fun onIOError(error: okio.IOException)
fun onWebDAVError(error: SardineException)
fun onJSONError(error: JSONException)
fun onError(error: Exception)
private fun getUrl(name: String): String {
val fileName = "${FILE_NAME_START}${if (name.isNotEmpty()) "_" else ""}${name}${FILE_NAME_END}"
Log.d(TAG, fileName)
return "$webDavURL/$fileName"
}
}

View File

@ -12,6 +12,7 @@ class NumericUtils (val context: Context) {
val measurement_unit_liquid_base: String
val measurement_unit_weight_base: String
val measurement_unit_weight_tiny: String
val measurement_unit_temperature_base: String
init {
this.numberFormat = NumberFormat.getInstance()
@ -34,18 +35,58 @@ class NumericUtils (val context: Context) {
else
R.string.measurement_unit_weight_tiny_imperial
)
this.measurement_unit_temperature_base = context.getString(
if (measurementSystem == LocaleData. MeasurementSystem.SI)
R.string.measurement_unit_temperature_base_metric
else
R.string.measurement_unit_temperature_base_imperial
)
}
fun formatEventQuantity(item: LunaEvent): String {
return if ((item.quantity ?: 0) > 0) {
numberFormat.format(item.quantity) + " " + when (item.type) {
val formatted = StringBuilder()
if ((item.quantity ?: 0) > 0) {
if (item.type == LunaEvent.TYPE_TEMPERATURE)
formatted.append((item.quantity / 10.0f).toString())
else
formatted.append(item.quantity)
formatted.append(" ")
formatted.append(
when (item.type) {
LunaEvent.TYPE_BABY_BOTTLE -> measurement_unit_liquid_base
LunaEvent.TYPE_WEIGHT -> measurement_unit_weight_base
LunaEvent.TYPE_MEDICINE -> measurement_unit_weight_tiny
LunaEvent.TYPE_TEMPERATURE -> measurement_unit_temperature_base
else -> ""
}
} else {
""
)
}
return formatted.toString()
}
/**
* Returns a valid quantity range for the event type.
* @return min, max, normal
*/
fun getValidEventQuantityRange(lunaEventType: String): Triple<Int, Int, Int>? {
val measurementSystem = LocaleData.getMeasurementSystem(ULocale.getDefault())
return when (lunaEventType) {
LunaEvent.TYPE_TEMPERATURE -> {
if (measurementSystem == LocaleData. MeasurementSystem.SI)
Triple(
context.resources.getInteger(R.integer.human_body_temp_min_metric),
context.resources.getInteger(R.integer.human_body_temp_max_metric),
context.resources.getInteger(R.integer.human_body_temp_normal_metric)
)
else
Triple(
context.resources.getInteger(R.integer.human_body_temp_min_imperial),
context.resources.getInteger(R.integer.human_body_temp_max_imperial),
context.resources.getInteger(R.integer.human_body_temp_normal_imperial)
)
}
else -> null
}
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@drawable/dropdown_list_item_background_pressed"/>
<item android:drawable="@drawable/dropdown_list_item_background_released"/>
</selector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="2dp"
android:color="@color/grey" />
<solid
android:color="@color/grey" />
<corners android:radius="15dp" />
<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />
</shape>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="2dp"
android:color="@color/grey" />
<solid
android:color="@color/cardview_dark_background"/>
<corners android:radius="15dp" />
<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />
</shape>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@ -6,18 +6,78 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:paddingTop="30dp"
android:paddingTop="10dp"
android:paddingLeft="15dp"
android:paddingRight="15dp">
<TextView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/button_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_gravity="end"
android:src="@drawable/ic_settings"
app:tint="@color/grey"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/title"
android:textSize="30sp"
android:gravity="center_horizontal"/>
android:textSize="26dp"
android:gravity="center"/>
<ImageView
android:id="@+id/button_sync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_gravity="start"
android:src="@drawable/ic_sync"
app:tint="@color/grey"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="38dp"
android:layout_margin="10dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/button_background">
<Spinner
android:id="@+id/logbooks_spinner"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
<TextView
android:id="@+id/logbooks_add_button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textStyle="bold"
android:textColor="@color/accent"
android:textSize="20dp"
android:text="+"
android:background="@drawable/button_background"/>
</LinearLayout>
<TextView
android:layout_width="match_parent"
@ -48,7 +108,7 @@
android:text="@string/event_bottle_type"/>
<TextView
android:id="@+id/button_scale"
android:id="@+id/button_food"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
@ -56,7 +116,7 @@
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="50dp"
android:text="@string/event_scale_type"/>
android:text="@string/event_food_type"/>
</LinearLayout>
@ -152,7 +212,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Diario di bordo"
android:text="@string/logbook"
android:textColor="@color/accent"
android:textStyle="bold"/>
@ -165,24 +225,6 @@
</LinearLayout>
<ImageView
android:id="@+id/button_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_gravity="end"
android:src="@drawable/ic_settings"
app:tint="@color/grey"/>
<ImageView
android:id="@+id/button_sync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_gravity="start"
android:src="@drawable/ic_sync"
app:tint="@color/grey"/>
<LinearLayout
android:id="@+id/no_connection_screen"
android:layout_width="match_parent"

View File

@ -122,6 +122,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginRight="20dp"
android:background="@drawable/button_background"
android:textColor="@color/accent"
android:text="@android:string/cancel"/>
<Button
@ -130,6 +133,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/accent"
android:background="@drawable/button_background"
android:text="@android:string/ok"/>
</LinearLayout>

View File

@ -0,0 +1,25 @@
<?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">
<TextView
android:id="@+id/dialog_add_logbook_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_add_logbook_message"/>
<EditText
android:id="@+id/dialog_add_logbook_logbookname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:lines="1"
android:inputType="text"
android:hint="@string/dialog_add_logbook_logbookname"
android:background="@drawable/textview_background"/>
</LinearLayout>

View File

@ -25,19 +25,23 @@
<TextView
android:id="@+id/dialog_event_detail_type_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginTop="20dp"
android:gravity="center_horizontal"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:drawableEnd="@drawable/ic_edit"
android:drawablePadding="10dp"
android:drawableTint="@color/accent"
android:textStyle="bold"
android:text="2020"/>
android:text="@string/dialog_event_detail_datetime_icon"/>
<TextView
android:id="@+id/dialog_event_detail_type_quantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="2020"/>
android:text="Quantity"/>
<ScrollView
android:layout_width="match_parent"
@ -49,7 +53,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="italic"
android:text="Lorem ipsum"/>
android:text="Notes"/>
</ScrollView>
</LinearLayout>

View File

@ -15,7 +15,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
android:background="@drawable/button_background"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_medicine"/>
@ -25,7 +25,7 @@
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/button_background"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_enema"/>
@ -35,27 +35,39 @@
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/button_background"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_note"/>
<LinearLayout
android:id="@+id/overflow_event_custom_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
<TextView
android:id="@+id/button_custom"
android:id="@+id/button_temperature"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/button_background"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_custom"/>
android:text="@string/overflow_event_temperature"/>
<TextView
android:id="@+id/button_colic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_colic"/>
<TextView
android:id="@+id/button_scale"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_scale"/>
</LinearLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="20dp"
android:paddingBottom="20dp"
android:singleLine="true"
android:ellipsize="end"/>

View File

@ -24,6 +24,7 @@
android:layout_weight="2"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/accent"
android:text="Description"/>
<TextView
@ -34,8 +35,6 @@
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:gravity="center_horizontal"
android:lines="1"
android:maxLines="1"
android:text="Qty"/>
<TextView

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/type"
android:layout_width="90dp"
android:layout_height="wrap_content"
android:textSize="28sp"
android:lines="1"
android:maxLines="1"
android:text="@string/event_diaperchange_pee_type"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:ellipsize="end"
android:maxLines="2"
android:gravity="center_horizontal"
android:textColor="@color/accent"
android:text="Description"/>
<TextView
android:id="@+id/quantity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:gravity="center_horizontal"
android:text="Qty"/>
<TextView
android:id="@+id/time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:gravity="center_horizontal"
android:textStyle="bold"
android:ellipsize="end"
android:maxLines="2"
android:text="Time"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<com.google.android.material.slider.Slider
android:id="@+id/dialog_temperature_value"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:valueFrom="@integer/human_body_temp_min_metric"
android:valueTo="@integer/human_body_temp_max_metric"
android:stepSize="0.1"
android:value="@integer/human_body_temp_normal_metric"
android:theme="@style/LTSlider"/>
<TextView
android:id="@+id/dialog_temperature_display"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
android:textColor="@color/accent"/>
</LinearLayout>

View File

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

View File

@ -10,12 +10,18 @@
<string name="log_weight_dialog_title">Pesata</string>
<string name="log_weight_dialog_description">Inserisci il peso rilevato</string>
<string name="log_temperature_dialog_title">Temperatura</string>
<string name="log_temperature_dialog_description">Inserisci la temperatura</string>
<string name="overflow_event_scale">⚖️ Peso</string>
<string name="overflow_event_medicine">💊 Medicina</string>
<string name="overflow_event_enema">🪠 Clistere</string>
<string name="overflow_event_note">📝 Nota</string>
<string name="overflow_event_custom"> Personalizzato</string>
<string name="overflow_event_temperature">🌡️ Temperatura</string>
<string name="overflow_event_colic">💨 Colichette</string>
<string name="event_bottle_desc">Biberon</string>
<string name="event_food_desc">Cibo</string>
<string name="event_scale_desc">Pesata</string>
<string name="event_breastfeeding_left_desc">Allatt. al seno (sx)</string>
<string name="event_breastfeeding_both_desc">Allatt. al seno</string>
@ -25,6 +31,8 @@
<string name="event_medicine_desc">Medicina</string>
<string name="event_enema_desc">Clistere</string>
<string name="event_note_desc">Nota</string>
<string name="event_temperature_desc">Temperatura</string>
<string name="event_colic_desc">Colichette</string>
<string name="event_unknown_desc"></string>
<string name="toast_event_added">Evento aggiunto</string>
@ -55,11 +63,13 @@
<string name="settings_storage_dav_pass">Password</string>
<string name="settings_network_error">Impossibile raggiungere il server: </string>
<string name="settings_webdav_error_denied">Nome utente o password WebDAV sbagliati</string>
<string name="settings_webdav_error_server_offline">Il server WebDAV non è al momento disponibile</string>
<string name="settings_webdav_error_generic">Si è verificato un errore tentando di accedere al server WebDAV:</string>
<string name="settings_webdav_creation_error_generic">Impossibile creare un file di salvataggio sul server WebDAV:</string>
<string name="settings_webdav_creation_ok">Connessione al server WebDAV avvenuta con successo</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_webdav_upload_error">Errore durante l\'upload del logbook locale %1$s su webdav: %2$s</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>
@ -73,5 +83,15 @@
<string name="log_notes_dialog_note_hint">Inserisci le note</string>
<string name="dialog_event_detail_title">Dettaglio evento</string>
<string name="dialog_event_detail_close_button">OK</string>
<string name="dialog_event_detail_delete_button">Elimina</string>
<string name="dialog_add_logbook_title">Aggiungi diario</string>
<string name="dialog_add_logbook_logbookname">👶 Nome del diario</string>
<string name="dialog_add_logbook_message">Scrivi un nome per identificare questo diario. Comparirà in cima allo schermo, e se usi WebDAV sarà incluso anche nel nome del file di salvataggio.\nSe vuoi un\'icona, inserisci una emoji!</string>
<string name="dialog_add_logbook_message_intro">Benvenuto! Per usare quest\'app devi creare almeno un diario. Probabilmente vuoi chiamarlo col nome di tuo figlio.</string>
<string name="default_logbook_name">👶 Il mio primo diario</string>
<string name="logbook_created">Creato nuovo diario: </string>
</resources>

View File

@ -8,4 +8,6 @@
<color name="grey">#ccc</color>
<color name="list_background_odd">#423B25</color>
<color name="list_background_even">@color/transparent</color>
<color name="danger">#f00</color>
<color name="textColor">@color/grey</color>
</resources>

View File

@ -10,7 +10,11 @@
<string name="log_weight_dialog_title">Weight</string>
<string name="log_weight_dialog_description">Insert the weight</string>
<string name="log_temperature_dialog_title">Temperature</string>
<string name="log_temperature_dialog_description">Insert the temperature</string>
<string name="event_bottle_type" translatable="false">🍼</string>
<string name="event_food_type" translatable="false">🥣</string>
<string name="event_scale_type" translatable="false">⚖️</string>
<string name="event_breastfeeding_left_type" translatable="false">🤱 ←</string>
<string name="event_breastfeeding_both_type" translatable="false">🤱 ↔</string>
@ -20,9 +24,12 @@
<string name="event_medicine_type" translatable="false">💊</string>
<string name="event_enema_type" translatable="false">🪠</string>
<string name="event_note_type" translatable="false">📝</string>
<string name="event_temperature_type" translatable="false">🌡️</string>
<string name="event_colic_type" translatable="false">💨</string>
<string name="event_unknown_type" translatable="false">\?</string>
<string name="event_bottle_desc">Baby bottle</string>
<string name="event_food_desc">Food</string>
<string name="event_scale_desc">Weight</string>
<string name="event_breastfeeding_left_desc">Breastfeeding (left)</string>
<string name="event_breastfeeding_both_desc">Breastfeeding</string>
@ -32,12 +39,16 @@
<string name="event_medicine_desc">Medicine</string>
<string name="event_enema_desc">Enema</string>
<string name="event_note_desc">Note</string>
<string name="event_temperature_desc">Temperature</string>
<string name="event_colic_desc">Gaseous colic</string>
<string name="event_unknown_desc"></string>
<string name="overflow_event_scale">⚖️ Weight</string>
<string name="overflow_event_medicine">💊 Medicine</string>
<string name="overflow_event_enema">🪠 Enema</string>
<string name="overflow_event_note">📝 Note</string>
<string name="overflow_event_custom"> Add custom</string>
<string name="overflow_event_temperature">🌡️ Temperature</string>
<string name="overflow_event_colic">💨 Gaseous colic</string>
<string name="toast_event_added">Event logged</string>
<string name="toast_logbook_saved">Logbook saved</string>
@ -60,22 +71,24 @@
<string name="settings_storage_local">On device</string>
<string name="settings_storage_local_desc">Most privacy-friendly solution: data doesn\'t leave your device</string>
<string name="settings_storage_dav">On a WebDAV server</string>
<string name="settings_storage_dav_desc">You can use any WebDAV service (i.e. Nextcloud, Owncloud, Dropbox, Box…) to save the data. In this way you can syncronize it between more devices, i.e. the dad\'s, the mom\'s, the grandma\'s… You will need the WebDAV url, you can find it in your provider\'s documentation (i.e. in Nextcloud web is in the Files app settings)</string>
<string name="settings_storage_dav_url">WebDAV url</string>
<string name="settings_storage_dav_desc">You can use any WebDAV service (i.e. Nextcloud, Owncloud, Dropbox, Box…) to save the data. In this way, you can syncronize it between more devices, i.e., the dad\'s, the mom\'s, the grandma\'s… You will need the WebDAV url, you can find it in your provider\'s documentation (i.e. in Nextcloud web is in the Files app settings)</string>
<string name="settings_storage_dav_url">WebDAV URL</string>
<string name="settings_storage_dav_url_hint">https://</string>
<string name="settings_storage_dav_user">Username</string>
<string name="settings_storage_dav_pass">Password</string>
<string name="settings_network_error">Unable to reach server: </string>
<string name="settings_webdav_error_denied">Wrong WebDAV user or password</string>
<string name="settings_webdav_error_server_offline">WebDAV server is currently unavailable</string>
<string name="settings_webdav_error_generic">Error while trying to access WebDAV:</string>
<string name="settings_webdav_creation_error_generic">Unable to save a file on the WebDAV server:</string>
<string name="settings_webdav_creation_ok">Successfully connected with WebDAV server</string>
<string name="settings_json_error">There\'s a save file on the server, but is corrupted or unreadable. Please delete it </string>
<string name="settings_webdav_creation_ok">Successfully connected with the WebDAV server</string>
<string name="settings_json_error">There\'s a save file on the server, but it is corrupted or unreadable. Please delete it </string>
<string name="settings_generic_error">Error: </string>
<string name="settings_webdav_upload_error">Error while uploading local logbook %1$s to webdav: %2$s</string>
<string name="trim_logbook_dialog_title">Your logbook is pretty big!</string>
<string name="trim_logbook_dialog_message_local">Your logbook file is growing a lot. We suggest trimming the oldest events to avoid crashes.</string>
<string name="trim_logbook_dialog_message_dav">Your logbook file is growing a lot. We suggest trimming the oldest events to avoid crashes. If you want to preserve all the events, please backup the "lunatracker_logbook.json" file on the WebDAV server or rename it to start a new logbook keeping the old one.</string>
<string name="trim_logbook_dialog_message_dav">Your logbook file is growing a lot. We suggest trimming the oldest events to avoid crashes. If you want to preserve all the events, please back up the "lunatracker_logbook.json" file on the WebDAV server or rename it to start a new logbook, keeping the old one.</string>
<string name="trim_logbook_dialog_button_ok">Trim it now</string>
<string name="trim_logbook_dialog_button_cancel">Remind me later</string>
@ -90,7 +103,20 @@
<string name="measurement_unit_liquid_base_imperial" translatable="false">fl oz.</string>
<string name="measurement_unit_weight_base_imperial" translatable="false">oz</string>
<string name="measurement_unit_weight_tiny_imperial" translatable="false">gr</string>
<string name="measurement_unit_temperature_base_imperial" translatable="false">°F</string>
<string name="measurement_unit_temperature_base_metric" translatable="false">°C</string>
<string name="dialog_event_detail_title">Event detail</string>
<string name="dialog_event_detail_datetime_icon" translatable="false">🕒 %s1</string>
<string name="dialog_event_detail_close_button">OK</string>
<string name="dialog_event_detail_delete_button">Delete</string>
<string name="dialog_add_logbook_title">Add logbook</string>
<string name="dialog_add_logbook_logbookname">👶 Logbook name</string>
<string name="dialog_add_logbook_message">Write a name to identify this logbook. This name will appear on top of the screen and, if you use WebDAV, will be in the save file name as well.</string>
<string name="dialog_add_logbook_message_intro">Welcome! To use this app, you need to create at least one logbook. You would probably want to call it with your child\'s name.</string>
<string name="default_logbook_name">👶 My first logbook</string>
<string name="logbook_created">New logbook created: </string>
</resources>

View File

@ -3,9 +3,15 @@
<style name="Theme.LunaTracker" parent="Theme.AppCompat.NoActionBar">
<item name="colorAccent">@color/accent</item>
<item name="android:textColor">@color/textColor</item>
</style>
<style name="OverflowMenuText">
<item name="android:textSize">20sp</item>
</style>
<style name="LTSlider" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="colorPrimary">@color/accent</item>
</style>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="human_body_temp_min_imperial">91</integer>
<integer name="human_body_temp_min_metric">33</integer>
<integer name="human_body_temp_max_imperial">109</integer>
<integer name="human_body_temp_max_metric">43</integer>
<integer name="human_body_temp_normal_imperial">98</integer>
<integer name="human_body_temp_normal_metric">37</integer>
</resources>

View File

@ -0,0 +1,3 @@
Multiple children support
Fixed interface in devices with big font size

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,3 @@
LunaTracker è un'app di tracciamento neonatale
Essere genitori può essere dura. Arrivate a casa dall'ospedale, esausti, con questo cosino fragile e sconosciuto che non ha un manuale d'uso, e ha un solo modo per segnalare che c'è qualcosa che non va: piangere.
Dovete essere sempre pronti a reagire, anche se sono le 4 di mattina e non avete idea del perché stia piangendo.