8 Commits

Author SHA1 Message Date
d204b877f5 activity_setting: fine tune layout style 2025-09-29 05:04:13 +02:00
335c1d13af add signature setting
For multiple users it helps to
keep track about who did what.
2025-09-29 05:04:09 +02:00
15dc8a1bef DateUtils: move event details formatting to DateUtils
Also display second as 0 since it is easier
to read and does not have meaning for the user.
2025-09-29 04:57:16 +02:00
251ebd647a add bath event type 2025-09-29 04:57:16 +02:00
e09df3b0bf add no-breastfeeding help text 2025-09-29 04:57:16 +02:00
f36005de3b more_events_popup: move enema to bottom and adjust padding
Enemas are usually are rare thing. Let's
move it to the bottom. Also adjust padding
to have more space to display all items.
2025-09-29 04:57:16 +02:00
19abf20628 simplify puke event 2025-09-29 04:57:13 +02:00
730ef95220 add complex puke event 2025-09-29 04:56:14 +02:00
12 changed files with 60 additions and 119 deletions

View File

@@ -13,24 +13,7 @@ Dedicated to my daughter Luna.
![Screenshot](fastlane/metadata/android/en-US/images/phoneScreenshots/1.png) ![Screenshot](fastlane/metadata/android/en-US/images/phoneScreenshots/1.png)
## Contributions ## Thanks for the valuable contributions to:
### Why isn't this hosted on GitHub?
I'm using a private git server just because I'm worried for the vast majority of open source code being hosted in a server property of Microsoft and being used to train theirs AI.
I didn't find a better option, BTW the Gitea project is working on implementing federation, so soon it will be possible to contribute using any other gitea server, selfhosted or not.
### How to contribute
The project is open to contribution, but with some limits:
- I'm sorry I can't accept AI-generated contributions. Reviewing a contribution requires time and effort from my side, while generating code with AI requires very little time and produces non reliable code that must be reviewed in detail. This is effectively shifting the work on my side, and in a forced way. If you feel you need a feature but you're not able to implement it by yourself, I prefer you to create an issue in the repository so I can implement it when I can, in a more mantainable way.
- I prefer to make project-wide changes (i.e. updating Android target, app name and icon, release number...) by myself.
To contribute, you'll have to create an account on this git instance. Unfortunately, I had to disable registration to avoid huge waves of fake accounts created by bots.
You can request an account writing to daniele.verducci@ichibi.eu
### Thanks for the valuable contributions to:
- Chepycou (French translation) - Chepycou (French translation)
- Daniel Neubauer (German translation) - Daniel Neubauer (German translation)

View File

