forked from penguin86/luna-tracker
Compare commits
7 Commits
cbd18cd891
...
feature/cr
| Author | SHA1 | Date | |
|---|---|---|---|
| 96422da7eb | |||
| c0e0ec8f51 | |||
| 235a355a05 | |||
| 81473f8f9f | |||
| 0776e4d6c2 | |||
| 3e8af97757 | |||
| 6a995d6561 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -107,5 +107,3 @@ app/release/output-metadata.json
|
||||
# Other
|
||||
app/src/main/java/it/danieleverducci/lunatracker/TemporaryHardcodedCredentials.kt
|
||||
.kotlin/sessions/*
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
|
||||
@@ -329,18 +329,38 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
fun startBreastfeedingTimer(eventType: String) {
|
||||
// Check if timer already running
|
||||
// Check if timer already running locally
|
||||
if (bfTimerType != null) {
|
||||
Toast.makeText(this, R.string.breastfeeding_timer_already_running, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
if (sleepTimerStartTime > 0) {
|
||||
Toast.makeText(this, R.string.another_timer_already_running, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Save timer state
|
||||
bfTimerStartTime = System.currentTimeMillis()
|
||||
// Check logbook for existing ongoing breastfeeding event (from another device)
|
||||
val existingOngoing = logbook?.logs?.let { LunaEvent.findOngoingBreastfeeding(it) }
|
||||
if (existingOngoing != null) {
|
||||
bfTimerStartTime = existingOngoing.time * 1000
|
||||
bfTimerType = existingOngoing.type
|
||||
saveBreastfeedingTimerState()
|
||||
showBreastfeedingTimerDialog(existingOngoing.type)
|
||||
return
|
||||
}
|
||||
|
||||
// Create ongoing event and save to logbook (syncs via WebDAV)
|
||||
val event = LunaEvent(eventType)
|
||||
event.ongoing = true
|
||||
event.signature = signature
|
||||
bfTimerStartTime = event.time * 1000
|
||||
bfTimerType = eventType
|
||||
saveBreastfeedingTimerState()
|
||||
|
||||
// Show timer dialog
|
||||
logbook?.logs?.add(0, event)
|
||||
recyclerView.adapter?.notifyItemInserted(0)
|
||||
saveLogbook()
|
||||
|
||||
showBreastfeedingTimerDialog(eventType)
|
||||
}
|
||||
|
||||
@@ -383,13 +403,19 @@ class MainActivity : AppCompatActivity() {
|
||||
fun stopBreastfeedingTimer() {
|
||||
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
|
||||
|
||||
val durationMillis = System.currentTimeMillis() - bfTimerStartTime
|
||||
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
|
||||
|
||||
val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoingBreastfeeding(it) }
|
||||
val eventType = bfTimerType
|
||||
clearBreastfeedingTimerState()
|
||||
|
||||
if (eventType != null) {
|
||||
if (ongoingEvent != null) {
|
||||
ongoingEvent.finalizeOngoing(System.currentTimeMillis())
|
||||
logbook?.sort()
|
||||
recyclerView.adapter?.notifyDataSetChanged()
|
||||
saveLogbook()
|
||||
} else if (eventType != null) {
|
||||
// Fallback: no ongoing event found (sync issue), create event the old way
|
||||
val durationMillis = System.currentTimeMillis() - bfTimerStartTime
|
||||
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt())
|
||||
logEvent(LunaEvent(eventType, durationMinutes))
|
||||
}
|
||||
}
|
||||
@@ -397,9 +423,23 @@ class MainActivity : AppCompatActivity() {
|
||||
fun cancelBreastfeedingTimer() {
|
||||
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
|
||||
clearBreastfeedingTimerState()
|
||||
|
||||
// Remove ongoing event from logbook
|
||||
val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoingBreastfeeding(it) }
|
||||
if (ongoingEvent != null) {
|
||||
logbook?.removedSinceLoad?.add(ongoingEvent.fingerprint())
|
||||
logbook?.logs?.remove(ongoingEvent)
|
||||
recyclerView.adapter?.notifyDataSetChanged()
|
||||
saveLogbook()
|
||||
}
|
||||
}
|
||||
|
||||
fun askBreastfeedingDuration(eventType: String) {
|
||||
// Cancel any running timer to avoid orphaned ongoing events
|
||||
if (bfTimerType != null) {
|
||||
cancelBreastfeedingTimer()
|
||||
}
|
||||
|
||||
val d = AlertDialog.Builder(this)
|
||||
val dialogView = layoutInflater.inflate(R.layout.breastfeeding_duration_dialog, null)
|
||||
d.setTitle(R.string.breastfeeding_duration_title)
|
||||
@@ -443,17 +483,36 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
// Sleep timer methods
|
||||
fun startSleepTimer() {
|
||||
// Check if timer already running
|
||||
// Check if timer already running locally
|
||||
if (sleepTimerStartTime > 0) {
|
||||
Toast.makeText(this, R.string.sleep_timer_already_running, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
if (bfTimerType != null) {
|
||||
Toast.makeText(this, R.string.another_timer_already_running, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Save timer state
|
||||
sleepTimerStartTime = System.currentTimeMillis()
|
||||
// Check logbook for existing ongoing sleep event (from another device)
|
||||
val existingOngoing = logbook?.logs?.let { LunaEvent.findOngoing(it, LunaEvent.TYPE_SLEEP) }
|
||||
if (existingOngoing != null) {
|
||||
sleepTimerStartTime = existingOngoing.time * 1000
|
||||
saveSleepTimerState()
|
||||
showSleepTimerDialog()
|
||||
return
|
||||
}
|
||||
|
||||
// Create ongoing event and save to logbook (syncs via WebDAV)
|
||||
val event = LunaEvent(LunaEvent.TYPE_SLEEP)
|
||||
event.ongoing = true
|
||||
event.signature = signature
|
||||
sleepTimerStartTime = event.time * 1000
|
||||
saveSleepTimerState()
|
||||
|
||||
// Show timer dialog
|
||||
logbook?.logs?.add(0, event)
|
||||
recyclerView.adapter?.notifyItemInserted(0)
|
||||
saveLogbook()
|
||||
|
||||
showSleepTimerDialog()
|
||||
}
|
||||
|
||||
@@ -499,20 +558,42 @@ class MainActivity : AppCompatActivity() {
|
||||
fun stopSleepTimer() {
|
||||
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
|
||||
|
||||
val durationMillis = System.currentTimeMillis() - sleepTimerStartTime
|
||||
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
|
||||
|
||||
val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoing(it, LunaEvent.TYPE_SLEEP) }
|
||||
clearSleepTimerState()
|
||||
|
||||
logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, durationMinutes))
|
||||
if (ongoingEvent != null) {
|
||||
ongoingEvent.finalizeOngoing(System.currentTimeMillis())
|
||||
logbook?.sort()
|
||||
recyclerView.adapter?.notifyDataSetChanged()
|
||||
saveLogbook()
|
||||
} else {
|
||||
// Fallback: no ongoing event found (sync issue), create event the old way
|
||||
val durationMillis = System.currentTimeMillis() - sleepTimerStartTime
|
||||
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt())
|
||||
logEvent(LunaEvent(LunaEvent.TYPE_SLEEP, durationMinutes))
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelSleepTimer() {
|
||||
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
|
||||
clearSleepTimerState()
|
||||
|
||||
// Remove ongoing event from logbook
|
||||
val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoing(it, LunaEvent.TYPE_SLEEP) }
|
||||
if (ongoingEvent != null) {
|
||||
logbook?.removedSinceLoad?.add(ongoingEvent.fingerprint())
|
||||
logbook?.logs?.remove(ongoingEvent)
|
||||
recyclerView.adapter?.notifyDataSetChanged()
|
||||
saveLogbook()
|
||||
}
|
||||
}
|
||||
|
||||
fun askSleepDuration() {
|
||||
// Cancel any running timer to avoid orphaned ongoing events
|
||||
if (sleepTimerStartTime > 0) {
|
||||
cancelSleepTimer()
|
||||
}
|
||||
|
||||
val d = AlertDialog.Builder(this)
|
||||
val dialogView = layoutInflater.inflate(R.layout.sleep_duration_dialog, null)
|
||||
d.setTitle(R.string.sleep_duration_title)
|
||||
@@ -829,6 +910,27 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
fun showEventDetailDialog(event: LunaEvent, items: ArrayList<LunaEvent>) {
|
||||
// If event is ongoing, show the appropriate timer dialog instead
|
||||
if (event.ongoing) {
|
||||
if (event.type in LunaEvent.BREASTFEEDING_TYPES) {
|
||||
if (bfTimerDialog?.isShowing == true) return
|
||||
if (bfTimerType == null) {
|
||||
bfTimerStartTime = event.time * 1000
|
||||
bfTimerType = event.type
|
||||
saveBreastfeedingTimerState()
|
||||
}
|
||||
showBreastfeedingTimerDialog(event.type)
|
||||
} else if (event.type == LunaEvent.TYPE_SLEEP) {
|
||||
if (sleepTimerDialog?.isShowing == true) return
|
||||
if (sleepTimerStartTime == 0L) {
|
||||
sleepTimerStartTime = event.time * 1000
|
||||
saveSleepTimerState()
|
||||
}
|
||||
showSleepTimerDialog()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Do not update list while the detail is shown, to avoid changing the object below while it is changed by the user
|
||||
pauseLogbookUpdate = true
|
||||
val d = AlertDialog.Builder(this)
|
||||
@@ -857,6 +959,8 @@ class MainActivity : AppCompatActivity() {
|
||||
TimePickerDialog(this, { _, hour, minute ->
|
||||
val pickedDateTime = Calendar.getInstance()
|
||||
pickedDateTime.set(year, month, day, hour, minute)
|
||||
// Track old fingerprint so merge doesn't re-add the old version
|
||||
logbook?.removedSinceLoad?.add(event.fingerprint())
|
||||
// Save event and move it to the right position in the logbook
|
||||
event.time = pickedDateTime.time.time / 1000 // Seconds since epoch
|
||||
dateTextView.text = String.format(getString(R.string.dialog_event_detail_datetime_icon), DateUtils.formatDateTime(event.time))
|
||||
@@ -890,12 +994,20 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
picker.minValue = 1
|
||||
picker.maxValue = if (isSleep) 180 else 60
|
||||
val oldQuantity = event.quantity
|
||||
picker.value = if (event.quantity > 0) Math.min(event.quantity, picker.maxValue) else if (isSleep) 30 else 15
|
||||
|
||||
pickerDialog.setTitle(if (isSleep) R.string.sleep_duration_title else R.string.breastfeeding_duration_title)
|
||||
pickerDialog.setView(pickerView)
|
||||
pickerDialog.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
event.quantity = picker.value
|
||||
val newQuantity = picker.value
|
||||
if (newQuantity != oldQuantity) {
|
||||
// Track old fingerprint so merge doesn't re-add the old version
|
||||
logbook?.removedSinceLoad?.add(event.fingerprint())
|
||||
// Adjust end time based on duration change (duration reduced = end time earlier)
|
||||
event.time = event.time - (oldQuantity - newQuantity) * 60L
|
||||
event.quantity = newQuantity
|
||||
}
|
||||
quantityTextView.text = NumericUtils(this@MainActivity).formatEventQuantity(event)
|
||||
recyclerView.adapter?.notifyDataSetChanged()
|
||||
saveLogbook()
|
||||
@@ -1219,6 +1331,7 @@ class MainActivity : AppCompatActivity() {
|
||||
findViewById<View>(R.id.no_connection_screen).visibility = View.GONE
|
||||
logbook = lb
|
||||
showLogbook()
|
||||
checkForOngoingEvents()
|
||||
|
||||
if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) {
|
||||
for (e in logbook?.logs ?: listOf()) {
|
||||
@@ -1277,6 +1390,71 @@ class MainActivity : AppCompatActivity() {
|
||||
})
|
||||
}
|
||||
|
||||
fun checkForOngoingEvents() {
|
||||
val logs = logbook?.logs ?: return
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
val maxOngoingSeconds = 24 * 60 * 60 // 24 hours
|
||||
|
||||
// Check for stale ongoing events (>24h)
|
||||
for (event in logs.toList()) {
|
||||
if (event.ongoing && (now - event.time) > maxOngoingSeconds) {
|
||||
showStaleTimerDialog(event)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Breastfeeding timer sync
|
||||
val ongoingBf = LunaEvent.findOngoingBreastfeeding(logs)
|
||||
if (bfTimerType == null) {
|
||||
// Adopt ongoing timer from another device
|
||||
if (ongoingBf != null) {
|
||||
bfTimerStartTime = ongoingBf.time * 1000
|
||||
bfTimerType = ongoingBf.type
|
||||
saveBreastfeedingTimerState()
|
||||
showBreastfeedingTimerDialog(ongoingBf.type)
|
||||
}
|
||||
} else if (ongoingBf == null) {
|
||||
// Timer running locally but cancelled/stopped on other device
|
||||
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
|
||||
bfTimerDialog?.dismiss()
|
||||
clearBreastfeedingTimerState()
|
||||
}
|
||||
|
||||
// Sleep timer sync
|
||||
val ongoingSleep = LunaEvent.findOngoing(logs, LunaEvent.TYPE_SLEEP)
|
||||
if (sleepTimerStartTime == 0L) {
|
||||
// Adopt ongoing timer from another device
|
||||
if (ongoingSleep != null) {
|
||||
sleepTimerStartTime = ongoingSleep.time * 1000
|
||||
saveSleepTimerState()
|
||||
showSleepTimerDialog()
|
||||
}
|
||||
} else if (ongoingSleep == null) {
|
||||
// Timer running locally but cancelled/stopped on other device
|
||||
sleepTimerDialog?.dismiss()
|
||||
clearSleepTimerState()
|
||||
}
|
||||
}
|
||||
|
||||
fun showStaleTimerDialog(event: LunaEvent) {
|
||||
val d = AlertDialog.Builder(this)
|
||||
d.setTitle(R.string.stale_timer_title)
|
||||
d.setMessage(R.string.stale_timer_message)
|
||||
d.setPositiveButton(R.string.stale_timer_finalize) { _, _ ->
|
||||
event.finalizeOngoing(System.currentTimeMillis())
|
||||
logbook?.sort()
|
||||
recyclerView.adapter?.notifyDataSetChanged()
|
||||
saveLogbook()
|
||||
}
|
||||
d.setNegativeButton(R.string.stale_timer_delete) { _, _ ->
|
||||
logbook?.removedSinceLoad?.add(event.fingerprint())
|
||||
logbook?.logs?.remove(event)
|
||||
recyclerView.adapter?.notifyDataSetChanged()
|
||||
saveLogbook()
|
||||
}
|
||||
d.create().show()
|
||||
}
|
||||
|
||||
fun logEvent(event: LunaEvent) {
|
||||
savingEvent(true)
|
||||
|
||||
@@ -1300,6 +1478,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
// Update data
|
||||
setLoading(true)
|
||||
logbook?.removedSinceLoad?.add(event.fingerprint())
|
||||
logbook?.logs?.remove(event)
|
||||
recyclerView.adapter?.notifyDataSetChanged()
|
||||
saveLogbook()
|
||||
|
||||
@@ -59,6 +59,21 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
|
||||
LunaEvent.TYPE_CUSTOM -> item.notes
|
||||
else -> item.getTypeDescription(context)
|
||||
}
|
||||
if (item.ongoing) {
|
||||
// Show ongoing timer info
|
||||
val elapsedSeconds = (System.currentTimeMillis() / 1000) - item.time
|
||||
val minutes = elapsedSeconds / 60
|
||||
val hours = minutes / 60
|
||||
holder.time.text = if (hours > 0) {
|
||||
String.format("%dh %02dm", hours, minutes % 60)
|
||||
} else {
|
||||
String.format("%d min", minutes)
|
||||
}
|
||||
holder.quantity.text = context.getString(R.string.event_ongoing)
|
||||
holder.quantity.setTextColor(ContextCompat.getColor(context, R.color.accent))
|
||||
return
|
||||
}
|
||||
|
||||
holder.time.text = DateUtils.formatTimeAgo(context, item.time)
|
||||
var quantityText = numericUtils.formatEventQuantity(item)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ class Logbook(val name: String) {
|
||||
const val MAX_SAFE_LOGBOOK_SIZE = 30000
|
||||
}
|
||||
val logs = ArrayList<LunaEvent>()
|
||||
val removedSinceLoad = mutableSetOf<String>()
|
||||
|
||||
fun isTooBig(): Boolean {
|
||||
return logs.size > MAX_SAFE_LOGBOOK_SIZE
|
||||
|
||||
@@ -31,6 +31,20 @@ class LunaEvent: Comparable<LunaEvent> {
|
||||
const val TYPE_PUKE = "PUKE"
|
||||
const val TYPE_BATH = "BATH"
|
||||
const val TYPE_SLEEP = "SLEEP"
|
||||
|
||||
val BREASTFEEDING_TYPES = listOf(
|
||||
TYPE_BREASTFEEDING_LEFT_NIPPLE,
|
||||
TYPE_BREASTFEEDING_BOTH_NIPPLE,
|
||||
TYPE_BREASTFEEDING_RIGHT_NIPPLE
|
||||
)
|
||||
|
||||
fun findOngoing(events: List<LunaEvent>, type: String): LunaEvent? {
|
||||
return events.firstOrNull { it.ongoing && it.type == type }
|
||||
}
|
||||
|
||||
fun findOngoingBreastfeeding(events: List<LunaEvent>): LunaEvent? {
|
||||
return events.firstOrNull { it.ongoing && it.type in BREASTFEEDING_TYPES }
|
||||
}
|
||||
}
|
||||
|
||||
private val jo: JSONObject
|
||||
@@ -62,6 +76,14 @@ class LunaEvent: Comparable<LunaEvent> {
|
||||
if (value.isNotEmpty())
|
||||
jo.put("signature", value)
|
||||
}
|
||||
var ongoing: Boolean
|
||||
get() = jo.optInt("ongoing", 0) == 1
|
||||
set(value) {
|
||||
if (value)
|
||||
jo.put("ongoing", 1)
|
||||
else
|
||||
jo.remove("ongoing")
|
||||
}
|
||||
|
||||
constructor(jo: JSONObject) {
|
||||
this.jo = jo
|
||||
@@ -138,6 +160,29 @@ class LunaEvent: Comparable<LunaEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
fun fingerprint(): String {
|
||||
return "${time}_${type}_${quantity}_${notes}_${signature}_${ongoing}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the approximate start time for timer events.
|
||||
* For ongoing events: time IS the start time.
|
||||
* For finalized events: start = time - quantity * 60 (time=end, quantity=duration in min).
|
||||
*/
|
||||
fun approximateStartTime(): Long {
|
||||
return if (ongoing) time else time - quantity * 60L
|
||||
}
|
||||
|
||||
fun finalizeOngoing(endTimeMillis: Long) {
|
||||
if (!ongoing) return
|
||||
val startTimeSeconds = this.time
|
||||
val endTimeSeconds = endTimeMillis / 1000
|
||||
val durationMinutes = Math.max(1, ((endTimeSeconds - startTimeSeconds) / 60).toInt())
|
||||
this.time = endTimeSeconds
|
||||
this.quantity = durationMinutes
|
||||
this.ongoing = false
|
||||
}
|
||||
|
||||
fun toJson(): JSONObject {
|
||||
return jo
|
||||
}
|
||||
|
||||
@@ -133,21 +133,125 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
|
||||
}
|
||||
|
||||
private fun saveLogbook(context: Context, logbook: Logbook) {
|
||||
// Lock logbook on WebDAV to avoid concurrent changes
|
||||
//sardine.lock(getUrl())
|
||||
// Reload logbook from WebDAV
|
||||
// Merge logbooks (based on time)
|
||||
// Write logbook
|
||||
// Unlock logbook on WebDAV
|
||||
//sardine.unlock(getUrl())
|
||||
// Take a snapshot of local events (UI thread may modify logbook.logs concurrently)
|
||||
val localSnapshot = ArrayList(logbook.logs)
|
||||
val removedFingerprints = HashSet(logbook.removedSinceLoad)
|
||||
|
||||
// Load remote logbook and merge before saving to avoid overwriting
|
||||
// events added by other devices
|
||||
val mergedEvents = ArrayList(localSnapshot)
|
||||
try {
|
||||
val remoteLogbook = loadLogbook(logbook.name)
|
||||
val added = mergeRemoteEvents(mergedEvents, remoteLogbook, removedFingerprints)
|
||||
if (added > 0) {
|
||||
Log.d(TAG, "Merged $added remote events, saving ${mergedEvents.size} total")
|
||||
}
|
||||
// Merged events will appear in UI on next periodic load (max 60s)
|
||||
} catch (e: Exception) {
|
||||
// Remote not available (404, network error, etc.) - save local version as-is
|
||||
Log.w(TAG, "Could not load remote logbook for merge: $e")
|
||||
}
|
||||
|
||||
val ja = JSONArray()
|
||||
for (l in logbook.logs) {
|
||||
for (l in mergedEvents) {
|
||||
ja.put(l.toJson())
|
||||
}
|
||||
sardine.put(getUrl(logbook.name), ja.toString().toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges remote events into the local event list.
|
||||
* - Events already in local (by fingerprint) are skipped
|
||||
* - Events in removedSinceLoad are skipped (deliberately deleted/cancelled locally)
|
||||
* - Ongoing events that were finalized locally are skipped
|
||||
* - Duplicate finalized timer events (same type + similar start time) are skipped
|
||||
* @return number of events added
|
||||
*/
|
||||
private fun mergeRemoteEvents(
|
||||
localEvents: ArrayList<LunaEvent>,
|
||||
remote: Logbook,
|
||||
removedFingerprints: Set<String>
|
||||
): Int {
|
||||
val localFingerprints = localEvents.map { it.fingerprint() }.toHashSet()
|
||||
var added = 0
|
||||
|
||||
for (remoteEvent in remote.logs) {
|
||||
val fingerprint = remoteEvent.fingerprint()
|
||||
|
||||
// Already exists locally with exact same state
|
||||
if (localFingerprints.contains(fingerprint)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Was deliberately removed locally (cancel/delete)
|
||||
if (removedFingerprints.contains(fingerprint)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remote ongoing event was finalized locally
|
||||
if (remoteEvent.ongoing && hasMatchingTimerEvent(remoteEvent, localEvents)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remote finalized timer event that duplicates a local one
|
||||
// (both devices stopped the same timer → slightly different end times)
|
||||
if (!remoteEvent.ongoing && isDuplicateTimerEvent(remoteEvent, localEvents)) {
|
||||
continue
|
||||
}
|
||||
|
||||
localEvents.add(remoteEvent)
|
||||
localFingerprints.add(fingerprint)
|
||||
added++
|
||||
}
|
||||
|
||||
if (added > 0) {
|
||||
localEvents.sortWith(compareByDescending { it.time })
|
||||
}
|
||||
return added
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a local timer event (ongoing or finalized) with
|
||||
* approximately the same start time as the given event.
|
||||
*/
|
||||
private fun hasMatchingTimerEvent(event: LunaEvent, localEvents: List<LunaEvent>): Boolean {
|
||||
val startTime = event.approximateStartTime()
|
||||
for (local in localEvents) {
|
||||
if (local.type != event.type) continue
|
||||
if (Math.abs(startTime - local.approximateStartTime()) < 120) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a finalized timer event is a duplicate of an existing local event
|
||||
* (same timer stopped on two devices → slightly different end times).
|
||||
* Only applies to timer-type events (sleep, breastfeeding) with duration > 0.
|
||||
*/
|
||||
private fun isDuplicateTimerEvent(event: LunaEvent, localEvents: List<LunaEvent>): Boolean {
|
||||
if (event.quantity <= 0) return false
|
||||
val timerTypes = setOf(
|
||||
LunaEvent.TYPE_SLEEP,
|
||||
LunaEvent.TYPE_BREASTFEEDING_LEFT_NIPPLE,
|
||||
LunaEvent.TYPE_BREASTFEEDING_BOTH_NIPPLE,
|
||||
LunaEvent.TYPE_BREASTFEEDING_RIGHT_NIPPLE
|
||||
)
|
||||
if (event.type !in timerTypes) return false
|
||||
|
||||
val startTime = event.approximateStartTime()
|
||||
for (local in localEvents) {
|
||||
if (local.ongoing) continue
|
||||
if (local.type != event.type) continue
|
||||
if (local.quantity <= 0) continue
|
||||
if (Math.abs(startTime - local.approximateStartTime()) < 120) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to server and check if a logbook already exists.
|
||||
* If it does not exist, try to upload the local one.
|
||||
|
||||
@@ -79,14 +79,14 @@ class StatisticsCalculator(private val events: List<LunaEvent>) {
|
||||
}
|
||||
|
||||
private fun getEventsInRange(startUnix: Long, endUnix: Long): List<LunaEvent> {
|
||||
return events.filter { it.time >= startUnix && it.time < endUnix }
|
||||
return events.filter { it.time >= startUnix && it.time < endUnix && !it.ongoing }
|
||||
}
|
||||
|
||||
private fun getEventsForDays(days: Int): List<LunaEvent> {
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
val startOfToday = getStartOfDay(now)
|
||||
val startTime = startOfToday - (days - 1) * 24 * 60 * 60
|
||||
return events.filter { it.time >= startTime }
|
||||
return events.filter { it.time >= startTime && !it.ongoing }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<string name="breastfeeding_timer_stop">Stopp</string>
|
||||
<string name="breastfeeding_timer_hint">Tippe Stopp wenn fertig</string>
|
||||
<string name="breastfeeding_timer_already_running">Es läuft bereits eine Stillsitzung</string>
|
||||
<string name="another_timer_already_running">Es läuft bereits ein anderer Timer</string>
|
||||
<string name="breastfeeding_duration_title">Stilldauer</string>
|
||||
<string name="breastfeeding_duration_description">Dauer in Minuten eingeben</string>
|
||||
|
||||
@@ -236,6 +237,13 @@
|
||||
<string name="import_success">%d Ereignisse importiert</string>
|
||||
<string name="import_error">Import fehlgeschlagen</string>
|
||||
|
||||
<!-- Laufende Events (geräteübergreifende Timer-Synchronisierung) -->
|
||||
<string name="event_ongoing">läuft…</string>
|
||||
<string name="stale_timer_title">Abgelaufener Timer</string>
|
||||
<string name="stale_timer_message">Ein Timer läuft seit über 24 Stunden. Wurde er vergessen?</string>
|
||||
<string name="stale_timer_finalize">Mit aktueller Dauer speichern</string>
|
||||
<string name="stale_timer_delete">Löschen</string>
|
||||
|
||||
<!-- Logbook-Verwaltung -->
|
||||
<string name="edit_logbook_title">Logbook bearbeiten</string>
|
||||
<string name="edit_logbook_name_hint">Logbook-Name</string>
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<string name="breastfeeding_timer_stop">Arrêter</string>
|
||||
<string name="breastfeeding_timer_hint">Appuyez sur Arrêter quand terminé</string>
|
||||
<string name="breastfeeding_timer_already_running">Une session d\'allaitement est déjà en cours</string>
|
||||
<string name="another_timer_already_running">Un autre minuteur est déjà en cours</string>
|
||||
<string name="breastfeeding_duration_title">Durée d\'allaitement</string>
|
||||
<string name="breastfeeding_duration_description">Entrez la durée en minutes</string>
|
||||
|
||||
@@ -204,6 +205,13 @@
|
||||
<string name="import_success">%d événements importés</string>
|
||||
<string name="import_error">Échec de l\'import</string>
|
||||
|
||||
<!-- Événements en cours (synchronisation minuterie inter-appareils) -->
|
||||
<string name="event_ongoing">en cours…</string>
|
||||
<string name="stale_timer_title">Minuterie expirée</string>
|
||||
<string name="stale_timer_message">Une minuterie est active depuis plus de 24 heures. A-t-elle été oubliée ?</string>
|
||||
<string name="stale_timer_finalize">Enregistrer avec la durée actuelle</string>
|
||||
<string name="stale_timer_delete">Supprimer</string>
|
||||
|
||||
<!-- Gestion du journal -->
|
||||
<string name="edit_logbook_title">Modifier le journal</string>
|
||||
<string name="edit_logbook_name_hint">Nom du journal</string>
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<string name="breastfeeding_timer_stop">Stop</string>
|
||||
<string name="breastfeeding_timer_hint">Premi Stop quando hai finito</string>
|
||||
<string name="breastfeeding_timer_already_running">Una sessione di allattamento è già in corso</string>
|
||||
<string name="another_timer_already_running">Un altro timer è già in corso</string>
|
||||
<string name="breastfeeding_duration_title">Durata allattamento</string>
|
||||
<string name="breastfeeding_duration_description">Inserisci la durata in minuti</string>
|
||||
|
||||
@@ -204,6 +205,13 @@
|
||||
<string name="import_success">%d eventi importati</string>
|
||||
<string name="import_error">Importazione fallita</string>
|
||||
|
||||
<!-- Eventi in corso (sincronizzazione timer tra dispositivi) -->
|
||||
<string name="event_ongoing">in corso…</string>
|
||||
<string name="stale_timer_title">Timer scaduto</string>
|
||||
<string name="stale_timer_message">Un timer è attivo da oltre 24 ore. È stato dimenticato?</string>
|
||||
<string name="stale_timer_finalize">Salva con la durata attuale</string>
|
||||
<string name="stale_timer_delete">Elimina</string>
|
||||
|
||||
<!-- Gestione diario -->
|
||||
<string name="edit_logbook_title">Modifica diario</string>
|
||||
<string name="edit_logbook_name_hint">Nome del diario</string>
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
<string name="breastfeeding_timer_stop">Stop</string>
|
||||
<string name="breastfeeding_timer_hint">Tap Stop when finished</string>
|
||||
<string name="breastfeeding_timer_already_running">A breastfeeding session is already in progress</string>
|
||||
<string name="another_timer_already_running">Another timer is already running</string>
|
||||
<string name="breastfeeding_duration_title">Breastfeeding duration</string>
|
||||
<string name="breastfeeding_duration_description">Enter the duration in minutes</string>
|
||||
<string name="measurement_unit_time_minutes" translatable="false">min</string>
|
||||
@@ -262,6 +263,13 @@
|
||||
<string name="import_success">Imported %d events</string>
|
||||
<string name="import_error">Import failed</string>
|
||||
|
||||
<!-- Ongoing events (cross-device timer sync) -->
|
||||
<string name="event_ongoing">running…</string>
|
||||
<string name="stale_timer_title">Expired timer</string>
|
||||
<string name="stale_timer_message">A timer has been running for more than 24 hours. Was it forgotten?</string>
|
||||
<string name="stale_timer_finalize">Save with current duration</string>
|
||||
<string name="stale_timer_delete">Delete</string>
|
||||
|
||||
<!-- Logbook management -->
|
||||
<string name="edit_logbook_title">Edit Logbook</string>
|
||||
<string name="edit_logbook_name_hint">Logbook name</string>
|
||||
|
||||
Reference in New Issue
Block a user