24 Commits
v0.4 ... master

Author SHA1 Message Date
30dc958a0c Merge pull request 'added french language translation and corrected a few english typos' (#6) from chepycou/luna-tracker:master into master
Reviewed-on: penguin86/luna-tracker#6
2025-07-02 13:22:15 +02:00
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
29 changed files with 824 additions and 167 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@ -1,5 +1,8 @@
package it.danieleverducci.lunatracker package it.danieleverducci.lunatracker
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@ -7,13 +10,17 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText import android.widget.EditText
import android.widget.NumberPicker import android.widget.NumberPicker
import android.widget.PopupWindow import android.widget.PopupWindow
import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
@ -24,6 +31,7 @@ import it.danieleverducci.lunatracker.entities.Logbook
import it.danieleverducci.lunatracker.entities.LunaEvent import it.danieleverducci.lunatracker.entities.LunaEvent
import it.danieleverducci.lunatracker.repository.FileLogbookRepository import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.LogbookLoadedListener import it.danieleverducci.lunatracker.repository.LogbookLoadedListener
import it.danieleverducci.lunatracker.repository.LogbookRepository import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.LogbookSavedListener import it.danieleverducci.lunatracker.repository.LogbookSavedListener
@ -33,6 +41,7 @@ import okio.IOException
import org.json.JSONException import org.json.JSONException
import utils.NumericUtils import utils.NumericUtils
import java.text.DateFormat import java.text.DateFormat
import java.util.Calendar
import java.util.Date import java.util.Date
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@ -42,15 +51,16 @@ class MainActivity : AppCompatActivity() {
val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false val DEBUG_CHECK_LOGBOOK_CONSISTENCY = false
} }
lateinit var logbook: Logbook var logbook: Logbook? = null
lateinit var adapter: LunaEventRecyclerAdapter var pauseLogbookUpdate = false
lateinit var progressIndicator: LinearProgressIndicator lateinit var progressIndicator: LinearProgressIndicator
lateinit var buttonsContainer: ViewGroup lateinit var buttonsContainer: ViewGroup
lateinit var recyclerView: RecyclerView lateinit var recyclerView: RecyclerView
lateinit var handler: Handler lateinit var handler: Handler
var savingEvent = false var savingEvent = false
val updateListRunnable: Runnable = Runnable { val updateListRunnable: Runnable = Runnable {
loadLogbook() if (logbook != null && !pauseLogbookUpdate)
loadLogbook(logbook!!.name)
handler.postDelayed(updateListRunnable, 1000*60) handler.postDelayed(updateListRunnable, 1000*60)
} }
var logbookRepo: LogbookRepository? = null var logbookRepo: LogbookRepository? = null
@ -60,12 +70,6 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
handler = Handler(mainLooper) handler = Handler(mainLooper)
adapter = LunaEventRecyclerAdapter(this)
adapter.onItemClickListener = object: LunaEventRecyclerAdapter.OnItemClickListener{
override fun onItemClick(event: LunaEvent) {
showEventDetailDialog(event)
}
}
// Show view // Show view
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
@ -74,11 +78,11 @@ class MainActivity : AppCompatActivity() {
buttonsContainer = findViewById<ViewGroup>(R.id.buttons_container) buttonsContainer = findViewById<ViewGroup>(R.id.buttons_container)
recyclerView = findViewById<RecyclerView>(R.id.list_events) recyclerView = findViewById<RecyclerView>(R.id.list_events)
recyclerView.setLayoutManager(LinearLayoutManager(applicationContext)) recyclerView.setLayoutManager(LinearLayoutManager(applicationContext))
recyclerView.adapter = adapter
// Set listeners // 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_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( findViewById<View>(R.id.button_nipple_left).setOnClickListener { logEvent(
LunaEvent( LunaEvent(
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE
@ -115,13 +119,24 @@ class MainActivity : AppCompatActivity() {
showSettings() showSettings()
}) })
findViewById<View>(R.id.button_no_connection_retry).setOnClickListener({ 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({ 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() { fun showSettings() {
val i = Intent(this, SettingsActivity::class.java) val i = Intent(this, SettingsActivity::class.java)
startActivity(i) startActivity(i)
@ -129,9 +144,10 @@ class MainActivity : AppCompatActivity() {
fun showLogbook() { fun showLogbook() {
// Show logbook // Show logbook
adapter.items.clear() if (logbook == null)
adapter.items.addAll(logbook.logs) Log.w(TAG, "showLogbook(): logbook is null!")
adapter.notifyDataSetChanged()
setListAdapter(logbook?.logs ?: arrayListOf())
} }
override fun onStart() { override fun onStart() {
@ -153,10 +169,15 @@ class MainActivity : AppCompatActivity() {
} }
// Update list dates // Update list dates
adapter.notifyDataSetChanged() recyclerView.adapter?.notifyDataSetChanged()
// Reload data if (logbook != null) {
loadLogbook() // Already running: reload data for currently selected logbook
loadLogbook(logbook!!.name)
} else {
// First start: load logbook list
loadLogbookList()
}
} }
override fun onStop() { override fun onStop() {
@ -236,12 +257,7 @@ class MainActivity : AppCompatActivity() {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_notes, null) val dialogView = layoutInflater.inflate(R.layout.dialog_notes, null)
d.setTitle(lunaEvent.getTypeDescription(this)) d.setTitle(lunaEvent.getTypeDescription(this))
d.setMessage( d.setMessage(lunaEvent.getDialogMessage(this))
when (lunaEvent.type){
LunaEvent.TYPE_MEDICINE -> R.string.log_medicine_dialog_description
else -> R.string.log_notes_dialog_description
}
)
d.setView(dialogView) d.setView(dialogView)
val notesET = dialogView.findViewById<EditText>(R.id.notes_edittext) val notesET = dialogView.findViewById<EditText>(R.id.notes_edittext)
val qtyET = dialogView.findViewById<EditText>(R.id.notes_qty_edittext) val qtyET = dialogView.findViewById<EditText>(R.id.notes_qty_edittext)
@ -276,7 +292,7 @@ class MainActivity : AppCompatActivity() {
} }
) )
d.setPositiveButton(R.string.trim_logbook_dialog_button_ok) { dialogInterface, i -> d.setPositiveButton(R.string.trim_logbook_dialog_button_ok) { dialogInterface, i ->
logbook.trim() logbook?.trim()
saveLogbook() saveLogbook()
} }
d.setNegativeButton(R.string.trim_logbook_dialog_button_cancel) { dialogInterface, i -> d.setNegativeButton(R.string.trim_logbook_dialog_button_cancel) { dialogInterface, i ->
@ -287,24 +303,200 @@ class MainActivity : AppCompatActivity() {
} }
fun showEventDetailDialog(event: LunaEvent) { 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 dateFormat = DateFormat.getDateTimeInstance();
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
d.setTitle(R.string.dialog_event_detail_title) d.setTitle(R.string.dialog_event_detail_title)
val dialogView = layoutInflater.inflate(R.layout.dialog_event_detail, null) val dialogView = layoutInflater.inflate(R.layout.dialog_event_detail, null)
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_emoji).setText(event.getTypeEmoji(this)) 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_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( dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_quantity).setText(
NumericUtils(this).formatEventQuantity(event) NumericUtils(this).formatEventQuantity(event)
) )
dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_notes).setText(event.notes) dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_notes).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.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() val alertDialog = d.create()
alertDialog.show() 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) if (savingEvent)
return return
@ -314,7 +506,7 @@ class MainActivity : AppCompatActivity() {
// Load data // Load data
setLoading(true) setLoading(true)
logbookRepo?.loadLogbook(this, object: LogbookLoadedListener{ logbookRepo?.loadLogbook(this, name, object: LogbookLoadedListener{
override fun onLogbookLoaded(lb: Logbook) { override fun onLogbookLoaded(lb: Logbook) {
runOnUiThread({ runOnUiThread({
setLoading(false) setLoading(false)
@ -323,7 +515,7 @@ class MainActivity : AppCompatActivity() {
showLogbook() showLogbook()
if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) { if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) {
for (e in logbook.logs) { for (e in logbook?.logs ?: listOf()) {
val em = e.getTypeEmoji(this@MainActivity) val em = e.getTypeEmoji(this@MainActivity)
if (em == getString(R.string.event_unknown_type)) { if (em == getString(R.string.event_unknown_type)) {
Log.e(TAG, "UNKNOWN: ${e.type}") Log.e(TAG, "UNKNOWN: ${e.type}")
@ -346,6 +538,8 @@ class MainActivity : AppCompatActivity() {
onRepoError( onRepoError(
if(error.toString().contains("401")) { if(error.toString().contains("401")) {
getString(R.string.settings_webdav_error_denied) getString(R.string.settings_webdav_error_denied)
} else if(error.toString().contains("503")) {
getString(R.string.settings_webdav_error_server_offline)
} else { } else {
getString(R.string.settings_webdav_error_generic) + error.toString() getString(R.string.settings_webdav_error_generic) + error.toString()
} }
@ -380,26 +574,40 @@ class MainActivity : AppCompatActivity() {
fun logEvent(event: LunaEvent) { fun logEvent(event: LunaEvent) {
savingEvent(true) savingEvent(true)
adapter.items.add(0, event)
adapter.notifyItemInserted(0)
recyclerView.smoothScrollToPosition(0)
setLoading(true) setLoading(true)
logbook.logs.add(0, event) logbook?.logs?.add(0, event)
recyclerView.adapter?.notifyItemInserted(0)
recyclerView.smoothScrollToPosition(0)
saveLogbook(event) saveLogbook(event)
// Check logbook size to avoid OOM errors // Check logbook size to avoid OOM errors
if (logbook.isTooBig()) { if (logbook?.isTooBig() == true) {
askToTrimLogbook() 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 * Saves the logbook. If saving while adding an event, please specify the event so in case
* of error can be removed from the list. * of error can be removed from the list.
*/ */
fun saveLogbook(lastEventAdded: LunaEvent? = null) { 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() { override fun onLogbookSaved() {
Log.d(TAG, "Logbook saved") Log.d(TAG, "Logbook saved")
runOnUiThread({ runOnUiThread({
@ -432,6 +640,8 @@ class MainActivity : AppCompatActivity() {
onRepoError( onRepoError(
if(error.toString().contains("401")) { if(error.toString().contains("401")) {
getString(R.string.settings_webdav_error_denied) getString(R.string.settings_webdav_error_denied)
} else if(error.toString().contains("503")) {
getString(R.string.settings_webdav_error_server_offline)
} else { } else {
getString(R.string.settings_webdav_error_generic) + error.toString() getString(R.string.settings_webdav_error_generic) + error.toString()
} }
@ -467,8 +677,7 @@ class MainActivity : AppCompatActivity() {
setLoading(false) setLoading(false)
Toast.makeText(this@MainActivity, R.string.toast_event_add_error, Toast.LENGTH_SHORT).show() Toast.makeText(this@MainActivity, R.string.toast_event_add_error, Toast.LENGTH_SHORT).show()
adapter.items.remove(event) recyclerView.adapter?.notifyDataSetChanged()
adapter.notifyDataSetChanged()
savingEvent(false) savingEvent(false)
}) })
} }
@ -521,6 +730,10 @@ class MainActivity : AppCompatActivity() {
) )
dismiss() dismiss()
}) })
contentView.findViewById<View>(R.id.button_scale).setOnClickListener({
askWeightValue()
dismiss()
})
}.also { popupWindow -> }.also { popupWindow ->
popupWindow.setOnDismissListener({ popupWindow.setOnDismissListener({
Handler(mainLooper).postDelayed({ Handler(mainLooper).postDelayed({

View File

@ -8,7 +8,10 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.thegrizzlylabs.sardineandroid.impl.SardineException import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.repository.FileLogbookRepository
import it.danieleverducci.lunatracker.repository.LocalSettingsRepository import it.danieleverducci.lunatracker.repository.LocalSettingsRepository
import it.danieleverducci.lunatracker.repository.LogbookListObtainedListener
import it.danieleverducci.lunatracker.repository.LogbookRepository
import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository import it.danieleverducci.lunatracker.repository.WebDAVLogbookRepository
import okio.IOException import okio.IOException
import org.json.JSONException import org.json.JSONException
@ -72,14 +75,35 @@ open class SettingsActivity : AppCompatActivity() {
textViewWebDAVPass.text.toString() textViewWebDAVPass.text.toString()
) )
progressIndicator.visibility = View.VISIBLE 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({ runOnUiThread({
progressIndicator.visibility = View.INVISIBLE progressIndicator.visibility = View.INVISIBLE
saveSettings() saveSettings()
Toast.makeText(this@SettingsActivity, R.string.settings_webdav_creation_ok, Toast.LENGTH_SHORT).show() Toast.makeText(this@SettingsActivity, R.string.settings_webdav_creation_ok, Toast.LENGTH_SHORT).show()
}) })
} }
}
override fun onIOError(error: IOException) { override fun onIOError(error: IOException) {
runOnUiThread({ 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) { override fun onJSONError(error: JSONException) {
runOnUiThread({ runOnUiThread({
progressIndicator.visibility = View.INVISIBLE 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() { fun saveSettings() {
@ -129,4 +157,32 @@ open class SettingsActivity : AppCompatActivity() {
finish() 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

@ -14,13 +14,14 @@ import utils.NumericUtils
class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.LunaEventVH> { class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.LunaEventVH> {
private val context: Context private val context: Context
val items = ArrayList<LunaEvent>() private val items: ArrayList<LunaEvent>
val numericUtils: NumericUtils val numericUtils: NumericUtils
var onItemClickListener: OnItemClickListener? = null var onItemClickListener: OnItemClickListener? = null
val layoutRes: Int val layoutRes: Int
constructor(context: Context) { constructor(context: Context, items: ArrayList<LunaEvent>) {
this.context = context this.context = context
this.items = items
this.numericUtils = NumericUtils(context) this.numericUtils = NumericUtils(context)
val fontScale = context.resources.configuration.fontScale val fontScale = context.resources.configuration.fontScale

View File

@ -1,6 +1,6 @@
package it.danieleverducci.lunatracker.entities package it.danieleverducci.lunatracker.entities
class Logbook { class Logbook(val name: String) {
companion object { companion object {
val MAX_SAFE_LOGBOOK_SIZE = 30000 val MAX_SAFE_LOGBOOK_SIZE = 30000
} }
@ -16,4 +16,8 @@ class Logbook {
fun trim() { fun trim() {
logs.subList(MAX_SAFE_LOGBOOK_SIZE/2, logs.size).clear() 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 * allow expandability and backwards compatibility (if a field is added in a
* release, it is simply ignored by previous ones). * release, it is simply ignored by previous ones).
*/ */
class LunaEvent { class LunaEvent: Comparable<LunaEvent> {
companion object { companion object {
val TYPE_BABY_BOTTLE = "BABY_BOTTLE" val TYPE_BABY_BOTTLE = "BABY_BOTTLE"
@ -27,6 +27,7 @@ class LunaEvent {
val TYPE_CUSTOM = "CUSTOM" val TYPE_CUSTOM = "CUSTOM"
val TYPE_COLIC = "COLIC" val TYPE_COLIC = "COLIC"
val TYPE_TEMPERATURE = "TEMPERATURE" val TYPE_TEMPERATURE = "TEMPERATURE"
val TYPE_FOOD = "FOOD"
} }
private val jo: JSONObject private val jo: JSONObject
@ -88,6 +89,7 @@ class LunaEvent {
TYPE_NOTE -> R.string.event_note_type TYPE_NOTE -> R.string.event_note_type
TYPE_TEMPERATURE -> R.string.event_temperature_type TYPE_TEMPERATURE -> R.string.event_temperature_type
TYPE_COLIC -> R.string.event_colic_type TYPE_COLIC -> R.string.event_colic_type
TYPE_FOOD -> R.string.event_food_type
else -> R.string.event_unknown_type else -> R.string.event_unknown_type
} }
) )
@ -108,11 +110,19 @@ class LunaEvent {
TYPE_NOTE -> R.string.event_note_desc TYPE_NOTE -> R.string.event_note_desc
TYPE_TEMPERATURE -> R.string.event_temperature_desc TYPE_TEMPERATURE -> R.string.event_temperature_desc
TYPE_COLIC -> R.string.event_colic_desc TYPE_COLIC -> R.string.event_colic_desc
TYPE_FOOD -> R.string.event_food_desc
else -> R.string.event_unknown_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 { fun toJson(): JSONObject {
return jo return jo
} }
@ -120,4 +130,8 @@ class LunaEvent {
override fun toString(): String { override fun toString(): String {
return "${type} qty: $quantity time: ${Date(time * 1000)}" return "${type} qty: $quantity time: ${Date(time * 1000)}"
} }
override fun compareTo(other: LunaEvent): Int {
return (this.time - other.time).toInt()
}
} }

View File

@ -9,26 +9,30 @@ import org.json.JSONException
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.FilenameFilter
class FileLogbookRepository: LogbookRepository { class FileLogbookRepository: LogbookRepository {
companion object { companion object {
val TAG = "FileLogbookRepository" 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 { try {
listener.onLogbookLoaded(loadLogbook(context)) listener.onLogbookLoaded(loadLogbook(context, name))
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
Log.d(TAG, "No logbook file found, create one") Log.d(TAG, "No logbook file found, create one")
val newLogbook = Logbook() val newLogbook = Logbook(name)
saveLogbook(context, newLogbook) saveLogbook(context, newLogbook)
listener.onLogbookLoaded(newLogbook) listener.onLogbookLoaded(newLogbook)
} }
} }
fun loadLogbook(context: Context): Logbook { fun loadLogbook(context: Context, name: String): Logbook {
val logbook = Logbook() val logbook = Logbook(name)
val file = File(context.getFilesDir(), "data.json") val fileName = getFileName(name)
val file = File(context.getFilesDir(), fileName)
val json = FileInputStream(file).bufferedReader().use { it.readText() } val json = FileInputStream(file).bufferedReader().use { it.readText() }
val ja = JSONArray(json) val ja = JSONArray(json)
for (i in 0 until ja.length()) { for (i in 0 until ja.length()) {
@ -53,11 +57,55 @@ class FileLogbookRepository: LogbookRepository {
} }
fun saveLogbook(context: Context, logbook: Logbook) { 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() val ja = JSONArray()
for (l in logbook.logs) { for (l in logbook.logs) {
ja.put(l.toJson()) ja.put(l.toJson())
} }
file.writeText(ja.toString()) 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 import org.json.JSONException
interface LogbookRepository { interface LogbookRepository {
fun loadLogbook(context: Context, listener: LogbookLoadedListener) companion object {
fun saveLogbook(context: Context, logbook: Logbook, listener: LogbookSavedListener) 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 { interface LogbookLoadedListener {
@ -26,3 +30,10 @@ interface LogbookSavedListener {
fun onJSONError(error: JSONException) fun onJSONError(error: JSONException)
fun onError(error: Exception) 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.content.Context
import android.util.Log import android.util.Log
import com.thegrizzlylabs.sardineandroid.DavResource
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import com.thegrizzlylabs.sardineandroid.impl.SardineException import com.thegrizzlylabs.sardineandroid.impl.SardineException
import it.danieleverducci.lunatracker.entities.Logbook import it.danieleverducci.lunatracker.entities.Logbook
@ -14,11 +15,13 @@ import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import kotlin.io.bufferedReader import kotlin.io.bufferedReader
import kotlin.text.replace
class WebDAVLogbookRepository(val webDavURL: String, val username: String, val password: String): LogbookRepository { class WebDAVLogbookRepository(val webDavURL: String, val username: String, val password: String): LogbookRepository {
companion object { companion object {
val TAG = "LogbookRepository" val TAG = "LogbookRepository"
val FILE_NAME = "lunatracker_logbook.json" val FILE_NAME_START = "lunatracker_logbook"
val FILE_NAME_END = ".json"
} }
val sardine: OkHttpSardine = OkHttpSardine() 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 { Thread(Runnable {
try { try {
val logbook = loadLogbook(context) val logbook = loadLogbook(name)
listener.onLogbookLoaded(logbook) listener.onLogbookLoaded(logbook)
} catch (e: SardineException) { } catch (e: SardineException) {
Log.e(TAG, e.toString()) Log.e(TAG, e.toString())
@ -52,12 +55,12 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start() }).start()
} }
private fun loadLogbook(context: Context): Logbook { private fun loadLogbook(name: String,): Logbook {
val inputStream = sardine.get("$webDavURL/$FILE_NAME") val inputStream = sardine.get(getUrl(name))
val json = inputStream.bufferedReader().use(BufferedReader::readText) val json = inputStream.bufferedReader().use(BufferedReader::readText)
inputStream.close() inputStream.close()
val ja = JSONArray(json) val ja = JSONArray(json)
val logbook = Logbook() val logbook = Logbook(name)
for (i in 0 until ja.length()) { for (i in 0 until ja.length()) {
try { try {
val evt: LunaEvent = LunaEvent(ja.getJSONObject(i)) val evt: LunaEvent = LunaEvent(ja.getJSONObject(i))
@ -95,6 +98,40 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}).start() }).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) { private fun saveLogbook(context: Context, logbook: Logbook) {
// Lock logbook on WebDAV to avoid concurrent changes // Lock logbook on WebDAV to avoid concurrent changes
//sardine.lock(getUrl()) //sardine.lock(getUrl())
@ -108,64 +145,56 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
for (l in logbook.logs) { for (l in logbook.logs) {
ja.put(l.toJson()) 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. * 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) { fun uploadLogbookIfNotExists(context: Context, name: String): String? {
Thread(Runnable { val flr = FileLogbookRepository()
try { try {
loadLogbook(context) loadLogbook(name)
listener.onLogbookCreated() Log.d(TAG, "Logbook file $name already exist on the webDav share: will not overwrite it")
return null
} catch (e: SardineException) { } catch (e: SardineException) {
if (e.toString().contains("404")) { 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 { try {
val flr = FileLogbookRepository() val logbook = flr.loadLogbook(context, name)
val logbook = flr.loadLogbook(context)
saveLogbook(context, logbook) saveLogbook(context, logbook)
Log.d(TAG, "Local logbook file found, uploaded") Log.d(TAG, "Local logbook file $name found, uploaded")
listener.onLogbookCreated() return null
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
Log.d(TAG, "No local logbook file found, uploading empty file") Log.e(TAG, "No local logbook file found, this should not happen!")
saveLogbook(context, Logbook()) return "No local logbook file found, app is in inconsistent state, please delete and reinstall it"
listener.onLogbookCreated()
} catch (e: SardineException) { } catch (e: SardineException) {
Log.e(TAG, "Unable to upload logbook: $e") Log.e(TAG, "Unable to upload logbook: $e")
listener.onWebDAVError(e) return e.toString()
} }
} else { } else {
Log.e(TAG, e.toString()) Log.e(TAG, e.toString())
listener.onWebDAVError(e) return e.toString()
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, e.toString()) Log.e(TAG, e.toString())
listener.onIOError(e) return e.toString()
} catch (e: SocketTimeoutException) { } catch (e: SocketTimeoutException) {
Log.e(TAG, e.toString()) Log.e(TAG, e.toString())
listener.onIOError(e) return e.toString()
} catch (e: JSONException) { } catch (e: JSONException) {
Log.e(TAG, e.toString()) Log.e(TAG, e.toString())
listener.onJSONError(e) return e.toString()
} catch (e: Exception) { } catch (e: Exception) {
listener.onError(e) return e.toString()
} }
}).start()
} }
private fun getUrl(): String { private fun getUrl(name: String): String {
return "$webDavURL/$FILE_NAME" val fileName = "${FILE_NAME_START}${if (name.isNotEmpty()) "_" else ""}${name}${FILE_NAME_END}"
} Log.d(TAG, fileName)
return "$webDavURL/$fileName"
interface LogbookCreatedListener {
fun onLogbookCreated()
fun onIOError(error: okio.IOException)
fun onWebDAVError(error: SardineException)
fun onJSONError(error: JSONException)
fun onError(error: Exception)
} }
} }

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

@ -7,16 +7,77 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="30dp" android:paddingTop="10dp"
android:paddingLeft="15dp" android:paddingLeft="15dp"
android:paddingRight="15dp"> android:paddingRight="15dp">
<TextView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" 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:text="@string/title"
android:textSize="30dp" android:textSize="26dp"
android:gravity="center_horizontal"/> 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 <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -47,7 +108,7 @@
android:text="@string/event_bottle_type"/> android:text="@string/event_bottle_type"/>
<TextView <TextView
android:id="@+id/button_scale" android:id="@+id/button_food"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
@ -55,7 +116,7 @@
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:textSize="50dp" android:textSize="50dp"
android:text="@string/event_scale_type"/> android:text="@string/event_food_type"/>
</LinearLayout> </LinearLayout>
@ -164,24 +225,6 @@
</LinearLayout> </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 <LinearLayout
android:id="@+id/no_connection_screen" android:id="@+id/no_connection_screen"
android:layout_width="match_parent" android:layout_width="match_parent"

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 <TextView
android:id="@+id/dialog_event_detail_type_date" android:id="@+id/dialog_event_detail_type_date"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="40dp"
android:layout_marginTop="20dp" 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:textStyle="bold"
android:text="2020"/> android:text="@string/dialog_event_detail_datetime_icon"/>
<TextView <TextView
android:id="@+id/dialog_event_detail_type_quantity" android:id="@+id/dialog_event_detail_type_quantity"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:text="2020"/> android:text="Quantity"/>
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -49,7 +53,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textStyle="italic" android:textStyle="italic"
android:text="Lorem ipsum"/> android:text="Notes"/>
</ScrollView> </ScrollView>
</LinearLayout> </LinearLayout>

View File

@ -15,7 +15,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="20dp" android:padding="20dp"
android:background="@drawable/button_background" android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText" style="@style/OverflowMenuText"
android:text="@string/overflow_event_medicine"/> android:text="@string/overflow_event_medicine"/>
@ -25,7 +25,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:padding="20dp" android:padding="20dp"
android:background="@drawable/button_background" android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText" style="@style/OverflowMenuText"
android:text="@string/overflow_event_enema"/> android:text="@string/overflow_event_enema"/>
@ -35,7 +35,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:padding="20dp" android:padding="20dp"
android:background="@drawable/button_background" android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText" style="@style/OverflowMenuText"
android:text="@string/overflow_event_note"/> android:text="@string/overflow_event_note"/>
@ -45,7 +45,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:padding="20dp" android:padding="20dp"
android:background="@drawable/button_background" android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText" style="@style/OverflowMenuText"
android:text="@string/overflow_event_temperature"/> android:text="@string/overflow_event_temperature"/>
@ -55,10 +55,20 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:padding="20dp" android:padding="20dp"
android:background="@drawable/button_background" android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText" style="@style/OverflowMenuText"
android:text="@string/overflow_event_colic"/> android:text="@string/overflow_event_colic"/>
<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> </LinearLayout>
</ScrollView> </ScrollView>

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

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

@ -13,6 +13,7 @@
<string name="log_temperature_dialog_title">Temperatura</string> <string name="log_temperature_dialog_title">Temperatura</string>
<string name="log_temperature_dialog_description">Inserisci la 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_medicine">💊 Medicina</string>
<string name="overflow_event_enema">🪠 Clistere</string> <string name="overflow_event_enema">🪠 Clistere</string>
<string name="overflow_event_note">📝 Nota</string> <string name="overflow_event_note">📝 Nota</string>
@ -20,6 +21,7 @@
<string name="overflow_event_colic">💨 Colichette</string> <string name="overflow_event_colic">💨 Colichette</string>
<string name="event_bottle_desc">Biberon</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_scale_desc">Pesata</string>
<string name="event_breastfeeding_left_desc">Allatt. al seno (sx)</string> <string name="event_breastfeeding_left_desc">Allatt. al seno (sx)</string>
<string name="event_breastfeeding_both_desc">Allatt. al seno</string> <string name="event_breastfeeding_both_desc">Allatt. al seno</string>
@ -61,11 +63,13 @@
<string name="settings_storage_dav_pass">Password</string> <string name="settings_storage_dav_pass">Password</string>
<string name="settings_network_error">Impossibile raggiungere il server: </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_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_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_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_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_json_error">Sul server esiste un salvataggio, ma è corrotto o illeggibile. Cancellare il file </string>
<string name="settings_generic_error">Si è verificato un errore: </string> <string name="settings_generic_error">Si è verificato un errore: </string>
<string name="settings_webdav_upload_error">Errore durante l\'upload del logbook locale %1$s su webdav: %2$s</string>
<string name="trim_logbook_dialog_title">Il tuo diario è bello grande!</string> <string name="trim_logbook_dialog_title">Il tuo diario è bello grande!</string>
<string name="trim_logbook_dialog_message_local">Il file del tuo diario sta crescendo molto. Ti suggeriamo di cancellare gli eventi più vecchi per evitare problemi di memoria.</string> <string name="trim_logbook_dialog_message_local">Il file del tuo diario sta crescendo molto. Ti suggeriamo di cancellare gli eventi più vecchi per evitare problemi di memoria.</string>
@ -79,5 +83,15 @@
<string name="log_notes_dialog_note_hint">Inserisci le note</string> <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_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> </resources>

View File

@ -14,6 +14,7 @@
<string name="log_temperature_dialog_description">Insert the temperature</string> <string name="log_temperature_dialog_description">Insert the temperature</string>
<string name="event_bottle_type" translatable="false">🍼</string> <string name="event_bottle_type" translatable="false">🍼</string>
<string name="event_food_type" translatable="false">🥣</string>
<string name="event_scale_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_left_type" translatable="false">🤱 ←</string>
<string name="event_breastfeeding_both_type" translatable="false">🤱 ↔</string> <string name="event_breastfeeding_both_type" translatable="false">🤱 ↔</string>
@ -28,6 +29,7 @@
<string name="event_unknown_type" translatable="false">\?</string> <string name="event_unknown_type" translatable="false">\?</string>
<string name="event_bottle_desc">Baby bottle</string> <string name="event_bottle_desc">Baby bottle</string>
<string name="event_food_desc">Food</string>
<string name="event_scale_desc">Weight</string> <string name="event_scale_desc">Weight</string>
<string name="event_breastfeeding_left_desc">Breastfeeding (left)</string> <string name="event_breastfeeding_left_desc">Breastfeeding (left)</string>
<string name="event_breastfeeding_both_desc">Breastfeeding</string> <string name="event_breastfeeding_both_desc">Breastfeeding</string>
@ -41,6 +43,7 @@
<string name="event_colic_desc">Gaseous colic</string> <string name="event_colic_desc">Gaseous colic</string>
<string name="event_unknown_desc"></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_medicine">💊 Medicine</string>
<string name="overflow_event_enema">🪠 Enema</string> <string name="overflow_event_enema">🪠 Enema</string>
<string name="overflow_event_note">📝 Note</string> <string name="overflow_event_note">📝 Note</string>
@ -68,22 +71,24 @@
<string name="settings_storage_local">On device</string> <string name="settings_storage_local">On device</string>
<string name="settings_storage_local_desc">Most privacy-friendly solution: data doesn\'t leave your device</string> <string name="settings_storage_local_desc">Most privacy-friendly solution: data doesn\'t leave your device</string>
<string name="settings_storage_dav">On a WebDAV server</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_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">WebDAV URL</string>
<string name="settings_storage_dav_url_hint">https://</string> <string name="settings_storage_dav_url_hint">https://</string>
<string name="settings_storage_dav_user">Username</string> <string name="settings_storage_dav_user">Username</string>
<string name="settings_storage_dav_pass">Password</string> <string name="settings_storage_dav_pass">Password</string>
<string name="settings_network_error">Unable to reach server: </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_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_error_generic">Error while trying to access WebDAV:</string>
<string name="settings_webdav_creation_error_generic">Unable to save a file on the WebDAV server:</string> <string name="settings_webdav_creation_error_generic">Unable to save a file on the WebDAV server:</string>
<string name="settings_webdav_creation_ok">Successfully connected with WebDAV server</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 is corrupted or unreadable. Please delete it </string> <string name="settings_json_error">There\'s a save file on the server, but it is corrupted or unreadable. Please delete it </string>
<string name="settings_generic_error">Error: </string> <string name="settings_generic_error">Error: </string>
<string name="settings_webdav_upload_error">Error while uploading local logbook %1$s to webdav: %2$s</string>
<string name="trim_logbook_dialog_title">Your logbook is pretty big!</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_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_ok">Trim it now</string>
<string name="trim_logbook_dialog_button_cancel">Remind me later</string> <string name="trim_logbook_dialog_button_cancel">Remind me later</string>
@ -102,5 +107,16 @@
<string name="measurement_unit_temperature_base_metric" translatable="false">°C</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_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> </resources>

View File

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

View File

@ -1,11 +1,11 @@
Parenting can be tough. You get home from the hospital, exhausted, with this little fragile unknown thingy that has no user manual and a single way to let you know something's not ok: crying. Parenting can be tough. You get home from the hospital, exhausted, with this little fragile unknown thingy that has no user manual and a single way to let you know something's not ok: crying.
You have to react fast, even if it's 4 AM and you have no idea why is crying. You have to react fast, even if it's 4 AM and you have no idea of the reason behind the cries.
This app is meant to log all the relevant events (diaper change, breastfeeding, baby bottle feeding...), so you can always remember when has been done last time. It supports syncing the data between different devices (using your WebDAV server for the best privacy) so that when the baby needs attentions, the dad can step in leaving the mom sleeping peacefully. This app is meant to log all the relevant events (diaper change, breastfeeding, baby bottle feeding, etc.), so you can always remember what was done last time and when. It supports syncing the data between different devices (using your WebDAV server for the best privacy) so that when the baby needs attention, the dad can step in, leaving the mom sleeping peacefully.
Dedicated to my daughter Luna. 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 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 🌛