@@ -12,8 +12,8 @@ android {
applicationId = "it.danieleverducci.lunatracker" applicationId = "it.danieleverducci.lunatracker"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 7 versionCode = 5
versionName = "0.9" versionName = "0.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -13,6 +13,7 @@ import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout
import android.widget.NumberPicker import android.widget.NumberPicker
import android.widget.PopupWindow import android.widget.PopupWindow
import android.widget.Spinner import android.widget.Spinner
@@ -210,7 +211,7 @@ class MainActivity : AppCompatActivity() {
numberPicker.wrapSelectorWheel = false numberPicker.wrapSelectorWheel = false
numberPicker.value = localSettings.loadBabyBottleContent() numberPicker.value = localSettings.loadBabyBottleContent()
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
logEvent(LunaEvent(LunaEvent.TYPE_BABY_BOTTLE, numberPicker.value * 10)) logEvent(LunaEvent(LunaEvent.TYPE_BABY_BOTTLE, signature, numberPicker.value * 10))
localSettings.saveBabyBottleContent(numberPicker.value) localSettings.saveBabyBottleContent(numberPicker.value)
} }
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
@@ -229,7 +230,7 @@ class MainActivity : AppCompatActivity() {
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val weight = weightET.text.toString().toIntOrNull() val weight = weightET.text.toString().toIntOrNull()
if (weight != null) if (weight != null)
logEvent(LunaEvent(LunaEvent.TYPE_WEIGHT, weight)) logEvent(LunaEvent(LunaEvent.TYPE_WEIGHT, signature, weight))
else else
Toast.makeText(this, R.string.toast_integer_error, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.toast_integer_error, Toast.LENGTH_SHORT).show()
} }
@@ -255,27 +256,7 @@ class MainActivity : AppCompatActivity() {
tempSlider.addOnChangeListener({s, v, b -> tempTextView.text = v.toString()}) tempSlider.addOnChangeListener({s, v, b -> tempTextView.text = v.toString()})
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val temperature = (tempSlider.value * 10).toInt() // In tenth of a grade val temperature = (tempSlider.value * 10).toInt() // In tenth of a grade
logEvent(LunaEvent(LunaEvent.TYPE_TEMPERATURE, temperature)) logEvent(LunaEvent(LunaEvent.TYPE_TEMPERATURE, signature, temperature))
}
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
val alertDialog = d.create()
alertDialog.show()
}
fun askPukeValue() {
val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.puke_dialog, null)
d.setTitle(R.string.log_puke_dialog_title)
d.setMessage(R.string.log_puke_dialog_description)
d.setView(dialogView)
val spinner = dialogView.findViewById<Spinner>(R.id.dialog_puke_value)
spinner.adapter = ArrayAdapter.createFromResource(this, R.array.AmountLabels, android.R.layout.simple_spinner_dropdown_item)
spinner.setSelection(1)
d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val pos = spinner.selectedItemPosition
logEvent(LunaEvent(LunaEvent.TYPE_PUKE, pos + 1))
} }
d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() } d.setNegativeButton(android.R.string.cancel) { dialogInterface, i -> dialogInterface.dismiss() }
val alertDialog = d.create() val alertDialog = d.create()
@@ -410,10 +391,11 @@ class MainActivity : AppCompatActivity() {
}) })
// show optional signature // show optional signature
if (event.signature.isNotEmpty()) { dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_signature).text = event.signature
val signatureTextEdit = dialogView.findViewById<TextView>(R.id.dialog_event_detail_type_signature) dialogView.findViewById<LinearLayout>(R.id.dialog_event_signature_layout).visibility = if (event.signature.isNotEmpty()) {
signatureTextEdit.text = String.format(getString(R.string.dialog_event_detail_signature), event.signature) View.VISIBLE
signatureTextEdit.visibility = View.VISIBLE } else {
View.GONE
} }
// create next/previous links to events of the same type // create next/previous links to events of the same type
@@ -665,8 +647,6 @@ class MainActivity : AppCompatActivity() {
fun logEvent(event: LunaEvent) { fun logEvent(event: LunaEvent) {
savingEvent(true) savingEvent(true)
event.signature = signature
setLoading(true) setLoading(true)
logbook?.logs?.add(0, event) logbook?.logs?.add(0, event)
recyclerView.adapter?.notifyItemInserted(0) recyclerView.adapter?.notifyItemInserted(0)
@@ -801,15 +781,15 @@ class MainActivity : AppCompatActivity() {
val inflater = LayoutInflater.from(anchor.context) val inflater = LayoutInflater.from(anchor.context)
contentView = inflater.inflate(R.layout.more_events_popup, null) contentView = inflater.inflate(R.layout.more_events_popup, null)
contentView.findViewById<View>(R.id.button_medicine).setOnClickListener { contentView.findViewById<View>(R.id.button_medicine).setOnClickListener {
askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE)) askNotes(LunaEvent(LunaEvent.TYPE_MEDICINE, signature))
dismiss() dismiss()
} }
contentView.findViewById<View>(R.id.button_enema).setOnClickListener({ contentView.findViewById<View>(R.id.button_enema).setOnClickListener({
logEvent(LunaEvent(LunaEvent.TYPE_ENEMA)) logEvent(LunaEvent(LunaEvent.TYPE_ENEMA, signature))
dismiss() dismiss()
}) })
contentView.findViewById<View>(R.id.button_note).setOnClickListener({ contentView.findViewById<View>(R.id.button_note).setOnClickListener({
askNotes(LunaEvent(LunaEvent.TYPE_NOTE)) askNotes(LunaEvent(LunaEvent.TYPE_NOTE, signature))
dismiss() dismiss()
}) })
contentView.findViewById<View>(R.id.button_temperature).setOnClickListener({ contentView.findViewById<View>(R.id.button_temperature).setOnClickListener({
@@ -817,12 +797,14 @@ class MainActivity : AppCompatActivity() {
dismiss() dismiss()
}) })
contentView.findViewById<View>(R.id.button_puke).setOnClickListener({ contentView.findViewById<View>(R.id.button_puke).setOnClickListener({
askPukeValue() logEvent(
LunaEvent(LunaEvent.TYPE_PUKE, signature)
)
dismiss() dismiss()
}) })
contentView.findViewById<View>(R.id.button_colic).setOnClickListener({ contentView.findViewById<View>(R.id.button_colic).setOnClickListener({
logEvent( logEvent(
LunaEvent(LunaEvent.TYPE_COLIC) LunaEvent(LunaEvent.TYPE_COLIC, signature)
) )
dismiss() dismiss()
}) })
@@ -832,7 +814,7 @@ class MainActivity : AppCompatActivity() {
}) })
contentView.findViewById<View>(R.id.button_bath).setOnClickListener({ contentView.findViewById<View>(R.id.button_bath).setOnClickListener({
logEvent( logEvent(
LunaEvent(LunaEvent.TYPE_BATH) LunaEvent(LunaEvent.TYPE_BATH, signature)
) )
dismiss() dismiss()
}) })

