forked from penguin86/luna-tracker
Compare commits
13 Commits
multiple-c
...
master
Author | SHA1 | Date | |
---|---|---|---|
30dc958a0c | |||
1de99e89e5 | |||
05c34178a4 | |||
0d3be20e1e | |||
4c5c7bcf1a | |||
07393faf41 | |||
043a1c0283 | |||
9fddd37fe9 | |||
efdb8f584a | |||
1511764497 | |||
744bfef62b | |||
34ca6c1cd6 | |||
b4f47ea6bb |
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
@ -18,7 +21,6 @@ import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
@ -39,6 +41,7 @@ import okio.IOException
|
||||
import org.json.JSONException
|
||||
import utils.NumericUtils
|
||||
import java.text.DateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@ -48,15 +51,16 @@ class MainActivity : AppCompatActivity() {
|
||||
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(logbook.name)
|
||||
if (logbook != null && !pauseLogbookUpdate)
|
||||
loadLogbook(logbook!!.name)
|
||||
handler.postDelayed(updateListRunnable, 1000*60)
|
||||
}
|
||||
var logbookRepo: LogbookRepository? = null
|
||||
@ -66,12 +70,6 @@ class MainActivity : AppCompatActivity() {
|
||||
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)
|
||||
@ -80,12 +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
|
||||
@ -122,13 +119,24 @@ class MainActivity : AppCompatActivity() {
|
||||
showSettings()
|
||||
})
|
||||
findViewById<View>(R.id.button_no_connection_retry).setOnClickListener({
|
||||
loadLogbook(logbook.name)
|
||||
// This may happen at start, when logbook is still null: better ask the logbook list
|
||||
loadLogbookList()
|
||||
})
|
||||
findViewById<View>(R.id.button_sync).setOnClickListener({
|
||||
loadLogbook(logbook.name)
|
||||
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)
|
||||
@ -136,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() {
|
||||
@ -160,10 +169,15 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
// Update list dates
|
||||
adapter.notifyDataSetChanged()
|
||||
recyclerView.adapter?.notifyDataSetChanged()
|
||||
|
||||
// Reload data
|
||||
loadLogbookList()
|
||||
if (logbook != null) {
|
||||
// Already running: reload data for currently selected logbook
|
||||
loadLogbook(logbook!!.name)
|
||||
} else {
|
||||
// First start: load logbook list
|
||||
loadLogbookList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
@ -243,12 +257,7 @@ class MainActivity : AppCompatActivity() {
|
||||
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)
|
||||
@ -283,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 ->
|
||||
@ -294,21 +303,57 @@ 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) {
|
||||
@ -359,8 +404,7 @@ class MainActivity : AppCompatActivity() {
|
||||
id: Long
|
||||
) {
|
||||
// Changed logbook: empty list
|
||||
adapter.items.clear()
|
||||
adapter.notifyDataSetChanged()
|
||||
setListAdapter(arrayListOf())
|
||||
// Load logbook
|
||||
loadLogbook(logbooksNames.get(position))
|
||||
}
|
||||
@ -372,15 +416,35 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onIOError(error: IOException) {
|
||||
TODO("Not yet implemented")
|
||||
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) {
|
||||
TODO("Not yet implemented")
|
||||
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) {
|
||||
TODO("Not yet implemented")
|
||||
Log.e(TAG, "Unable to load logbooks list: $error")
|
||||
runOnUiThread({
|
||||
setLoading(false)
|
||||
onRepoError(getString(R.string.settings_generic_error) + error.toString())
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -409,6 +473,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()
|
||||
}
|
||||
@ -449,7 +515,7 @@ class MainActivity : AppCompatActivity() {
|
||||
showLogbook()
|
||||
|
||||
if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) {
|
||||
for (e in logbook.logs) {
|
||||
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}")
|
||||
@ -472,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()
|
||||
}
|
||||
@ -506,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({
|
||||
@ -558,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()
|
||||
}
|
||||
@ -593,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)
|
||||
})
|
||||
}
|
||||
@ -647,6 +730,10 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
dismiss()
|
||||
})
|
||||
contentView.findViewById<View>(R.id.button_scale).setOnClickListener({
|
||||
askWeightValue()
|
||||
dismiss()
|
||||
})
|
||||
}.also { popupWindow ->
|
||||
popupWindow.setOnDismissListener({
|
||||
Handler(mainLooper).postDelayed({
|
||||
|
@ -14,13 +14,14 @@ 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
|
||||
|
@ -16,4 +16,8 @@ class Logbook(val name: String) {
|
||||
fun trim() {
|
||||
logs.subList(MAX_SAFE_LOGBOOK_SIZE/2, logs.size).clear()
|
||||
}
|
||||
|
||||
fun sort() {
|
||||
logs.sortDescending()
|
||||
}
|
||||
}
|
@ -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"
|
||||
@ -27,6 +27,7 @@ class LunaEvent {
|
||||
val TYPE_CUSTOM = "CUSTOM"
|
||||
val TYPE_COLIC = "COLIC"
|
||||
val TYPE_TEMPERATURE = "TEMPERATURE"
|
||||
val TYPE_FOOD = "FOOD"
|
||||
}
|
||||
|
||||
private val jo: JSONObject
|
||||
@ -88,6 +89,7 @@ class LunaEvent {
|
||||
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
|
||||
}
|
||||
)
|
||||
@ -108,11 +110,19 @@ class LunaEvent {
|
||||
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
|
||||
}
|
||||
@ -120,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()
|
||||
}
|
||||
}
|
@ -103,19 +103,32 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
|
||||
listener: LogbookListObtainedListener
|
||||
) {
|
||||
Thread(Runnable {
|
||||
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, "")
|
||||
)
|
||||
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)
|
||||
}
|
||||
listener.onLogbookListObtained(logbooksNames)
|
||||
}).start()
|
||||
}
|
||||
|
||||
|
5
app/src/main/res/drawable/ic_edit.xml
Normal file
5
app/src/main/res/drawable/ic_edit.xml
Normal 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>
|
@ -108,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"
|
||||
@ -116,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>
|
||||
|
||||
|
@ -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>
|
@ -59,6 +59,16 @@
|
||||
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>
|
||||
|
||||
</ScrollView>
|
97
app/src/main/res/values-fr/strings.xml
Normal file
97
app/src/main/res/values-fr/strings.xml
Normal 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>
|
@ -13,6 +13,7 @@
|
||||
<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>
|
||||
@ -20,6 +21,7 @@
|
||||
<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>
|
||||
@ -61,6 +63,7 @@
|
||||
<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>
|
||||
@ -80,11 +83,14 @@
|
||||
<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>
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
<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>
|
||||
@ -28,6 +29,7 @@
|
||||
<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>
|
||||
@ -41,6 +43,7 @@
|
||||
<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>
|
||||
@ -68,23 +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>
|
||||
|
||||
@ -103,11 +107,14 @@
|
||||
<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="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>
|
||||
|
3
fastlane/metadata/android/en-US/changelogs/3.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/3.txt
Normal file
@ -0,0 +1,3 @@
|
||||
Multiple children support
|
||||
Fixed interface in devices with big font size
|
||||
|
@ -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.
|
||||
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.
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
1
fastlane/metadata/android/fr-FR/changelogs/1.txt
Normal file
1
fastlane/metadata/android/fr-FR/changelogs/1.txt
Normal file
@ -0,0 +1 @@
|
||||
Première version alpha : carnet de bord, synchronisation.
|
2
fastlane/metadata/android/fr-FR/changelogs/3.txt
Normal file
2
fastlane/metadata/android/fr-FR/changelogs/3.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Prise en charge de plusieurs enfants
|
||||
Correction de l'interface sur les appareils avec une grande taille de police.
|
13
fastlane/metadata/android/fr-FR/full_description.txt
Normal file
13
fastlane/metadata/android/fr-FR/full_description.txt
Normal 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
|
1
fastlane/metadata/android/fr-FR/short_description.txt
Normal file
1
fastlane/metadata/android/fr-FR/short_description.txt
Normal file
@ -0,0 +1 @@
|
||||
LunaTracker est une application de suivi des nouveau-nés.
|
1
fastlane/metadata/android/fr-FR/title.txt
Normal file
1
fastlane/metadata/android/fr-FR/title.txt
Normal file
@ -0,0 +1 @@
|
||||
🌜 LunaTracker 🌛
|
Reference in New Issue
Block a user