36 Commits

Author SHA1 Message Date
6f69b581e4 MainActivity: sort events before saving 2025-12-25 01:54:03 +01:00
ec80f78baf StatisticsActivity: rework all statistics
Improve the overall code.
2025-12-25 01:51:17 +01:00
2f877bdec7 NumericUtils: remove possible trailing whitespace 2025-12-25 01:51:17 +01:00
22e5e3ddec MainActivity: do not switch logbook on reload 2025-12-25 01:51:17 +01:00
2ae5a2deda LunaEvent: reorganize event text getters
Use method names that better reflect
the use of the returned text.
2025-12-25 01:51:17 +01:00
e8480176c3 MainAcitivty: add dynamic header setting
The setting allows to build the menu and
popup list to be populated by the frequency
of events that has been created.
This also makes the 'no breastfeeding' setting irrelevant.
2025-12-25 01:51:13 +01:00
1c90fbd7c4 LunaEvent: use enum class for event types
This helps to have compile errors when some
case it not handled while adding a new type.
The enum class can also be interated over
to create a complete drop down list.
2025-12-25 01:46:38 +01:00
b164a80531 MainActivity: increase bottle volume to 340ml
This is the maximum amount found in sold bottles.
2025-12-25 01:46:38 +01:00
ae965ebe5e gradle: use uniform implementation directive for sardine-android 2025-12-25 01:46:38 +01:00
daac0d063f gradle: avoid inclusion of apk signing blobs
See https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs
2025-12-25 01:46:38 +01:00
eab55de651 gradle: set compileSDK/targetSdk to 36 2025-12-25 01:46:38 +01:00
7cf396026b StatisticsActivity: add statistics for bottle and sleep events 2025-12-25 01:46:20 +01:00
6759956461 MainActivity: show save button if any values has changed 2025-12-14 19:42:08 +01:00
c521a3373f MainActivity: use unique templates for notes 2025-12-14 19:42:08 +01:00
1ad5f31b32 LunaEvent: add sleep event 2025-12-14 19:42:03 +01:00
9f8f277d75 MainActivity: allow amount for poo and pee events
An unspecified amount has also been added
to have the same semantics as before.

During these actions, the strings for title
and description of dialogs have been cleaned up.
2025-11-19 20:37:10 +01:00
742c5515b1 strings: rename scale to weight in identifiers 2025-11-19 20:37:10 +01:00
d49701a488 LunaEvent: remove quantity when the value is invalid 2025-11-19 20:37:10 +01:00
fb6edf981b notes: add icons to use previous/next event as template 2025-11-19 20:37:10 +01:00
52b23151d6 MainActivity: preset quantity of bottle, weight and temperature
Use the quantity of previous events
to initialize new events.
2025-11-19 20:37:10 +01:00
23372408d5 events: allow editing of all used values
1. Allow to change the date/time and
other relevant values of an event
on creation and after it was created.

2. Harmonize layout file names and
variable names.
2025-11-19 20:37:01 +01:00
5eeb462836 Bumped version 2025-11-06 22:21:05 +01:00
5b777c0b88 Merge pull request 'fix puke event quantity' (#18) from mwarning/luna-tracker:fixpuke into master
I already tagged the commit when I accepted the previous PR, so it may have already been picked up by the F-Droid build pipeline. I'll send this one too and see if it makes in time.
2025-11-06 22:17:40 +01:00
6f3061974d fix puke event quantity
The LunaEvent class treats quantities of 0
as a value to set. To workaround this, the
quantity index needs to start at >0.
2025-11-06 21:49:08 +01:00
3ea396c045 Bumped version 2025-11-05 07:58:28 +01:00
193e21ce25 Merge pull request 'Add puke and bath events, add signature setting' (#16) from mwarning/luna-tracker:master into develop
Reviewed-on: #16
2025-11-05 07:56:01 +01:00
7f67c758c9 activity_setting: fine tune layout style 2025-10-26 20:38:26 +01:00
dfa64d71a8 add signature setting
For multiple users it helps to
keep track about who did what.
2025-10-26 20:38:26 +01:00
b7180068f3 DateUtils: move event details formatting to DateUtils
Also do not display seconds, because it is not
meaningful and is not selected in date picker.
2025-10-26 20:38:22 +01:00
36b848b95e add bath event type 2025-10-26 13:54:16 +01:00
a1bde917f8 add no-breastfeeding help text 2025-10-26 13:54:16 +01:00
4f4ff5ed21 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-10-26 13:54:16 +01:00
453d838470 add puke event 2025-10-26 13:54:04 +01:00
34aa092722 NumericUtils: provide fallback for LocaleData.getMeasurementSystem
LocaleData.getMeasurementSystem is available at API level 28
but the app supports API level 21.
2025-09-29 03:34:08 +02:00
961e7b90e7 small code cleanup
No code behavior has been changed.
2025-09-29 03:33:30 +02:00
be77c7fb22 Added "get on F-Droid" button to readme 2025-09-21 09:28:02 +02:00
35 changed files with 2521 additions and 605 deletions

View File

@@ -1,5 +1,7 @@
# 🌜 LunaTracker 🌛 # 🌜 LunaTracker 🌛
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/it/packages/it.danieleverducci.lunatracker/)
LunaTracker is a newborn baby tracking app. LunaTracker is a newborn baby tracking app.
Parenting can be tough. You get home from the hospital, exhausted, with this little fragile unknown thingy that has no user manual and a single way to let you know something's not ok: crying. 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.

View File

@@ -6,14 +6,14 @@ plugins {
android { android {
namespace = "it.danieleverducci.lunatracker" namespace = "it.danieleverducci.lunatracker"
compileSdk = 34 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "it.danieleverducci.lunatracker" applicationId = "it.danieleverducci.lunatracker"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 36
versionCode = 5 versionCode = 7
versionName = "0.7" versionName = "0.9"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -31,6 +31,12 @@ android {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
} }
@@ -40,7 +46,6 @@ android {
} }
dependencies { dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
@@ -51,7 +56,7 @@ dependencies {
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.recyclerview) implementation(libs.androidx.recyclerview)
implementation("com.github.thegrizzlylabs:sardine-android:v0.9") implementation(libs.sardine.android)
implementation(libs.material) implementation(libs.material)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
@@ -60,4 +65,5 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.mpandroidchart.vv310)
} }

View File

@@ -30,6 +30,10 @@
android:name=".SettingsActivity" android:name=".SettingsActivity"
android:label="@string/settings_title" android:label="@string/settings_title"
android:theme="@style/Theme.LunaTracker"/> android:theme="@style/Theme.LunaTracker"/>
<activity
android:name=".StatisticsActivity"
android:label="@string/statistics_title"
android:theme="@style/Theme.LunaTracker"/>
</application> </application>
</manifest> </manifest>

View File