View File

@@ -69,16 +69,18 @@ class LunaEvent: Comparable<LunaEvent> {
throw IllegalArgumentException("JSONObject is not a LunaEvent") throw IllegalArgumentException("JSONObject is not a LunaEvent")
} }
constructor(type: String) { constructor(type: String, signature: String) {
this.jo = JSONObject() this.jo = JSONObject()
this.time = System.currentTimeMillis() / 1000 this.time = System.currentTimeMillis() / 1000
this.signature = signature
this.type = type this.type = type
} }
constructor(type: String, quantity: Int) { constructor(type: String, signature: String, quantity: Int) {
this.jo = JSONObject() this.jo = JSONObject()
this.time = System.currentTimeMillis() / 1000 this.time = System.currentTimeMillis() / 1000
this.type = type this.type = type
this.signature = signature
this.quantity = quantity this.quantity = quantity
} }

View File

@@ -1,11 +1,11 @@
package utils package utils
import android.content.Context import android.content.Context
import android.os.Build
import android.text.format.DateFormat import android.text.format.DateFormat
import it.danieleverducci.lunatracker.R import it.danieleverducci.lunatracker.R
import java.util.Date import java.util.Date
class DateUtils { class DateUtils {
companion object { companion object {
/** /**
@@ -108,19 +108,15 @@ class DateUtils {
} }
/** /**
* Format time as localized string without seconds. E.g. "Sept 18, 2025, 03:36 PM". * Format time as localized string without seconds. E.g. "28 Sept 03:36:00".
* The seconds are set to 0 since they are distracting and not relevant.
* Used in the event detail dialog. * Used in the event detail dialog.
*/ */
fun formatDateTime(unixTime: Long): String { fun formatDateTime(unixTime: Long): String {
val date = Date(unixTime * 1000) val roundedUnixTime = unixTime - (unixTime % 60)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val date = Date(roundedUnixTime * 1000)
val dateFormat = android.icu.text.DateFormat.getDateTimeInstance(android.icu.text.DateFormat.DEFAULT, android.icu.text.DateFormat.SHORT)
return dateFormat.format(date)
} else {
// fallback
val dateFormat = java.text.DateFormat.getDateTimeInstance() val dateFormat = java.text.DateFormat.getDateTimeInstance()
return dateFormat.format(date) return dateFormat.format(date)
} }
} }
} }
}

View File