@@ -2,7 +2,9 @@ package it.danieleverducci.lunatracker
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.EditText
import android.widget.RadioButton import android.widget.RadioButton
import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -23,7 +25,8 @@ open class SettingsActivity : AppCompatActivity() {
protected lateinit var textViewWebDAVUser: TextView protected lateinit var textViewWebDAVUser: TextView
protected lateinit var textViewWebDAVPass: TextView protected lateinit var textViewWebDAVPass: TextView
protected lateinit var progressIndicator: LinearProgressIndicator protected lateinit var progressIndicator: LinearProgressIndicator
protected lateinit var switchNoBreastfeeding: SwitchMaterial protected lateinit var switchDynamicMenu: SwitchMaterial
protected lateinit var textViewSignature: EditText
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -35,7 +38,8 @@ open class SettingsActivity : AppCompatActivity() {
textViewWebDAVUser = findViewById(R.id.settings_data_webdav_user) textViewWebDAVUser = findViewById(R.id.settings_data_webdav_user)
textViewWebDAVPass = findViewById(R.id.settings_data_webdav_pass) textViewWebDAVPass = findViewById(R.id.settings_data_webdav_pass)
progressIndicator = findViewById(R.id.progress_indicator) progressIndicator = findViewById(R.id.progress_indicator)
switchNoBreastfeeding = findViewById(R.id.switch_no_breastfeeding) switchDynamicMenu = findViewById(R.id.switch_dynamic_menu)
textViewSignature = findViewById(R.id.settings_signature)
findViewById<View>(R.id.settings_save).setOnClickListener({ findViewById<View>(R.id.settings_save).setOnClickListener({
validateAndSave() validateAndSave()
@@ -51,19 +55,21 @@ open class SettingsActivity : AppCompatActivity() {
fun loadSettings() { fun loadSettings() {
val dataRepo = settingsRepository.loadDataRepository() val dataRepo = settingsRepository.loadDataRepository()
val webDavCredentials = settingsRepository.loadWebdavCredentials() val webDavCredentials = settingsRepository.loadWebdavCredentials()
val noBreastfeeding = settingsRepository.loadNoBreastfeeding() val dynamicMenu = settingsRepository.loadDynamicMenu()
val signature = settingsRepository.loadSignature()
when (dataRepo) { when (dataRepo) {
LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> radioDataLocal.isChecked = true LocalSettingsRepository.DATA_REPO.LOCAL_FILE -> radioDataLocal.isChecked = true
LocalSettingsRepository.DATA_REPO.WEBDAV -> radioDataWebDAV.isChecked = true LocalSettingsRepository.DATA_REPO.WEBDAV -> radioDataWebDAV.isChecked = true
} }
switchNoBreastfeeding.isChecked = noBreastfeeding textViewSignature.setText(signature)
switchDynamicMenu.isChecked = dynamicMenu
if (webDavCredentials != null) { if (webDavCredentials != null) {
textViewWebDAVUrl.setText(webDavCredentials[0]) textViewWebDAVUrl.text = webDavCredentials[0]
textViewWebDAVUser.setText(webDavCredentials[1]) textViewWebDAVUser.text = webDavCredentials[1]
textViewWebDAVPass.setText(webDavCredentials[2]) textViewWebDAVPass.text = webDavCredentials[2]
} }
} }
@@ -155,7 +161,8 @@ open class SettingsActivity : AppCompatActivity() {
if (radioDataWebDAV.isChecked) LocalSettingsRepository.DATA_REPO.WEBDAV if (radioDataWebDAV.isChecked) LocalSettingsRepository.DATA_REPO.WEBDAV
else LocalSettingsRepository.DATA_REPO.LOCAL_FILE else LocalSettingsRepository.DATA_REPO.LOCAL_FILE
) )
settingsRepository.saveNoBreastfeeding(switchNoBreastfeeding.isChecked) settingsRepository.saveDynamicMenu(switchDynamicMenu.isChecked)
settingsRepository.saveSignature(textViewSignature.text.toString())
settingsRepository.saveWebdavCredentials( settingsRepository.saveWebdavCredentials(
textViewWebDAVUrl.text.toString(), textViewWebDAVUrl.text.toString(),
textViewWebDAVUser.text.toString(), textViewWebDAVUser.text.toString(),
@@ -170,7 +177,7 @@ open class SettingsActivity : AppCompatActivity() {
*/ */
private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) { private fun copyLocalLogbooksToWebdav(webDAVLogbookRepository: WebDAVLogbookRepository, listener: OnCopyLocalLogbooksToWebdavFinishedListener) {
Thread(Runnable { Thread(Runnable {
var errors = StringBuilder() val errors = StringBuilder()
val fileLogbookRepo = FileLogbookRepository() val fileLogbookRepo = FileLogbookRepository()
val logbooks = fileLogbookRepo.getAllLogbooks(this) val logbooks = fileLogbookRepo.getAllLogbooks(this)
for (logbook in logbooks) { for (logbook in logbooks) {

View File

@@ -0,0 +1,976 @@
package it.danieleverducci.lunatracker
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.toColorInt
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.formatter.ValueFormatter
import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet
import com.github.mikephil.charting.listener.OnChartValueSelectedListener
import it.danieleverducci.lunatracker.entities.LunaEvent
import utils.DateUtils
import utils.NumericUtils
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class StatisticsActivity : AppCompatActivity() {
var lastToastShown = 0L
lateinit var barChart: BarChart
lateinit var noDataTextView: TextView
lateinit var graphTypeSpinner: Spinner
lateinit var timeRangeSpinner: Spinner
lateinit var unixToSpan: (Long) -> Int
lateinit var spanToUnix: (Int) -> Long
enum class GraphType {
BOTTLE_EVENTS,
BOTTLE_SUM,
SLEEP_SUM,
SLEEP_EVENTS,
SLEEP_PATTERN,
MEDICINE_EVENTS
}
enum class TimeRange {
DAY,
WEEK,
MONTH
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_statistics)
val logbookName = intent.getStringExtra("LOOGBOOK_NAME")
if (logbookName == null) {
finish()
return
}
noDataTextView = findViewById(R.id.no_data)
barChart = findViewById(R.id.bar_chart)
barChart.setBackgroundColor(Color.WHITE)
barChart.description.text = logbookName
barChart.setDrawValueAboveBar(false)
barChart.axisLeft.setAxisMinimum(0F)
barChart.axisLeft.setDrawGridLines(false)
barChart.axisLeft.setDrawLabels(false)
barChart.axisRight.setDrawGridLines(false)
barChart.axisRight.setDrawLabels(false)
barChart.xAxis.setDrawGridLines(true)
barChart.xAxis.setDrawLabels(true)
barChart.xAxis.setDrawAxisLine(false)
graphTypeSpinner = findViewById(R.id.graph_type_selection)
timeRangeSpinner = findViewById(R.id.time_range_selection)
setupSpinner(graphTypeSelection.name,
R.id.graph_type_selection,
R.array.StatisticsTypeLabels,
R.array.StatisticsTypeValues,
object : SpinnerItemSelected {
override fun call(newValue: String?) {
//Log.d("event", "new value: $newValue")
newValue ?: return
graphTypeSelection = GraphType.valueOf(newValue)
updateGraph()
}
}
)
setupSpinner(timeRangeSelection.name,
R.id.time_range_selection,
R.array.StatisticsTimeLabels,
R.array.StatisticsTimeValues,
object : SpinnerItemSelected {
override fun call(newValue: String?) {
//Log.d("event", "new value: $newValue")
newValue ?: return
timeRangeSelection = TimeRange.valueOf(newValue)
setSpans()
updateGraph()
}
}
)
setSpans()
updateGraph()
}
fun setSpans() {
unixToSpan = when (timeRangeSelection) {
TimeRange.DAY -> { unix: Long -> unixToDays(unix) }
TimeRange.WEEK -> { unix: Long -> unixToWeeks(unix) }
TimeRange.MONTH -> { unix: Long -> unixToMonths(unix) }
}
spanToUnix = when (timeRangeSelection) {
TimeRange.DAY -> { span: Int -> daysToUnix(span) }
TimeRange.WEEK -> { span: Int -> weeksToUnix(span) }
TimeRange.MONTH -> { span: Int -> monthsToUnix(span) }
}
}
fun showMedicineBarGraph(state: GraphState) {
val values = HashMap<String, ArrayList<BarEntry>>()
for (event in state.events) {
val index = unixToSpan(event.time) - state.startSpan
val key = event.notes.trim().lowercase()
val array = values.getOrPut(key) {
ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) })
}
array[index].y += 1F
}
Log.d(TAG, "values.size: ${values.size}")
for ((key, value) in values) {
Log.d(TAG, "key: $key, value.size: ${value.size} ,value: ${value.joinToString { it.y.toLong().toString() }}")
}
// make sure legend names are not too long
fun shorten(notes: String): String {
return if (notes.length > 16) {
notes.take(13) + "..."
} else {
notes
}
}
fun chooseColor(notes: String): Int {
return (abs(notes.hashCode()) * 16777215) or (0xFF shl 24)
}
val sets = arrayListOf<IBarDataSet>()
for ((key, value) in values.entries) {
if (key.startsWith("v")) {
val description = shorten(key)
val barDataSet = BarDataSet(value, description)
barDataSet.color = chooseColor(key)
sets.add(barDataSet)
}
}
val data = BarData(sets)
//data.groupBars(0F, 0.2F, 0.1F);
data.setValueTextSize(12f)
data.barWidth = 1F
//data.groupBars(0F, 1F, 1F)
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return if (value == 0F) {
""
} else {
value.toInt().toString()
}
}
})
barChart.setScaleEnabled(true)
barChart.legend.isEnabled = true
//barChart.xAxis.setLabelCount(min(values.size, 24), false);
//val maxCount = min(maxIndex, 30) // values.size
//Log.d(TAG, "maxCount: $maxCount")
barChart.setVisibleXRangeMaximum(20F) //maxCount.toFloat()) // show max 24 entries
barChart.xAxis.setLabelCount(30, true)
//barChart.xAxis.isEnabled = false
barChart.xAxis.setCenterAxisLabels(true)
barChart.setScaleEnabled(false)
//barChart.axisLeft.isSLEEP_PATTERN_GRANULARITYEnabled = true
//barChart.axisLeft.setSLEEP_PATTERN_GRANULARITY(0.8F)
barChart.setData(data)
barChart.invalidate()
}
data class SleepRange(val start: Long, var end: Long)
fun toSleepRanges(events: List<LunaEvent>): ArrayList<SleepRange> {
val ranges = arrayListOf<SleepRange>()
val now = System.currentTimeMillis() / 1000
// Transform events into time ranges.
// Merge overlapping times and extend
// ongoing sleep events until now.
var warningShown = false
for (event in events) {
val startTime = event.time
val endTime = if (event.quantity == 0) {
now
} else {
event.time + event.quantity
}
// handle overlap
val previousRange = ranges.lastOrNull()
if (previousRange != null && previousRange.end > startTime) {
// cap previous range to avoid overlap
previousRange.end = startTime
if (!warningShown) {
Toast.makeText(applicationContext, "Overlapping sleep event at ${DateUtils.formatDateTime(startTime)}", Toast.LENGTH_SHORT).show()
warningShown = true
}
}
ranges.add(SleepRange(startTime, endTime))
}
return ranges
}
fun showSleepPatternBarGraph(state: GraphState) {
val ranges = toSleepRanges(state.events)
Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}")
val values = ArrayList<BarEntry>()
val stack = ArrayList(List(state.endSpan - state.startSpan + 1) { IntArray(24 * 60 * 60 / SLEEP_PATTERN_GRANULARITY) })
Log.d(TAG, "stack.size: ${stack.size}, array.size: ${stack[0].size}, dayCounter.daysWithData.size: ${state.dayCounter.daysWithData.size}")
fun stackValuePattern(index: Int, spanBegin: Long, spanEnd: Long, begin: Long, end: Long) {
val beginDays = unixToDays(begin)
val endDays = unixToDays(end)
var mid = begin
//Log.d(TAG, "stackValuePattern: ${beginDays}..${endDays}")
for (i in beginDays..endDays) {
// i is the days/weeks/months since unix epoch
val dayBegin = daysToUnix(i)
val dayEnd = daysToUnix(i + 1)
val sleepBegin = max(mid, dayBegin)
val sleepEnd = min(end, dayEnd)
if (sleepBegin != sleepEnd) {
//val index2 = i - spanBegin
//val duration = dayEnd - dayBegin
assert(dayBegin <= dayEnd)
assert(sleepBegin <= sleepEnd)
//val duration = sleepEnd - sleepBegin
val iBegin = (sleepBegin - dayBegin) / SLEEP_PATTERN_GRANULARITY
val iEnd = iBegin + (sleepEnd - sleepBegin) / SLEEP_PATTERN_GRANULARITY
Log.d(TAG, "index: $index, iBegin: $iBegin, iEnd: $iEnd, dayBegin: ${Date(dayBegin * 1000)}, dayEnd: ${Date(dayEnd * 1000)}, sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}")
for (j in iBegin..iEnd) {
stack[index][j.toInt()] += 1
}
}
mid = sleepEnd
}
}
for (range in ranges) {
// a sleep event can span to another day
// distribute sleep time over the days
val startUnix = range.start
val endUnix = range.end
val begIndex = unixToSpan(startUnix)
val endIndex = unixToSpan(endUnix)
var mid = startUnix
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})")
for (i in begIndex..endIndex) {
// i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}")
val sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan
val duration = sleepEnd - sleepBegin
//Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}")
state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
mid = sleepEnd
}
}
fun mapColor(occurrences: Int, maxOccurrences: Int): Int {
// occurences: number of reported sleeps in a specific time slice
// maxOccurrences: maximum number of days with data that can contribute to maxOccurrences
assert(maxOccurrences > 0)
assert(occurrences <= maxOccurrences)
// map to color
val q = occurrences.toFloat() / maxOccurrences.toFloat()
val i = q * (SLEEP_PATTERN_COLORS.size - 1).toFloat()
return SLEEP_PATTERN_COLORS[i.toInt()]
}
val allColors = ArrayList<Int>()
// convert array of time slots that represent a day to value and color arrays used by chart library
for ((index, dayArray) in stack.withIndex()) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
//Log.d(TAG, "index: $index: dayArray: ${dayArray.joinToString { it.toString() }}")
val vals = ArrayList<Float>()
var prevIndex = -1 // time slice index
var prevValue = -1 // number of entries we have found for time slice
for ((i, v) in dayArray.withIndex()) {
if (i == 0) {
prevIndex = i
prevValue = v
} else if (prevValue != v) {
vals.add((i - prevIndex).toFloat())
allColors.add(mapColor(prevValue, daysWithData))
prevIndex = i
prevValue = v
}
}
if (prevIndex != -1) {
vals.add((dayArray.size - prevIndex).toFloat())
allColors.add(mapColor(prevValue, daysWithData))
}
//Log.d(TAG, "Range $index, vals: ${vals.joinToString { it.toInt().toString() }}") //, allColors: ${allColors.joinToString { it.toString() }}")
values.add(BarEntry(index.toFloat(), vals.toFloatArray()))
}
Log.d(TAG, "daysWithData: ${state.dayCounter.daysWithData.joinToString()}")
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) {
if (e != null && h != null && e.x.toInt() != -1 && h.stackIndex != -1) {
if ((lastToastShown + 3500) > System.currentTimeMillis()) {
// only show one Toast message after another
return
}
val index = e.x.toInt()
val value = values[index]
val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + e.x.toInt())
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, x: ${e.x.toInt()}, dayStartUnix: ${Date(dayStartUnix * 1000)}")
val startSeconds = SLEEP_PATTERN_GRANULARITY * value.yVals.sliceArray(0..<h.stackIndex).fold(0) { acc, y -> acc + y.toInt() }
val durationSeconds = SLEEP_PATTERN_GRANULARITY * value.yVals[h.stackIndex].toInt()
val endSeconds = startSeconds + durationSeconds
val format = SimpleDateFormat("HH:mm", Locale.getDefault())
val startTimeString = format.format((dayStartUnix + startSeconds) * 1000).toString()
val endTimeString = format.format((dayStartUnix + endSeconds) * 1000).toString()
val durationString = NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, durationSeconds)
val daysWithData = stack[e.x.toInt()][startSeconds / SLEEP_PATTERN_GRANULARITY]
val daysWithDataMax = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
// percentage of days in this span where baby is asleep in this time slot
val pc = if (daysWithDataMax > 0) {
(100F * daysWithData.toFloat() / daysWithDataMax.toFloat()).toInt()
} else {
// no data for this day
0
}
Toast.makeText(applicationContext, "$startTimeString - $endTimeString ($durationString) - ${pc}%", Toast.LENGTH_LONG).show()
lastToastShown = System.currentTimeMillis()
}
}
override fun onNothingSelected() {}
})
val set1 = BarDataSet(values, "")
val data = BarData(set1)
set1.colors = allColors
//set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt())
set1.setDrawValues(false) // usually too many values
set1.isHighlightEnabled = true
set1.setDrawIcons(false)
barChart.legend.isEnabled = false
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
barChart.setData(data)
barChart.invalidate()
}
fun showSleepPatternBarGraphDaily(state: GraphState) {
val ranges = toSleepRanges(state.events)
Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}")
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), FloatArray(0)) })
// stack awake/sleep durations
fun stackValuePattern(index: Int, spanBegin: Long, spanEnd: Long, begin: Long, end: Long) {
assert(begin in spanBegin..spanEnd)
assert(end in spanBegin..spanEnd)
assert(begin <= end)
val x = values[index].x
val yVals = values[index].yVals // alternating sleep/awake durations
val y = yVals.fold(0F) { acc, next -> acc + next }
// y value is seconds when last awake
val awakeDuration = max(begin - spanBegin - y.toLong(), 0L)
val sleepDuration = end - begin
if ((awakeDuration + sleepDuration) > (spanEnd - spanBegin)) {
Log.e(TAG, "Invalid sleep duration, exceeds day/week or month bounds => ignore value")
return
}
// update value
val newYVals = appendToFloatArray(yVals, awakeDuration.toFloat(), sleepDuration.toFloat())
values[index] = BarEntry(x, newYVals)
}
for (range in ranges) {
// a sleep event can span to another day
// distribute sleep time over the days
val startUnix = range.start
val endUnix = range.end
val begIndex = unixToSpan(startUnix)
val endIndex = unixToSpan(endUnix)
var mid = startUnix
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})")
for (i in begIndex..endIndex) {
// i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}")
val sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan
val duration = sleepEnd - sleepBegin
//Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}")
state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
mid = sleepEnd
}
}
val set1 = BarDataSet(values, "")
val data = BarData(set1)
// awake phase color is transparent
set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt())
set1.setDrawValues(false) // usually too many values
set1.isHighlightEnabled = true
//barChart.xAxis.setCenterAxisLabels(true)
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) {
if (e != null && h != null && e.x.toInt() != -1 && h.stackIndex != -1) {
if ((lastToastShown + 3500) > System.currentTimeMillis()) {
// only show one Toast message after another
return
}
val value = values[e.x.toInt()]
val duration = value.yVals[h.stackIndex].toInt()
val durationString = NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, duration)
val offsetUnix = spanToUnix(state.startSpan + e.x.toInt()) // start of the time span (day/week/month)
val startUnix = offsetUnix + value.yVals.sliceArray(0..<h.stackIndex).fold(0) { acc, y -> acc + y.toInt() }
val endUnix = startUnix + duration
val format = SimpleDateFormat("HH:mm", Locale.getDefault())
val startTimeString = format.format(startUnix * 1000).toString()
val endTimeString = format.format(endUnix * 1000).toString()
Toast.makeText(applicationContext, "$startTimeString - $endTimeString ($durationString)", Toast.LENGTH_LONG).show()
lastToastShown = System.currentTimeMillis()
}
}
override fun onNothingSelected() {}
})
set1.setDrawIcons(false)
barChart.legend.isEnabled = false
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
barChart.setData(data)
barChart.invalidate()
}
fun showSleepBarGraph(state: GraphState) {
val ranges = toSleepRanges(state.events)
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) })
Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}")
for (range in ranges) {
// a sleep event can span to another day
// distribute sleep time over the days
val startUnix = range.start
val endUnix = range.end
val begIndex = unixToSpan(startUnix)
val endIndex = unixToSpan(endUnix)
var mid = startUnix
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})")
for (i in begIndex..endIndex) {
// i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1)
//Log.d(TAG, "mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}")
val sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan
val duration = sleepEnd - sleepBegin
//Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}")
state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
if (graphTypeSelection == GraphType.SLEEP_SUM) {
values[index].y += duration.toFloat()
} else if (graphTypeSelection == GraphType.SLEEP_EVENTS) {
values[index].y += 1F
} else {
Log.e(TAG, "Unexpected graph type.")
return
}
mid = sleepEnd
}
}
if (graphTypeSelection == GraphType.SLEEP_SUM) {
for (index in values.indices) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
if (daysWithData == 0) {
assert(values[index].y == 0F)
} else {
values[index].y /= daysWithData
}
}
}
val set1 = BarDataSet(values, "")
val data = BarData(set1)
set1.setDrawValues(true)
set1.isHighlightEnabled = false
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return when (graphTypeSelection) {
GraphType.SLEEP_EVENTS -> value.toInt().toString()
GraphType.SLEEP_SUM -> {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, value.toInt())
}
else -> {
Log.e(TAG, "unhandled graphTypeSelection $graphTypeSelection")
value.toInt().toString()
}
}
}
})
set1.setDrawIcons(false)
barChart.legend.isEnabled = false
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
barChart.setData(data)
barChart.invalidate()
}
fun showBottleBarGraph(state: GraphState) {
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) })
// needed?
for (i in values.indices) {
values[i].x = i.toFloat()
}
for (event in state.events) {
val index = unixToSpan(event.time) - state.startSpan
state.dayCounter.setDaysWithData(event.time, event.time)
// setDaysWithData(sleepBegin, sleepEnd)
if (graphTypeSelection == GraphType.BOTTLE_EVENTS) {
values[index].y += 1F
} else if (graphTypeSelection == GraphType.BOTTLE_SUM) {
values[index].y += event.quantity.toFloat()
} else {
Log.e(TAG, "unhandled graphTypeSelection: $graphTypeSelection")
return
}
}
if (graphTypeSelection == GraphType.BOTTLE_SUM) {
for (index in values.indices) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
//Log.d(TAG, "index: $index, daysWithData: $daysWithData")
if (daysWithData == 0) {
assert(values[index].y == 0F)
} else {
values[index].y /= daysWithData
}
}
}
val set1 = BarDataSet(values, "")
set1.setDrawValues(true)
set1.isHighlightEnabled = false
val maximumRange = 20F
if (graphTypeSelection == GraphType.BOTTLE_SUM || graphTypeSelection == GraphType.BOTTLE_EVENTS) {
//val count = values.size.coerceIn(5, 20)
barChart.setVisibleXRangeMaximum(maximumRange) // show max 24 entries
barChart.xAxis.setLabelCount(maximumRange.toInt(), false)
//barChart.xAxis.isEnabled = false
barChart.xAxis.setCenterAxisLabels(true)
barChart.setScaleEnabled(false)
//barChart.axisLeft.isSLEEP_PATTERN_GRANULARITYEnabled = true
//barChart.axisLeft.setSLEEP_PATTERN_GRANULARITY(0.8F)
}
//val dataSets = ArrayList<IBarDataSet?>()
//dataSets.add(set1)
val data = BarData(set1)
//data.barWidth = 0.3F // 0.85 default // ratio of barWidth to totalWidth.
//Log.d(TAG, "data.barWidth: ${data.barWidth}")
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
//Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}")
return when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS -> value.toInt().toString()
GraphType.BOTTLE_SUM -> {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.BABY_BOTTLE, value.toInt())
}
//GraphType.BOTTLE_SUM_AVERAGE -> "⌀ " + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.TYPE_BABY_BOTTLE, value.toInt())
else -> {
Log.e(TAG, "unhandled graphTypeSelection")
value.toInt().toString()
}
}
}
})
// hm, does not work yet
Log.d(TAG, "last value: ${values.lastOrNull()!!.x}")
barChart.moveViewToX(100F)
data.setValueTextSize(12f)
barChart.setData(data)
barChart.invalidate()
}
class DayCounter(val startDays: Int, val stopDays: Int) {
val daysWithData = BooleanArray(stopDays - startDays + 1)
// count days in a span that have data
// e.g. return 7 (days) for applied span of a week where there is data for every day
fun countDaysWithData(beginUnix: Long, endUnix: Long): Int {
val beginDays = unixToDays(beginUnix)
val endDays = unixToDays(endUnix)
var count = 0
for (i in (beginDays - startDays)..<(endDays - startDays)) {
count += if (daysWithData[i]) { 1 } else { 0 }
}
return count
}
fun setDaysWithData(beginUnix: Long, endUnix: Long) {
val beginDays = unixToDays(beginUnix)
val endDays = unixToDays(endUnix)
assert(beginDays <= endDays)
assert(startDays <= beginDays)
for (i in (beginDays - startDays)..(endDays - startDays)) {
daysWithData[i] = true
}
}
}
data class GraphState(val events: List<LunaEvent>, val dayCounter: DayCounter, val startUnix: Long, val endUnix: Long, val startSpan: Int, val endSpan: Int)
// wrapper for comon graph setup
fun prepareGraph(type: LunaEvent.Type, cb: (GraphState) -> Unit) {
val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time }
if (events.isEmpty()) {
barChart.visibility = View.GONE
noDataTextView.visibility = View.VISIBLE
return
} else {
barChart.visibility = View.VISIBLE
noDataTextView.visibility = View.GONE
}
// unix time span of all events
val startUnix = events.minOf { it.time }
val endUnix = System.currentTimeMillis() / 1000
// convert to days, weeks or months
val startSpan = unixToSpan(startUnix)
val endSpan = unixToSpan(endUnix)
// days when the a day/week/month starts/ends
val startDays = unixToDays(spanToUnix(startSpan))
val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next week
val dayCounter = DayCounter(startDays, endDays)
// print dates
barChart.xAxis.valueFormatter = object: ValueFormatter() {
override fun getFormattedValue(value: Float): String {
val index = value.toInt()
val unixSeconds = spanToUnix(startSpan + index)
val dateTime = Calendar.getInstance()
dateTime.time = Date(1000L * unixSeconds)
val year = dateTime.get(Calendar.YEAR)
val month = dateTime.get(Calendar.MONTH) + 1 // month starts at 0
val week = dateTime.get(Calendar.WEEK_OF_YEAR)
val day = dateTime.get(Calendar.DAY_OF_MONTH)
return when (timeRangeSelection) {
TimeRange.DAY -> "$day/$month/$year"
TimeRange.WEEK -> "$week/$year"
TimeRange.MONTH -> "$month/$year"
}
}
}
Log.d(TAG, "startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}")
cb(GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan))
}
fun updateGraph() {
Log.d(TAG, "updateGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection")
when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM -> prepareGraph(LunaEvent.Type.BABY_BOTTLE) { state -> showBottleBarGraph(state) }
GraphType.SLEEP_EVENTS,
GraphType.SLEEP_SUM -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepBarGraph(state) }
GraphType.SLEEP_PATTERN -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepPatternBarGraph(state) }
GraphType.MEDICINE_EVENTS -> prepareGraph(LunaEvent.Type.MEDICINE) { state -> showMedicineBarGraph(state) }
}
}
private interface SpinnerItemSelected {
fun call(newValue: String?)
}
private fun setupSpinner(
currentValue: String,
spinnerId: Int,
arrayId: Int,
arrayValuesId: Int,
callback: SpinnerItemSelected
) {
val arrayValues = resources.getStringArray(arrayValuesId)
val spinner = findViewById<Spinner>(spinnerId)
val spinnerAdapter =
ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item)
//Log.d(TAG, "spinner ${arrayValues.indexOf(currentValue)} (${arrayValues.joinToString { it }})")
spinner.adapter = spinnerAdapter
spinner.setSelection(arrayValues.indexOf(currentValue))
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
var check = 0
override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
if (pos >= arrayValues.size) {
Toast.makeText(
this@StatisticsActivity,
"pos out of bounds: $arrayValues", Toast.LENGTH_SHORT
).show()
return
}
if (check++ > 0) {
callback.call(arrayValues[pos])
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// ignore
}
}
}
companion object {
const val TAG = "StatisticsActivity"
// 15 min steps
val SLEEP_PATTERN_GRANULARITY = 15 * 60
// color gradient
val SLEEP_PATTERN_COLORS = arrayOf(
"#FFFFFF".toColorInt(), "#EEF5F7".toColorInt(), "#DDEBEF".toColorInt(), "#CCE2E7".toColorInt(),
"#BBD8DF".toColorInt(), "#AACED7".toColorInt(), "#99C4CF".toColorInt(), "#88BAC7".toColorInt(),
"#77B1BF".toColorInt(), "#66A7B7".toColorInt(), "#559DAF".toColorInt(), "#4493A7".toColorInt(),
"#33899F".toColorInt(), "#228097".toColorInt(), "#11768F".toColorInt(), "#006C87".toColorInt()
)
private val dateTime = Calendar.getInstance() // scratch pad
var graphTypeSelection = GraphType.SLEEP_SUM
var timeRangeSelection = TimeRange.DAY
fun unixToMonths(seconds: Long): Int {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR)
val months = dateTime.get(Calendar.MONTH)
return 12 * years + months
}
fun monthsToUnix(months: Int): Long {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, months / 12)
dateTime.set(Calendar.MONTH, months % 12)
dateTime.set(Calendar.HOUR, 0)
dateTime.set(Calendar.MINUTE, 0)
dateTime.set(Calendar.SECOND, 0)
return dateTime.time.time / 1000
}
fun unixToWeeks(seconds: Long): Int {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR)
val weeks = dateTime.get(Calendar.WEEK_OF_YEAR)
return 52 * years + weeks
}
fun weeksToUnix(weeks: Int): Long {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, weeks / 52)
dateTime.set(Calendar.WEEK_OF_YEAR, weeks % 52)
dateTime.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
dateTime.set(Calendar.HOUR, 0)
dateTime.set(Calendar.MINUTE, 0)
dateTime.set(Calendar.SECOND, 0)
return dateTime.time.time / 1000
}
fun unixToDays(seconds: Long): Int {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR)
val days = dateTime.get(Calendar.DAY_OF_YEAR)
return 365 * years + days
}
// convert from days to Date
fun daysToUnix(days: Int): Long {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, days / 365)
dateTime.set(Calendar.DAY_OF_YEAR, days % 365)
dateTime.set(Calendar.HOUR, 0)
dateTime.set(Calendar.MINUTE, 0)
dateTime.set(Calendar.SECOND, 0)
return dateTime.time.time / 1000
}
fun appendToFloatArray(array: FloatArray, vararg values: Float): FloatArray {
// create new array
val newArray = FloatArray(array.size + values.size)
// copy old values
for (i in array.indices) {
newArray[i] = array[i]
}
// add new values
for (i in values.indices) {
newArray[array.size + i] = values[i]
}
return newArray
}
/*
fun colorGradient(fromColor: Int, toColor: Int, percent: Int): Int {
assert(percent in 0..100)
val a1 = fromColor.shr(24).and(0xff)
val r1 = fromColor.shr(16).and(0xff)
val g1 = fromColor.shr(8).and(0xff)
val b1 = fromColor.shr(0).and(0xff)
//Log.d(TAG, "${a1.toHexString()} ${r1.toHexString()} ${g1.toHexString()} ${b1.toHexString()}")
val a2 = toColor.shr(24).and(0xff)
val r2 = toColor.shr(16).and(0xff)
val g2 = toColor.shr(8).and(0xff)
val b2 = toColor.shr(0).and(0xff)
//Log.d(TAG, "${a2.toHexString()} ${r2.toHexString()} ${g2.toHexString()} ${b2.toHexString()}")
val pc = (percent.toFloat() / 100F).coerceIn(0F, 1F)
val a = a1 + (pc * abs(a2 - a1)).toInt()
val r = r1 + (pc * abs(r2 - r1)).toInt()
val g = g1 + (pc * abs(g2 - g1)).toInt()
val b = a1 + (pc * abs(b2 - b1)).toInt()
//Log.d(TAG, "${a.toHexString()} ${r.toHexString()} ${g.toHexString()} ${b.toHexString()}")
val Red = r.shl(16).and(0x00FF0000)
val Green = g.shl(8).and(0x0000FF00)
val Blue = b.and(0x000000FF)
val aa = a.shl(24).and(0xFF000000.toInt())
val color = aa.or(Red).or(Green).or(Blue)
return color
//Log.d(TAG, "c: ${c.toHexString()} ${color.toInt().toHexString()}")
//return Color.argb(a, r, g, b)
}
*/
// for debugging
fun debugBarValues(values: ArrayList<BarEntry>) {
for (value in values) {
val yVals = value.yVals
if (yVals != null) {
val y = yVals.fold(0F) { acc, next -> acc + next }
val yVals = yVals.joinToString { it.toString() }
Log.d(TAG, "value: ${value.x} $y ($yVals)")
} else {
Log.d(TAG, "value: ${value.x} ${value.y}")
}
}
}
}
}

View File

@@ -52,18 +52,22 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
) )
holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.textColor)) holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.textColor))
// Contents // Contents
holder.type.text = item.getTypeEmoji(context) holder.type.text = item.getHeaderEmoji(context)
holder.description.text = when(item.type) { holder.description.text = when (item.type) {
LunaEvent.TYPE_MEDICINE -> item.notes LunaEvent.Type.MEDICINE -> item.notes
LunaEvent.TYPE_NOTE -> item.notes LunaEvent.Type.NOTE -> item.notes
LunaEvent.TYPE_CUSTOM -> item.notes else -> item.getRowItemTitle(context)
else -> item.getTypeDescription(context)
} }
holder.time.text = DateUtils.formatTimeAgo(context, item.time) val endTime = if (item.type == LunaEvent.Type.SLEEP) {
item.quantity + item.time
} else {
item.time
}
holder.time.text = DateUtils.formatTimeAgo(context, endTime)
var quantityText = numericUtils.formatEventQuantity(item) var quantityText = numericUtils.formatEventQuantity(item)
// if the event is weight, show difference with the last one // if the event is weight, show difference with the last one
if (item.type == LunaEvent.TYPE_WEIGHT) { if (item.type == LunaEvent.Type.WEIGHT) {
val lastWeight = getPreviousWeightEvent(position) val lastWeight = getPreviousWeightEvent(position)
if (lastWeight != null) { if (lastWeight != null) {
val differenceInWeight = item.quantity - lastWeight.quantity val differenceInWeight = item.quantity - lastWeight.quantity
@@ -94,7 +98,7 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
return null return null
for (pos in startFromPosition + 1 until items.size) { for (pos in startFromPosition + 1 until items.size) {
val item = items.get(pos) val item = items.get(pos)
if (item.type != LunaEvent.TYPE_WEIGHT) if (item.type != LunaEvent.Type.WEIGHT)
continue continue
return item return item
} }

View File

@@ -12,22 +12,24 @@ import java.util.Date
* release, it is simply ignored by previous ones). * release, it is simply ignored by previous ones).
*/ */
class LunaEvent: Comparable<LunaEvent> { class LunaEvent: Comparable<LunaEvent> {
enum class Type {
companion object { BABY_BOTTLE,
const val TYPE_BABY_BOTTLE = "BABY_BOTTLE" FOOD,
const val TYPE_WEIGHT = "WEIGHT" BREASTFEEDING_LEFT_NIPPLE,
const val TYPE_BREASTFEEDING_LEFT_NIPPLE = "BREASTFEEDING_LEFT_NIPPLE" BREASTFEEDING_BOTH_NIPPLE,
const val TYPE_BREASTFEEDING_BOTH_NIPPLE = "BREASTFEEDING_BOTH_NIPPLE" BREASTFEEDING_RIGHT_NIPPLE,
const val TYPE_BREASTFEEDING_RIGHT_NIPPLE = "BREASTFEEDING_RIGHT_NIPPLE" DIAPERCHANGE_POO,
const val TYPE_DIAPERCHANGE_POO = "DIAPERCHANGE_POO" DIAPERCHANGE_PEE,
const val TYPE_DIAPERCHANGE_PEE = "DIAPERCHANGE_PEE" SLEEP,
const val TYPE_MEDICINE = "MEDICINE" WEIGHT,
const val TYPE_ENEMA = "ENEMA" MEDICINE,
const val TYPE_NOTE = "NOTE" ENEMA,
const val TYPE_CUSTOM = "CUSTOM" NOTE,
const val TYPE_COLIC = "COLIC" COLIC,
const val TYPE_TEMPERATURE = "TEMPERATURE" TEMPERATURE,
const val TYPE_FOOD = "FOOD" PUKE,
BATH,
UNKNOWN
} }
private val jo: JSONObject private val jo: JSONObject
@@ -37,22 +39,36 @@ class LunaEvent: Comparable<LunaEvent> {
set(value) { set(value) {
jo.put("time", value) jo.put("time", value)
} }
var type: String var type: Type
get(): String = jo.getString("type") get(): Type {
return try {
Type.valueOf(jo.getString("type"))
} catch (_: Exception) {
Type.UNKNOWN
}
}
set(value) { set(value) {
jo.put("type", value) jo.put("type", value.name)
} }
var quantity: Int var quantity: Int
get() = jo.optInt("quantity") get() = jo.optInt("quantity")
set(value) { set(value) {
if (value > 0) if (value > 0)
jo.put("quantity", value) jo.put("quantity", value)
else
jo.remove("quantity")
} }
var notes: String var notes: String
get(): String = jo.optString("notes") get(): String = jo.optString("notes")
set(value) { set(value) {
jo.put("notes", value) jo.put("notes", value)
} }
var signature: String
get(): String = jo.optString("signature")
set(value) {
if (value.isNotEmpty())
jo.put("signature", value)
}
constructor(jo: JSONObject) { constructor(jo: JSONObject) {
this.jo = jo this.jo = jo
@@ -61,66 +77,42 @@ class LunaEvent: Comparable<LunaEvent> {
throw IllegalArgumentException("JSONObject is not a LunaEvent") throw IllegalArgumentException("JSONObject is not a LunaEvent")
} }
constructor(type: String) { constructor(event: LunaEvent) {
this.jo = JSONObject()
this.type = event.type
this.time = event.time
this.quantity = event.quantity
this.notes = event.notes
this.signature = event.signature
}
constructor(type: Type) {
this.jo = JSONObject() this.jo = JSONObject()
this.time = System.currentTimeMillis() / 1000 this.time = System.currentTimeMillis() / 1000
this.type = type this.type = type
} }
constructor(type: String, quantity: Int) { constructor(type: Type, 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.quantity = quantity this.quantity = quantity
} }
fun getTypeEmoji(context: Context): String { fun getHeaderEmoji(context: Context): String {
return context.getString( return getHeaderEmoji(context, type)
when (type) {
TYPE_BABY_BOTTLE -> R.string.event_bottle_type
TYPE_WEIGHT -> R.string.event_scale_type
TYPE_BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_type
TYPE_BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_type
TYPE_BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_type
TYPE_DIAPERCHANGE_POO -> R.string.event_diaperchange_poo_type
TYPE_DIAPERCHANGE_PEE -> R.string.event_diaperchange_pee_type
TYPE_MEDICINE -> R.string.event_medicine_type
TYPE_ENEMA -> R.string.event_enema_type
TYPE_NOTE -> R.string.event_note_type
TYPE_TEMPERATURE -> R.string.event_temperature_type
TYPE_COLIC -> R.string.event_colic_type
TYPE_FOOD -> R.string.event_food_type
else -> R.string.event_unknown_type
}
)
} }
fun getTypeDescription(context: Context): String { fun getDialogTitle(context: Context): String {
return context.getString( return getDialogTitle(context, type)
when (type) {
TYPE_BABY_BOTTLE -> R.string.event_bottle_desc
TYPE_WEIGHT -> R.string.event_scale_desc
TYPE_BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_desc
TYPE_BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_desc
TYPE_BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_desc
TYPE_DIAPERCHANGE_POO -> R.string.event_diaperchange_poo_desc
TYPE_DIAPERCHANGE_PEE -> R.string.event_diaperchange_pee_desc
TYPE_MEDICINE -> R.string.event_medicine_desc
TYPE_ENEMA -> R.string.event_enema_desc
TYPE_NOTE -> R.string.event_note_desc
TYPE_TEMPERATURE -> R.string.event_temperature_desc
TYPE_COLIC -> R.string.event_colic_desc
TYPE_FOOD -> R.string.event_food_desc
else -> R.string.event_unknown_desc
}
)
} }
fun getDialogMessage(context: Context): String? { fun getRowItemTitle(context: Context): String {
return when(type) { return getPopupItemTitle(context, type).split(" ", limit = 2).last() // remove emoji
TYPE_MEDICINE -> context.getString(R.string.log_medicine_dialog_description)
else -> null
} }
fun getDialogMessage(context: Context): String {
return getDialogMessage(context, type)
} }
fun toJson(): JSONObject { fun toJson(): JSONObject {
@@ -128,10 +120,101 @@ class LunaEvent: Comparable<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 { override fun compareTo(other: LunaEvent): Int {
return (this.time - other.time).toInt() return (this.time - other.time).toInt()
} }
companion object {
fun getHeaderEmoji(context: Context, type: Type): String {
return context.getString(
when (type) {
Type.BABY_BOTTLE -> R.string.event_bottle_type
Type.WEIGHT -> R.string.event_weight_type
Type.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_type
Type.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_type
Type.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_type
Type.DIAPERCHANGE_POO -> R.string.event_diaperchange_poo_type
Type.DIAPERCHANGE_PEE -> R.string.event_diaperchange_pee_type
Type.MEDICINE -> R.string.event_medicine_type
Type.ENEMA -> R.string.event_enema_type
Type.NOTE -> R.string.event_note_type
Type.TEMPERATURE -> R.string.event_temperature_type
Type.COLIC -> R.string.event_colic_type
Type.FOOD -> R.string.event_food_type
Type.PUKE -> R.string.event_puke_type
Type.BATH -> R.string.event_bath_type
Type.SLEEP -> R.string.event_sleep_type
Type.UNKNOWN -> R.string.event_unknown_type
}
)
}
fun getDialogMessage(context: Context, type: Type): String {
return context.getString(
when (type) {
Type.BABY_BOTTLE -> R.string.log_bottle_dialog_description
Type.MEDICINE -> R.string.log_medicine_dialog_description
Type.TEMPERATURE -> R.string.log_temperature_dialog_description
Type.DIAPERCHANGE_POO,
Type.DIAPERCHANGE_PEE,
Type.PUKE -> R.string.log_amount_dialog_description
Type.WEIGHT -> R.string.log_weight_dialog_description
Type.SLEEP -> R.string.log_sleep_dialog_description
else -> R.string.log_unknown_dialog_description
}
)
}
fun getDialogTitle(context: Context, type: Type): String {
return context.getString(
when (type) {
Type.BABY_BOTTLE -> R.string.event_bottle_desc
Type.WEIGHT -> R.string.event_weight_desc
Type.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_breastfeeding_left_desc
Type.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_breastfeeding_both_desc
Type.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_breastfeeding_right_desc
Type.DIAPERCHANGE_POO -> R.string.event_diaperchange_poo_desc
Type.DIAPERCHANGE_PEE -> R.string.event_diaperchange_pee_desc
Type.MEDICINE -> R.string.event_medicine_desc
Type.ENEMA -> R.string.event_enema_desc
Type.NOTE -> R.string.event_note_desc
Type.TEMPERATURE -> R.string.event_temperature_desc
Type.COLIC -> R.string.event_colic_desc
Type.FOOD -> R.string.event_food_desc
Type.PUKE -> R.string.event_puke_desc
Type.BATH -> R.string.event_bath_desc
Type.SLEEP -> R.string.event_sleep_desc
Type.UNKNOWN -> R.string.event_unknown_desc
}
)
}
// Entries for for popup list
fun getPopupItemTitle(context: Context, type: Type): String {
return context.getString(
when (type) {
Type.BABY_BOTTLE -> R.string.event_type_item_bottle
Type.WEIGHT -> R.string.event_type_item_weight
Type.BREASTFEEDING_LEFT_NIPPLE -> R.string.event_type_item_breastfeeding_left
Type.BREASTFEEDING_BOTH_NIPPLE -> R.string.event_type_item_breastfeeding_both
Type.BREASTFEEDING_RIGHT_NIPPLE -> R.string.event_type_item_breastfeeding_right
Type.DIAPERCHANGE_POO -> R.string.event_type_item_diaperchange_poo
Type.DIAPERCHANGE_PEE -> R.string.event_type_item_diaperchange_pee
Type.MEDICINE -> R.string.event_type_item_medicine
Type.ENEMA -> R.string.event_type_item_enema
Type.NOTE -> R.string.event_type_item_note
Type.TEMPERATURE -> R.string.event_type_item_temperature
Type.COLIC -> R.string.event_type_item_colic
Type.FOOD -> R.string.event_type_item_food
Type.PUKE -> R.string.event_type_item_puke
Type.BATH -> R.string.event_type_item_bath
Type.SLEEP -> R.string.event_type_item_sleep
Type.UNKNOWN -> R.string.event_type_item_unknown
}
)
}
}
} }

View File

@@ -13,9 +13,9 @@ import java.io.FilenameFilter
class FileLogbookRepository: LogbookRepository { class FileLogbookRepository: LogbookRepository {
companion object { companion object {
val TAG = "FileLogbookRepository" const val TAG = "FileLogbookRepository"
val FILE_NAME_START = "data" const val FILE_NAME_START = "data"
val FILE_NAME_END = ".json" const val FILE_NAME_END = ".json"
} }
override fun loadLogbook(context: Context, name: String, listener: LogbookLoadedListener) { override fun loadLogbook(context: Context, name: String, listener: LogbookLoadedListener) {
@@ -32,7 +32,7 @@ class FileLogbookRepository: LogbookRepository {
fun loadLogbook(context: Context, name: String): Logbook { fun loadLogbook(context: Context, name: String): Logbook {
val logbook = Logbook(name) val logbook = Logbook(name)
val fileName = getFileName(name) val fileName = getFileName(name)
val file = File(context.getFilesDir(), fileName) val file = File(context.filesDir, 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()) {
@@ -58,7 +58,7 @@ class FileLogbookRepository: LogbookRepository {
fun saveLogbook(context: Context, logbook: Logbook) { fun saveLogbook(context: Context, logbook: Logbook) {
val fileName = getFileName(logbook.name) val fileName = getFileName(logbook.name)
val file = File(context.getFilesDir(), fileName) val file = File(context.filesDir, 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())
@@ -82,7 +82,7 @@ class FileLogbookRepository: LogbookRepository {
} }
private fun listLogbooks(context: Context): ArrayList<String> { private fun listLogbooks(context: Context): ArrayList<String> {
val logbooksFileNames = context.getFilesDir().list(object: FilenameFilter { val logbooksFileNames = context.filesDir.list(object: FilenameFilter {
override fun accept(dir: File?, name: String?): Boolean { override fun accept(dir: File?, name: String?): Boolean {
if (name == null) if (name == null)
return false return false

View File

@@ -7,13 +7,14 @@ import androidx.core.content.edit
class LocalSettingsRepository(val context: Context) { class LocalSettingsRepository(val context: Context) {
companion object { companion object {
val SHARED_PREFS_FILE_NAME = "lunasettings" const val SHARED_PREFS_FILE_NAME = "lunasettings"
val SHARED_PREFS_BB_CONTENT = "bbcontent" const val SHARED_PREFS_BB_CONTENT = "bbcontent"
val SHARED_PREFS_DATA_REPO = "data_repo" const val SHARED_PREFS_DATA_REPO = "data_repo"
val SHARED_PREFS_DAV_URL = "webdav_url" const val SHARED_PREFS_DAV_URL = "webdav_url"
val SHARED_PREFS_DAV_USER = "webdav_user" const val SHARED_PREFS_DAV_USER = "webdav_user"
val SHARED_PREFS_DAV_PASS = "webdav_password" const val SHARED_PREFS_DAV_PASS = "webdav_password"
val SHARED_PREFS_NO_BREASTFEEDING = "no_breastfeeding" const val SHARED_PREFS_DYNAMIC_MENU = "dynamic_menu"
const val SHARED_PREFS_SIGNATURE = "signature"
} }
enum class DATA_REPO {LOCAL_FILE, WEBDAV} enum class DATA_REPO {LOCAL_FILE, WEBDAV}
val sharedPreferences: SharedPreferences val sharedPreferences: SharedPreferences
@@ -22,32 +23,32 @@ class LocalSettingsRepository(val context: Context) {
sharedPreferences = context.getSharedPreferences(SHARED_PREFS_FILE_NAME, MODE_PRIVATE) sharedPreferences = context.getSharedPreferences(SHARED_PREFS_FILE_NAME, MODE_PRIVATE)
} }
fun saveBabyBottleContent(content: Int) { fun saveSignature(content: String) {
sharedPreferences.edit().putInt(SHARED_PREFS_BB_CONTENT, content).apply() sharedPreferences.edit { putString(SHARED_PREFS_SIGNATURE, content) }
} }
fun loadBabyBottleContent(): Int { fun loadSignature(): String {
return sharedPreferences.getInt(SHARED_PREFS_BB_CONTENT, 1) return sharedPreferences.getString(SHARED_PREFS_SIGNATURE, "") ?: ""
} }
fun saveNoBreastfeeding(content: Boolean) { fun saveDynamicMenu(content: Boolean) {
sharedPreferences.edit().putBoolean(SHARED_PREFS_NO_BREASTFEEDING, content).apply() sharedPreferences.edit { putBoolean(SHARED_PREFS_DYNAMIC_MENU, content) }
} }
fun loadNoBreastfeeding(): Boolean { fun loadDynamicMenu(): Boolean {
return sharedPreferences.getBoolean(SHARED_PREFS_NO_BREASTFEEDING, false) return sharedPreferences.getBoolean(SHARED_PREFS_DYNAMIC_MENU, false)
} }
fun saveDataRepository(repo: DATA_REPO) { fun saveDataRepository(repo: DATA_REPO) {
val spe = sharedPreferences.edit() sharedPreferences.edit(commit = true) {
spe.putString( putString(
SHARED_PREFS_DATA_REPO, SHARED_PREFS_DATA_REPO,
when (repo) { when (repo) {
DATA_REPO.WEBDAV -> "webdav" DATA_REPO.WEBDAV -> "webdav"
DATA_REPO.LOCAL_FILE -> "localfile" DATA_REPO.LOCAL_FILE -> "localfile"
} }
) )
spe.commit() }
} }
fun loadDataRepository(): DATA_REPO { fun loadDataRepository(): DATA_REPO {
@@ -60,11 +61,11 @@ class LocalSettingsRepository(val context: Context) {
} }
fun saveWebdavCredentials(url: String, username: String, password: String) { fun saveWebdavCredentials(url: String, username: String, password: String) {
val spe = sharedPreferences.edit() sharedPreferences.edit(commit = true) {
spe.putString(SHARED_PREFS_DAV_URL, url) putString(SHARED_PREFS_DAV_URL, url)
spe.putString(SHARED_PREFS_DAV_USER, username) putString(SHARED_PREFS_DAV_USER, username)
spe.putString(SHARED_PREFS_DAV_PASS, password) putString(SHARED_PREFS_DAV_PASS, password)
spe.commit() }
} }
fun loadWebdavCredentials(): Array<String>? { fun loadWebdavCredentials(): Array<String>? {

View File

@@ -1,12 +1,17 @@
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 {
/**
* Format time duration in seconds as e.g. "2 hours, 1 min".
* Used for the duration to the next/previous event in the event details dialog.
*/
fun formatTimeDuration(context: Context, secondsDiff: Long): String { fun formatTimeDuration(context: Context, secondsDiff: Long): String {
var seconds = secondsDiff var seconds = secondsDiff
@@ -57,15 +62,14 @@ class DateUtils {
return format(days, hours, R.string.day_ago, R.string.days_ago, R.string.hour_ago, R.string.hours_ago) return format(days, hours, R.string.day_ago, R.string.days_ago, R.string.hour_ago, R.string.hours_ago)
} else if (hours > 0) { } else if (hours > 0) {
return format(hours, minutes, R.string.hour_ago, R.string.hours_ago, R.string.minute_ago, R.string.minutes_ago) return format(hours, minutes, R.string.hour_ago, R.string.hours_ago, R.string.minute_ago, R.string.minutes_ago)
} else if (minutes > 0) {
return format(minutes, seconds, R.string.minute_ago, R.string.minute_ago, R.string.second_ago, R.string.seconds_ago)
} else { } else {
return context.getString(R.string.now) return format(minutes, seconds, R.string.minute_ago, R.string.minute_ago, R.string.second_ago, R.string.seconds_ago)
} }
} }
/** /**
* Formats the provided unix timestamp in a string like "3 hours, 26 minutes ago) * Formats the provided unix timestamp in a string like "3 hours, 26 minutes ago".
* Used for the event list.
*/ */
fun formatTimeAgo(context: Context, unixTime: Long): String { fun formatTimeAgo(context: Context, unixTime: Long): String {
val secondsDiff = (System.currentTimeMillis() / 1000) - unixTime val secondsDiff = (System.currentTimeMillis() / 1000) - unixTime
@@ -100,5 +104,21 @@ class DateUtils {
} }
return formattedTime.toString() return formattedTime.toString()
} }
/**
* Format time as localized string without seconds. E.g. "Sept 18, 2025, 03:36 PM".
* Used in the event detail dialog.
*/
fun formatDateTime(unixTime: Long): String {
val date = Date(unixTime * 1000)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
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()
return dateFormat.format(date)
}
}
} }
} }

View File

@@ -3,8 +3,11 @@ package utils
import android.content.Context import android.content.Context
import android.icu.util.LocaleData import android.icu.util.LocaleData
import android.icu.util.ULocale import android.icu.util.ULocale
import android.os.Build
import android.util.Log
import it.danieleverducci.lunatracker.R import it.danieleverducci.lunatracker.R
import it.danieleverducci.lunatracker.entities.LunaEvent import it.danieleverducci.lunatracker.entities.LunaEvent
import utils.DateUtils.Companion.formatTimeDuration
import java.text.NumberFormat import java.text.NumberFormat
class NumericUtils (val context: Context) { class NumericUtils (val context: Context) {
@@ -14,66 +17,101 @@ class NumericUtils (val context: Context) {
val measurement_unit_weight_tiny: String val measurement_unit_weight_tiny: String
val measurement_unit_temperature_base: String val measurement_unit_temperature_base: String
private fun isMetricSystem(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val measurementSystem = LocaleData.getMeasurementSystem(ULocale.getDefault())
return (measurementSystem == LocaleData.MeasurementSystem.SI)
} else {
val locale = context.resources.configuration.locale
return when (locale.country) {
// https://en.wikipedia.org/wiki/United_States_customary_units
// https://en.wikipedia.org/wiki/Imperial_units
"US" -> false // US IMPERIAL
// UK, Myanmar, Liberia,
"GB", "MM", "LR" -> false // IMPERIAL
else -> true // METRIC
}
}
}
init { init {
this.numberFormat = NumberFormat.getInstance() this.numberFormat = NumberFormat.getInstance()
val measurementSystem = LocaleData.getMeasurementSystem(ULocale.getDefault())
this.measurement_unit_liquid_base = context.getString( this.measurement_unit_liquid_base = context.getString(
if (measurementSystem == LocaleData. MeasurementSystem.SI) if (isMetricSystem())
R.string.measurement_unit_liquid_base_metric R.string.measurement_unit_liquid_base_metric
else else
R.string.measurement_unit_liquid_base_imperial R.string.measurement_unit_liquid_base_imperial
) )
this.measurement_unit_weight_base = context.getString( this.measurement_unit_weight_base = context.getString(
if (measurementSystem == LocaleData. MeasurementSystem.SI) if (isMetricSystem())
R.string.measurement_unit_weight_base_metric R.string.measurement_unit_weight_base_metric
else else
R.string.measurement_unit_weight_base_imperial R.string.measurement_unit_weight_base_imperial
) )
this.measurement_unit_weight_tiny = context.getString( this.measurement_unit_weight_tiny = context.getString(
if (measurementSystem == LocaleData. MeasurementSystem.SI) if (isMetricSystem())
R.string.measurement_unit_weight_tiny_metric R.string.measurement_unit_weight_tiny_metric
else else
R.string.measurement_unit_weight_tiny_imperial R.string.measurement_unit_weight_tiny_imperial
) )
this.measurement_unit_temperature_base = context.getString( this.measurement_unit_temperature_base = context.getString(
if (measurementSystem == LocaleData. MeasurementSystem.SI) if (isMetricSystem())
R.string.measurement_unit_temperature_base_metric R.string.measurement_unit_temperature_base_metric
else else
R.string.measurement_unit_temperature_base_imperial R.string.measurement_unit_temperature_base_imperial
) )
} }
fun formatEventQuantity(item: LunaEvent): String { fun formatEventQuantity(event: LunaEvent): String {
return formatEventQuantity(event.type, event.quantity)
}
fun formatEventQuantity(type: LunaEvent.Type, quantity: Int): String {
val formatted = StringBuilder() val formatted = StringBuilder()
if ((item.quantity ?: 0) > 0) { if (quantity > 0) {
if (item.type == LunaEvent.TYPE_TEMPERATURE) formatted.append(when (type) {
formatted.append((item.quantity / 10.0f).toString()) LunaEvent.Type.TEMPERATURE ->
else (quantity / 10.0f).toString()
formatted.append(item.quantity) LunaEvent.Type.DIAPERCHANGE_POO,
LunaEvent.Type.DIAPERCHANGE_PEE,
LunaEvent.Type.PUKE -> {
val array = context.resources.getStringArray(R.array.AmountLabels)
return array.getOrElse(quantity) {
Log.e("NumericUtils", "Invalid index $quantity")
return ""
}
}
LunaEvent.Type.SLEEP -> formatTimeDuration(context, quantity.toLong())
else -> quantity
})
formatted.append(" ") formatted.append(" ")
formatted.append( formatted.append(
when (item.type) { when (type) {
LunaEvent.TYPE_BABY_BOTTLE -> measurement_unit_liquid_base LunaEvent.Type.BABY_BOTTLE -> measurement_unit_liquid_base
LunaEvent.TYPE_WEIGHT -> measurement_unit_weight_base LunaEvent.Type.WEIGHT -> measurement_unit_weight_base
LunaEvent.TYPE_MEDICINE -> measurement_unit_weight_tiny LunaEvent.Type.MEDICINE -> measurement_unit_weight_tiny
LunaEvent.TYPE_TEMPERATURE -> measurement_unit_temperature_base LunaEvent.Type.TEMPERATURE -> measurement_unit_temperature_base
else -> "" else -> ""
} }
) )
} else {
formatted.append(when (type) {
LunaEvent.Type.SLEEP -> "💤" // baby is sleeping
else -> ""
})
} }
return formatted.toString() return formatted.toString().trim()
} }
/** /**
* Returns a valid quantity range for the event type. * Returns a valid quantity range for the event type.
* @return min, max, normal * @return min, max, normal
*/ */
fun getValidEventQuantityRange(lunaEventType: String): Triple<Int, Int, Int>? { fun getValidEventQuantityRange(lunaEventType: LunaEvent.Type): Triple<Int, Int, Int>? {
val measurementSystem = LocaleData.getMeasurementSystem(ULocale.getDefault())
return when (lunaEventType) { return when (lunaEventType) {
LunaEvent.TYPE_TEMPERATURE -> { LunaEvent.Type.TEMPERATURE -> {
if (measurementSystem == LocaleData. MeasurementSystem.SI) if (isMetricSystem())
Triple( Triple(
context.resources.getInteger(R.integer.human_body_temp_min_metric), context.resources.getInteger(R.integer.human_body_temp_min_metric),
context.resources.getInteger(R.integer.human_body_temp_max_metric), context.resources.getInteger(R.integer.human_body_temp_max_metric),

View File

@@ -86,15 +86,16 @@
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:id="@+id/linear_layout_row1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
android:id="@+id/button_bottle" android:id="@+id/button1_row1"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="2" android:layout_weight="1"
android:layout_margin="5dp" android:layout_margin="5dp"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
@@ -102,7 +103,7 @@
android:text="@string/event_bottle_type"/> android:text="@string/event_bottle_type"/>
<TextView <TextView
android:id="@+id/button_food" android:id="@+id/button2_row1"
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"
@@ -115,77 +116,73 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:id="@+id/linear_layout_row2"
android:layout_height="wrap_content"
android:id="@+id/layout_nipples">
<TextView
android:id="@+id/button_nipple_left"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🤱⬅️"/>
<TextView
android:id="@+id/button_nipple_both"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🤱↔️"/>
<TextView
android:id="@+id/button_nipple_right"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"
android:text="🤱➡️️"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/button_change_poo" android:id="@+id/button1_row2"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="5dp" android:layout_margin="5dp"
android:layout_weight="2" android:layout_weight="1"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:textSize="30sp" android:textSize="30sp"/>
android:text="🚼 💩"/>
<TextView <TextView
android:id="@+id/button_change_pee" android:id="@+id/button2_row2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"/>
<TextView
android:id="@+id/button3_row2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/linear_layout_row3"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/button1_row3"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="5dp" android:layout_margin="5dp"
android:layout_weight="2" android:layout_weight="2"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:textSize="30sp" android:textSize="30sp"/>
android:text="🚼 💧"/>
<TextView
android:id="@+id/button2_row3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="2"
android:background="@drawable/button_background"
android:gravity="center_horizontal"
android:textSize="30sp"/>
<ImageView <ImageView
android:id="@+id/button_more" android:id="@+id/button_more"
android:layout_width="0dp" android:layout_width="60dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_margin="5dp" android:layout_margin="5dp"
android:layout_weight="1" android:layout_weight="0"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:src="@drawable/ic_more" android:src="@drawable/ic_more"

View File

@@ -119,28 +119,57 @@
android:visibility="invisible"/> android:visibility="invisible"/>
</RadioGroup> </RadioGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="@string/settings_signature" />
<EditText
android:id="@+id/settings_signature"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:inputType="textEmailAddress"
android:background="@drawable/textview_background"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:text="@string/settings_signature_desc"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginTop="5dp" android:layout_marginTop="20dp">
android:layout_marginEnd="30dp">
<TextView <TextView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:textStyle="bold" android:textStyle="bold"
android:text="@string/no_breastfeeding" /> android:text="@string/settings_dynamic_menu" />
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_no_breastfeeding" android:id="@+id/switch_dynamic_menu"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_weight="1" /> android:layout_weight="1" />
</LinearLayout> </LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:text="@string/settings_dynamic_menu_desc"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -0,0 +1,50 @@
<?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:orientation="vertical"
android:padding="10dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<com.github.mikephil.charting.charts.HorizontalBarChart
android:id="@+id/bar_chart"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/no_data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:gravity="center"
android:text="No Data"/>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"
android:orientation="horizontal">
<Spinner
android:id="@+id/graph_type_selection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"/>
<Spinner
android:id="@+id/time_range_selection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"/>
</LinearLayout>
</LinearLayout>

View File

@@ -4,19 +4,19 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center"> android:gravity="center"
android:orientation="vertical">
<EditText <Spinner
android:id="@+id/dialog_number_edittext" android:id="@+id/dialog_amount_value"
android:layout_width="150dp" android:layout_width="250dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="number" android:paddingHorizontal="16dp"/>
android:hint="0"
android:background="@drawable/textview_background"/>
<TextView <TextView
android:id="@+id/dialog_date_picker"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginTop="20dp"/>
android:text="g"/>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,35 @@
<?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:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal">
<NumberPicker
android:id="@+id/dialog_number_picker"
android:layout_width="150dp"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="ml"/>
</LinearLayout>
<TextView
android:id="@+id/dialog_date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"/>
</LinearLayout>

View File

@@ -0,0 +1,55 @@
<?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">
<TextView
android:id="@+id/dialog_date_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="💤"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="10dp"
android:orientation="horizontal">
<Button
android:id="@+id/dialog_date_duration_minus5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="-5"/>
<Button
android:id="@+id/dialog_date_duration_now"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/now"/>
<Button
android:id="@+id/dialog_date_duration_plus5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="+5"/>
</LinearLayout>
<TextView
android:id="@+id/dialog_date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"/>
</LinearLayout>

View File

@@ -0,0 +1,57 @@
<?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">
<EditText
android:id="@+id/notes_qty_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="1"
android:inputType="number"
android:hint="@string/log_notes_dialog_qty_hint"
android:background="@drawable/textview_background"/>
<EditText
android:id="@+id/notes_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="top"
android:lines="10"
android:hint="@string/log_notes_dialog_note_hint"
android:background="@drawable/textview_background"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/notes_template_prev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="⬅️"/>
<TextView
android:id="@+id/notes_template_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="➡️"/>
</LinearLayout>
<TextView
android:id="@+id/dialog_date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="15dp"/>
</LinearLayout>

View File

@@ -4,16 +4,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center"> android:gravity="center"
android:orientation="vertical">
<NumberPicker
android:id="@+id/dialog_number_picker"
android:layout_width="150dp"
android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/dialog_date_picker"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"/>
android:layout_marginStart="10dp"
android:text="ml"/>
</LinearLayout> </LinearLayout>

View File

@@ -23,4 +23,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="30sp" android:textSize="30sp"
android:textColor="@color/accent"/> android:textColor="@color/accent"/>
<TextView
android:id="@+id/dialog_date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"/>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,38 @@
<?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:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal">
<EditText
android:id="@+id/dialog_number_edittext"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:inputType="number"
android:hint="0"
android:background="@drawable/textview_background"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="g"/>
</LinearLayout>
<TextView
android:id="@+id/dialog_date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"/>
</LinearLayout>

View File

@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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:padding="20dp"> android:paddingTop="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"
@@ -34,9 +35,7 @@
android:drawablePadding="10dp" android:drawablePadding="10dp"
android:drawableTint="@color/accent" android:drawableTint="@color/accent"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" android:textStyle="bold"/>
android:text="@string/dialog_event_detail_datetime_icon"
app:drawableEndCompat="@drawable/ic_edit" />
<TextView <TextView
android:id="@+id/dialog_event_detail_type_quantity" android:id="@+id/dialog_event_detail_type_quantity"
@@ -46,6 +45,18 @@
android:textSize="28sp" android:textSize="28sp"
android:text="@string/dialog_event_detail_quantity"/> android:text="@string/dialog_event_detail_quantity"/>
<TextView
android:id="@+id/dialog_event_detail_type_date_end"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:drawablePadding="10dp"
android:drawableTint="@color/accent"
android:visibility="gone"
android:textSize="16sp"
android:textStyle="bold"/>
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dp" android:layout_height="200dp"
@@ -61,6 +72,14 @@
</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"

View File

@@ -1,27 +0,0 @@
<?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">
<EditText
android:id="@+id/notes_qty_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lines="1"
android:inputType="number"
android:hint="@string/log_notes_dialog_qty_hint"
android:background="@drawable/textview_background"/>
<EditText
android:id="@+id/notes_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="top"
android:lines="10"
android:hint="@string/log_notes_dialog_note_hint"
android:background="@drawable/textview_background"/>
</LinearLayout>

View File

@@ -2,73 +2,25 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
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="10dp"
android:background="@color/transparent"> android:background="@color/transparent">
<LinearLayout <LinearLayout
android:id="@+id/layout_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/button_medicine" android:id="@+id/button_statistics"
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="10dp"
android:background="@drawable/dropdown_list_item_background" android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText" style="@style/OverflowMenuText"
android:text="@string/overflow_event_medicine"/> android:text="📊 Statistics"/>
<TextView
android:id="@+id/button_enema"
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_enema"/>
<TextView
android:id="@+id/button_note"
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_note"/>
<TextView
android:id="@+id/button_temperature"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_temperature"/>
<TextView
android:id="@+id/button_colic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_colic"/>
<TextView
android:id="@+id/button_scale"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="20dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="@string/overflow_event_scale"/>
<!-- Other buttons are inserted dynamically -->
</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="@+id/tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:padding="10dp"
android:background="@drawable/dropdown_list_item_background"
style="@style/OverflowMenuText"
android:text="Item Template"/>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:gravity="center"
android:padding="5dip" />

View File

@@ -3,18 +3,9 @@
<string name="title">🌜 LunaTracker 🌛</string> <string name="title">🌜 LunaTracker 🌛</string>
<string name="logbook">Ereignisprotokoll</string> <string name="logbook">Ereignisprotokoll</string>
<string name="log_bottle_dialog_title">Fläschchen</string>
<string name="log_bottle_dialog_description">Trinkmenge eingeben</string>
<string name="log_weight_dialog_title">Gewicht</string>
<string name="log_weight_dialog_description">Gewicht eingeben</string>
<string name="log_temperature_dialog_title">Temperatur</string>
<string name="log_temperature_dialog_description">Temperatur eingeben</string>
<string name="event_bottle_desc">Fläschchen</string> <string name="event_bottle_desc">Fläschchen</string>
<string name="event_food_desc">Essen</string> <string name="event_food_desc">Essen</string>
<string name="event_scale_desc">Gewicht</string> <string name="event_weight_desc">Gewicht</string>
<string name="event_breastfeeding_left_desc">Stillen (links)</string> <string name="event_breastfeeding_left_desc">Stillen (links)</string>
<string name="event_breastfeeding_both_desc">Stillen</string> <string name="event_breastfeeding_both_desc">Stillen</string>
<string name="event_breastfeeding_right_desc">Stillen (rechts)</string> <string name="event_breastfeeding_right_desc">Stillen (rechts)</string>
@@ -27,13 +18,6 @@
<string name="event_colic_desc">Blähungskolik</string> <string name="event_colic_desc">Blähungskolik</string>
<string name="event_unknown_desc"></string> <string name="event_unknown_desc"></string>
<string name="overflow_event_scale">⚖️ Gewicht</string>
<string name="overflow_event_medicine">💊 Medikament</string>
<string name="overflow_event_enema">🪠 Einlauf</string>
<string name="overflow_event_note">📝 Notiz</string>
<string name="overflow_event_temperature">🌡️ Temperatur</string>
<string name="overflow_event_colic">💨 Blähungskolik</string>
<string name="toast_event_added">Ereignis gespeichert</string> <string name="toast_event_added">Ereignis gespeichert</string>
<string name="toast_logbook_saved">Logbuch gespeichert</string> <string name="toast_logbook_saved">Logbuch gespeichert</string>
<string name="toast_event_add_error">Ereignis konnte nicht protokolliert werden</string> <string name="toast_event_add_error">Ereignis konnte nicht protokolliert werden</string>
@@ -50,8 +34,6 @@
<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_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>
@@ -78,10 +60,13 @@
<string name="trim_logbook_dialog_button_ok">Jetzt bereinigen</string> <string name="trim_logbook_dialog_button_ok">Jetzt bereinigen</string>
<string name="trim_logbook_dialog_button_cancel">Später erinnern</string> <string name="trim_logbook_dialog_button_cancel">Später erinnern</string>
<string name="log_notes_dialog_description">Notizen:</string> <string name="log_bottle_dialog_description">Trinkmenge eingeben</string>
<string name="log_medicine_dialog_description">Medikamentenname, Menge, Art, Notizen, …:</string> <string name="log_medicine_dialog_description">Medikamentenname, Menge, Art, Notizen, …:</string>
<string name="log_notes_dialog_qty_hint">Menge (optional)</string> <string name="log_notes_dialog_description">Notizen:</string>
<string name="log_notes_dialog_note_hint">Notiz eingeben</string> <string name="log_notes_dialog_note_hint">Notiz eingeben</string>
<string name="log_notes_dialog_qty_hint">Menge (optional)</string>
<string name="log_temperature_dialog_description">Temperatur eingeben</string>
<string name="log_weight_dialog_description">Gewicht eingeben</string>
<string name="dialog_event_detail_title">Ereignisdetails</string> <string name="dialog_event_detail_title">Ereignisdetails</string>
<string name="dialog_event_detail_close_button">OK</string> <string name="dialog_event_detail_close_button">OK</string>

View File

@@ -3,18 +3,9 @@
<string name="title">🌜 LunaTracker 🌛</string> <string name="title">🌜 LunaTracker 🌛</string>
<string name="logbook">Entrées enregistrées</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_bottle_desc">Biberon</string>
<string name="event_food_desc">Nourriture</string> <string name="event_food_desc">Nourriture</string>
<string name="event_scale_desc">Poids</string> <string name="event_weight_desc">Poids</string>
<string name="event_breastfeeding_left_desc">Allaitement (sein gauche)</string> <string name="event_breastfeeding_left_desc">Allaitement (sein gauche)</string>
<string name="event_breastfeeding_both_desc">Allaitement</string> <string name="event_breastfeeding_both_desc">Allaitement</string>
<string name="event_breastfeeding_right_desc">Allaitement (sein droit)</string> <string name="event_breastfeeding_right_desc">Allaitement (sein droit)</string>
@@ -27,13 +18,6 @@
<string name="event_colic_desc">Colique gazeuse</string> <string name="event_colic_desc">Colique gazeuse</string>
<string name="event_unknown_desc"></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_event_added">Entrée ajoutée</string>
<string name="toast_logbook_saved">Journal ajouté</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_event_add_error">Impossible d\'enregistrer cette entrée</string>
@@ -76,10 +60,13 @@
<string name="trim_logbook_dialog_button_ok">Supprimer les vieilles entrées maintenant</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="trim_logbook_dialog_button_cancel">Me rappeller plus tard</string>
<string name="log_notes_dialog_description">Notes:</string> <string name="log_bottle_dialog_description">Renseignez la quantité contenue dans le biberon</string>
<string name="log_medicine_dialog_description">nom du médicament, quantité, type, 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_description">Notes:</string>
<string name="log_notes_dialog_note_hint">Notes ...</string> <string name="log_notes_dialog_note_hint">Notes ...</string>
<string name="log_notes_dialog_qty_hint">Quantité (ou vide)</string>
<string name="log_temperature_dialog_description">Renseignez la Température</string>
<string name="log_weight_dialog_description">Renseignez le poids</string>
<string name="dialog_event_detail_title">Détails de l\'entrée</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_close_button">OK</string>

View File

@@ -3,25 +3,9 @@
<string name="title">🌜 LunaTracker 🌛</string> <string name="title">🌜 LunaTracker 🌛</string>
<string name="logbook">Diario di bordo</string> <string name="logbook">Diario di bordo</string>
<string name="log_bottle_dialog_title">Biberon</string>
<string name="log_bottle_dialog_description">Inserisci la quantità contenuta nel biberon</string>
<string name="log_weight_dialog_title">Pesata</string>
<string name="log_weight_dialog_description">Inserisci il peso rilevato</string>
<string name="log_temperature_dialog_title">Temperatura</string>
<string name="log_temperature_dialog_description">Inserisci la temperatura</string>
<string name="overflow_event_scale">⚖️ Peso</string>
<string name="overflow_event_medicine">💊 Medicina</string>
<string name="overflow_event_enema">🪠 Clistere</string>
<string name="overflow_event_note">📝 Nota</string>
<string name="overflow_event_temperature">🌡️ Temperatura</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_food_desc">Cibo</string>
<string name="event_scale_desc">Pesata</string> <string name="event_weight_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>
<string name="event_breastfeeding_right_desc">Allatt. al seno (dx)</string> <string name="event_breastfeeding_right_desc">Allatt. al seno (dx)</string>
@@ -76,10 +60,13 @@
<string name="trim_logbook_dialog_button_ok">Cancella i più vecchi</string> <string name="trim_logbook_dialog_button_ok">Cancella i più vecchi</string>
<string name="trim_logbook_dialog_button_cancel">Ricordamelo dopo</string> <string name="trim_logbook_dialog_button_cancel">Ricordamelo dopo</string>
<string name="log_notes_dialog_description">Note:</string> <string name="log_bottle_dialog_description">Inserisci la quantità contenuta nel biberon</string>
<string name="log_medicine_dialog_description">Nome della medicina, quantità, formato, note…:</string> <string name="log_medicine_dialog_description">Nome della medicina, quantità, formato, note…:</string>
<string name="log_notes_dialog_qty_hint">Quantità, o vuoto</string> <string name="log_notes_dialog_description">Note:</string>
<string name="log_notes_dialog_note_hint">Inserisci le note</string> <string name="log_notes_dialog_note_hint">Inserisci le note</string>
<string name="log_notes_dialog_qty_hint">Quantità, o vuoto</string>
<string name="log_temperature_dialog_description">Inserisci la temperatura</string>
<string name="log_weight_dialog_description">Inserisci il peso rilevato</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_close_button">OK</string>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="AmountLabels">
<item>@string/amount_unspecified</item>
<item>@string/amount_little</item>
<item>@string/amount_normal</item>
<item>@string/amount_plenty</item>
</string-array>
<string-array name="StatisticsTypeLabels">
<item>@string/statistics_bottle_sum</item>
<item>@string/statistics_bottle_events</item>
<item>@string/statistics_sleep_sum</item>
<item>@string/statistics_sleep_events</item>
<item>@string/statistics_sleep_pattern</item>
<item>@string/statistics_medicine_events</item>
</string-array>
<string-array name="StatisticsTypeValues">
<item>BOTTLE_SUM</item>
<item>BOTTLE_EVENTS</item>
<item>SLEEP_SUM</item>
<item>SLEEP_EVENTS</item>
<item>SLEEP_PATTERN</item>
<item>MEDICINE_EVENTS</item>
</string-array>
<string-array name="StatisticsTimeLabels">
<item>Day</item>
<item>Week</item>
<item>Month</item>
</string-array>
<string-array name="StatisticsTimeValues">
<item>DAY</item>
<item>WEEK</item>
<item>MONTH</item>
</string-array>
</resources>

View File

@@ -3,21 +3,13 @@
<string name="title">🌜 LunaTracker 🌛</string> <string name="title">🌜 LunaTracker 🌛</string>
<string name="logbook">Logged events</string> <string name="logbook">Logged events</string>
<string name="log_bottle_dialog_title">Baby bottle</string> <!-- menu header items -->
<string name="log_bottle_dialog_description">Insert the quantity contained in the baby bottle</string>
<string name="log_weight_dialog_title">Weight</string>
<string name="log_weight_dialog_description">Insert the weight</string>
<string name="log_temperature_dialog_title">Temperature</string>
<string name="log_temperature_dialog_description">Insert the temperature</string>
<string name="event_bottle_type" translatable="false">🍼</string> <string name="event_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_weight_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>
<string name="event_breastfeeding_right_type" translatable="false">🤱</string> <string name="event_breastfeeding_right_type" translatable="false">🤱➡️️</string>
<string name="event_diaperchange_poo_type" translatable="false">🚼 💩</string> <string name="event_diaperchange_poo_type" translatable="false">🚼 💩</string>
<string name="event_diaperchange_pee_type" translatable="false">🚼 💧</string> <string name="event_diaperchange_pee_type" translatable="false">🚼 💧</string>
<string name="event_medicine_type" translatable="false">💊</string> <string name="event_medicine_type" translatable="false">💊</string>
@@ -25,34 +17,54 @@
<string name="event_note_type" translatable="false">📝</string> <string name="event_note_type" translatable="false">📝</string>
<string name="event_temperature_type" translatable="false">🌡️</string> <string name="event_temperature_type" translatable="false">🌡️</string>
<string name="event_colic_type" translatable="false">💨</string> <string name="event_colic_type" translatable="false">💨</string>
<string name="event_unknown_type" translatable="false">\?</string> <string name="event_puke_type" translatable="false">🤮</string>
<string name="event_bath_type" translatable="false">🛁</string>
<string name="event_sleep_type" translatable="false">💤</string>
<string name="event_unknown_type" translatable="false"></string>
<string name="event_bottle_desc">Baby bottle</string> <!-- dropdown menu item -->
<string name="event_type_item_bottle">🍼 Bottle</string>
<string name="event_type_item_food">🥣 Food</string>
<string name="event_type_item_weight">⚖️ Weight</string>
<string name="event_type_item_breastfeeding_left">🤱⬅️ Nursing</string>
<string name="event_type_item_breastfeeding_both">🤱↔️ Nursing</string>
<string name="event_type_item_breastfeeding_right">🤱➡️️ Nursing</string>
<string name="event_type_item_diaperchange_poo">🚼💩 Diaper</string>
<string name="event_type_item_diaperchange_pee">🚼💧 Diaper</string>
<string name="event_type_item_medicine">💊 Medicine</string>
<string name="event_type_item_enema">🪠 Enema</string>
<string name="event_type_item_note">📝 Note</string>
<string name="event_type_item_temperature">🌡️ Temperature</string>
<string name="event_type_item_colic">💨 Colic</string>
<string name="event_type_item_puke">🤮 Puke</string>
<string name="event_type_item_sleep">💤 Sleep</string>
<string name="event_type_item_bath">🛁 Bath</string>
<string name="event_type_item_unknown">❓ Unknown</string>
<!-- dialog titles -->
<string name="event_bottle_desc">Milk Bottle</string>
<string name="event_food_desc">Food</string> <string name="event_food_desc">Food</string>
<string name="event_scale_desc">Weight</string> <string name="event_weight_desc">Weight</string>
<string name="event_breastfeeding_left_desc">Breastfeeding (left)</string> <string name="event_breastfeeding_left_desc">Nursing (left)</string>
<string name="event_breastfeeding_both_desc">Breastfeeding</string> <string name="event_breastfeeding_both_desc">Nursing (both)</string>
<string name="event_breastfeeding_right_desc">Breastfeeding (right)</string> <string name="event_breastfeeding_right_desc">Nursing (right)</string>
<string name="event_diaperchange_poo_desc">Diaper chg (poo)</string> <string name="event_diaperchange_poo_desc">Diaper Change (poo)</string>
<string name="event_diaperchange_pee_desc">Diaper chg (pee)</string> <string name="event_diaperchange_pee_desc">Diaper Change (pee)</string>
<string name="event_medicine_desc">Medicine</string> <string name="event_medicine_desc">Medicine</string>
<string name="event_enema_desc">Enema</string> <string name="event_enema_desc">Enema</string>
<string name="event_note_desc">Note</string> <string name="event_note_desc">Note</string>
<string name="event_temperature_desc">Temperature</string> <string name="event_temperature_desc">Temperature</string>
<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_puke_desc">Puke</string>
<string name="event_bath_desc">Bath</string>
<string name="overflow_event_scale">⚖️ Weight</string> <string name="event_sleep_desc">Sleep</string>
<string name="overflow_event_medicine">💊 Medicine</string> <string name="event_unknown_desc">Unknown</string>
<string name="overflow_event_enema">🪠 Enema</string>
<string name="overflow_event_note">📝 Note</string>
<string name="overflow_event_temperature">🌡️ Temperature</string>
<string name="overflow_event_colic">💨 Gaseous colic</string>
<string name="toast_event_added">Event logged</string> <string name="toast_event_added">Event logged</string>
<string name="toast_logbook_saved">Logbook saved</string> <string name="toast_logbook_saved">Logbook saved</string>
<string name="toast_event_add_error">Unable to log the event</string> <string name="toast_event_add_error">Unable to log the event</string>
<string name="toast_integer_error">Invalid value. Insert an integer.</string> <string name="toast_integer_error">Invalid value. Insert an integer.</string>
<string name="toast_date_error">Invalid date.</string>
<string name="now">now</string> <string name="now">now</string>
<string name="second_ago">sec</string> <string name="second_ago">sec</string>
@@ -66,14 +78,23 @@
<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_unspecified"></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>
<string name="no_connection_retry">Retry</string> <string name="no_connection_retry">Retry</string>
<string name="no_breastfeeding">No Breastfeeding</string> <string name="statistics_title">Statistics</string>
<string name="settings_dynamic_menu">Dynamic Menu</string>
<string name="settings_dynamic_menu_desc">Populate the header menu with the most used events.</string>
<string name="settings_title">Settings</string> <string name="settings_title">Settings</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_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>
@@ -99,10 +120,16 @@
<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>
<string name="log_notes_dialog_description">Notes:</string> <string name="log_amount_dialog_description">Select the amount:</string>
<string name="log_bottle_dialog_description">Insert the quantity contained in the baby bottle:</string>
<string name="log_medicine_dialog_description">Medicine name, quantity, type, notes…:</string> <string name="log_medicine_dialog_description">Medicine name, quantity, type, notes…:</string>
<string name="log_notes_dialog_qty_hint">Quantity (or empty)</string> <string name="log_notes_dialog_description">Notes:</string>
<string name="log_notes_dialog_note_hint">Write some notes</string> <string name="log_notes_dialog_note_hint">Write some notes</string>
<string name="log_notes_dialog_qty_hint">Quantity (or empty)</string>
<string name="log_temperature_dialog_description">Select the temperature:</string>
<string name="log_unknown_dialog_description"></string>
<string name="log_weight_dialog_description">Insert the weight:</string>
<string name="log_sleep_dialog_description">Set sleep duration:</string>
<string name="measurement_unit_liquid_base_metric" translatable="false">ml</string> <string name="measurement_unit_liquid_base_metric" translatable="false">ml</string>
<string name="measurement_unit_weight_base_metric" translatable="false">g</string> <string name="measurement_unit_weight_base_metric" translatable="false">g</string>
@@ -113,16 +140,24 @@
<string name="measurement_unit_temperature_base_imperial" translatable="false">°F</string> <string name="measurement_unit_temperature_base_imperial" translatable="false">°F</string>
<string name="measurement_unit_temperature_base_metric" translatable="false">°C</string> <string name="measurement_unit_temperature_base_metric" translatable="false">°C</string>
<string name="statistics_bottle_events">Bottle Events</string>
<string name="statistics_bottle_sum">Bottle Per Day</string>
<string name="statistics_medicine_events">Medicine Events</string>
<string name="statistics_sleep_sum">Sleep Per Day</string>
<string name="statistics_sleep_events">Sleep Events</string>
<string name="statistics_sleep_pattern">Sleep Pattern</string>
<string name="row_luna_event_description">Description</string> <string name="row_luna_event_description">Description</string>
<string name="row_luna_event_quantity">Qty</string> <string name="row_luna_event_quantity">Qty</string>
<string name="row_luna_event_time">Time</string> <string name="row_luna_event_time">Time</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">🕒 %s</string> <string name="dialog_event_detail_close_button">Close</string>
<string name="dialog_event_detail_close_button">OK</string> <string name="dialog_event_detail_save_button">Save</string>
<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_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>

View File

@@ -4,6 +4,11 @@
<style name="Theme.LunaTracker" parent="Theme.AppCompat.NoActionBar"> <style name="Theme.LunaTracker" parent="Theme.AppCompat.NoActionBar">
<item name="colorAccent">@color/accent</item> <item name="colorAccent">@color/accent</item>
<item name="android:textColor">@color/textColor</item> <item name="android:textColor">@color/textColor</item>
<!-- make the screen not overlap with the system bars -->
<item name="android:fitsSystemWindows">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style> </style>
<style name="OverflowMenuText"> <style name="OverflowMenuText">

View File

@@ -9,8 +9,11 @@ lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0" activityCompose = "1.8.0"
composeBom = "2024.04.01" composeBom = "2024.04.01"
appcompat = "1.7.0" appcompat = "1.7.0"
mpandroidchart = "v4.2.2"
mpandroidchartVersion = "v3.1.0"
recyclerview = "1.3.2" recyclerview = "1.3.2"
material = "1.12.0" material = "1.12.0"
sardineAndroid = "v0.9"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -30,6 +33,9 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }
mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchart" }
mpandroidchart-vv310 = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchartVersion" }
sardine-android = { module = "com.github.thegrizzlylabs:sardine-android", version.ref = "sardineAndroid" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }

View File

@@ -16,7 +16,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven(url = "https://jitpack.io") maven(url = uri("https://jitpack.io"))
} }
} }