@@ -66,8 +66,6 @@ class NumericUtils (val context: Context) {
formatted.append(when (item.type) { formatted.append(when (item.type) {
LunaEvent.TYPE_TEMPERATURE -> LunaEvent.TYPE_TEMPERATURE ->
(item.quantity / 10.0f).toString() (item.quantity / 10.0f).toString()
LunaEvent.TYPE_PUKE ->
context.resources.getStringArray(R.array.AmountLabels)[item.quantity - 1]
else -> else ->
item.quantity item.quantity
}) })

View File

@@ -130,7 +130,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:layout_marginTop="5dp" android:layout_marginTop="20dp"
android:inputType="textEmailAddress" android:inputType="textEmailAddress"
android:background="@drawable/textview_background"/> android:background="@drawable/textview_background"/>

View File

@@ -4,9 +4,7 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingTop="20dp" android:padding="20dp">
android:paddingBottom="10dp"
android:paddingHorizontal="20dp">
<TextView <TextView
android:id="@+id/dialog_event_detail_type_emoji" android:id="@+id/dialog_event_detail_type_emoji"
@@ -63,14 +61,6 @@
</ScrollView> </ScrollView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:id="@+id/dialog_event_detail_type_signature"
android:layout_marginBottom="5dp"
android:visibility="gone"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -99,4 +89,25 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/dialog_event_signature_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:text="@string/dialog_event_detail_signature"/>
<TextView
android:id="@+id/dialog_event_detail_type_signature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<Spinner
android:id="@+id/dialog_puke_value"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"/>
</LinearLayout>

View File

@@ -50,8 +50,9 @@
<string name="no_connection_go_to_settings">Einstellungen</string> <string name="no_connection_go_to_settings">Einstellungen</string>
<string name="no_connection_retry">Erneut versuchen</string> <string name="no_connection_retry">Erneut versuchen</string>
<string name="no_breastfeeding">Kein Stillen</string>
<string name="settings_title">Einstellungen</string> <string name="settings_title">Einstellungen</string>
<string name="settings_no_breastfeeding">Kein Stillen</string>
<string name="settings_storage">Speicherort für Daten auswählen</string> <string name="settings_storage">Speicherort für Daten auswählen</string>
<string name="settings_storage_local">Auf dem Gerät</string> <string name="settings_storage_local">Auf dem Gerät</string>
<string name="settings_storage_local_desc">Datenschutzfreundlichste Lösung: Deine Daten verlassen dein Gerät nicht</string> <string name="settings_storage_local_desc">Datenschutzfreundlichste Lösung: Deine Daten verlassen dein Gerät nicht</string>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="AmountLabels">
<item>@string/amount_little</item>
<item>@string/amount_normal</item>
<item>@string/amount_plenty</item>
</string-array>
</resources>

View File

@@ -12,9 +12,6 @@
<string name="log_temperature_dialog_title">Temperature</string> <string name="log_temperature_dialog_title">Temperature</string>
<string name="log_temperature_dialog_description">Insert the temperature</string> <string name="log_temperature_dialog_description">Insert the temperature</string>
<string name="log_puke_dialog_title">Puke</string>
<string name="log_puke_dialog_description">Select the amount</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_food_type" translatable="false">🥣</string>
<string name="event_scale_type" translatable="false">⚖️</string> <string name="event_scale_type" translatable="false">⚖️</string>
@@ -75,10 +72,6 @@
<string name="year_ago">year</string> <string name="year_ago">year</string>
<string name="years_ago">years</string> <string name="years_ago">years</string>
<string name="amount_little">Little</string>
<string name="amount_normal">Normal</string>
<string name="amount_plenty">Plenty</string>
<string name="no_connection">No connection</string> <string name="no_connection">No connection</string>
<string name="no_connection_explain">Unable to reach WebDAV service</string> <string name="no_connection_explain">Unable to reach WebDAV service</string>
<string name="no_connection_go_to_settings">Settings</string> <string name="no_connection_go_to_settings">Settings</string>
@@ -86,7 +79,7 @@
<string name="settings_title">Settings</string> <string name="settings_title">Settings</string>
<string name="settings_signature">Signature</string> <string name="settings_signature">Signature</string>
<string name="settings_signature_desc">Attach a signature to each event you create and for others to see. Useful if multiple people add events.</string> <string name="settings_signature_desc">Attach a signature to each event your create and for others to see. Useful if multiple people add events.</string>
<string name="settings_storage">Choose where to save data</string> <string name="settings_storage">Choose where to save data</string>
<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>
@@ -138,7 +131,7 @@
<string name="dialog_event_detail_delete_button">Delete</string> <string name="dialog_event_detail_delete_button">Delete</string>
<string name="dialog_event_detail_quantity">Quantity</string> <string name="dialog_event_detail_quantity">Quantity</string>
<string name="dialog_event_detail_notes">Notes</string> <string name="dialog_event_detail_notes">Notes</string>
<string name="dialog_event_detail_signature">by %s</string> <string name="dialog_event_detail_signature">Created By</string>
<string name="dialog_add_logbook_title">Add logbook</string> <string name="dialog_add_logbook_title">Add logbook</string>
<string name="dialog_add_logbook_logbookname">👶 Logbook name</string> <string name="dialog_add_logbook_logbookname">👶 Logbook name</string>