3 Commits

Author SHA1 Message Date
cbd18cd891 Add configurable buttons, separate settings screens and backup activity
- Add ButtonConfigActivity for customizing main screen buttons with
  drag-and-drop reordering and individual size options (S/M/L)
- Move storage settings to separate StorageSettingsActivity
- Move signature setting to storage settings (relevant for WebDAV sync)
- Move data backup to separate BackupActivity with export/import
- Make "more" overflow button configurable in size
- Simplify SettingsActivity to 3 navigation buttons
- Add logbook rename/delete functionality
- Improve S/M/L button contrast with visible borders
2026-01-11 21:31:49 +01:00
b6110c2cbb Add sleep tracking, statistics module and backup features
Features:
- Sleep tracking with timer and manual duration input
- Statistics module with 5 tabs (daily summary, feeding, diapers, sleep, growth)
- Export/Import backup functionality in settings
- Complete German, French and Italian translations
2026-01-08 09:33:36 +01:00
dccc89a8e2 Add CLAUDE.md and .claude/ to gitignore 2026-01-08 09:33:04 +01:00
11 changed files with 29 additions and 403 deletions

2
.gitignore vendored
View File

@@ -107,3 +107,5 @@ app/release/output-metadata.json
# Other
app/src/main/java/it/danieleverducci/lunatracker/TemporaryHardcodedCredentials.kt
.kotlin/sessions/*
CLAUDE.md
.claude/

View File

@@ -329,38 +329,18 @@ class MainActivity : AppCompatActivity() {
}
fun startBreastfeedingTimer(eventType: String) {
// Check if timer already running locally
// Check if timer already running
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
}
// 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
// Save timer state
bfTimerStartTime = System.currentTimeMillis()
bfTimerType = eventType
saveBreastfeedingTimerState()
logbook?.logs?.add(0, event)
recyclerView.adapter?.notifyItemInserted(0)
saveLogbook()
// Show timer dialog
showBreastfeedingTimerDialog(eventType)
}
@@ -403,19 +383,13 @@ class MainActivity : AppCompatActivity() {
fun stopBreastfeedingTimer() {
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoingBreastfeeding(it) }
val durationMillis = System.currentTimeMillis() - bfTimerStartTime
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
val eventType = bfTimerType
clearBreastfeedingTimerState()
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())
if (eventType != null) {
logEvent(LunaEvent(eventType, durationMinutes))
}
}
@@ -423,23 +397,9 @@ 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)
@@ -483,36 +443,17 @@ class MainActivity : AppCompatActivity() {
// Sleep timer methods
fun startSleepTimer() {
// Check if timer already running locally
// Check if timer already running
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
}
// 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
// Save timer state
sleepTimerStartTime = System.currentTimeMillis()
saveSleepTimerState()
logbook?.logs?.add(0, event)
recyclerView.adapter?.notifyItemInserted(0)
saveLogbook()
// Show timer dialog
showSleepTimerDialog()
}
@@ -558,42 +499,20 @@ class MainActivity : AppCompatActivity() {
fun stopSleepTimer() {
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoing(it, LunaEvent.TYPE_SLEEP) }
val durationMillis = System.currentTimeMillis() - sleepTimerStartTime
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
clearSleepTimerState()
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))
}
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)
@@ -910,27 +829,6 @@ 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)
@@ -959,8 +857,6 @@ 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))
@@ -994,20 +890,12 @@ 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) { _, _ ->
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
}
event.quantity = picker.value
quantityTextView.text = NumericUtils(this@MainActivity).formatEventQuantity(event)
recyclerView.adapter?.notifyDataSetChanged()
saveLogbook()
@@ -1331,7 +1219,6 @@ 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()) {
@@ -1390,71 +1277,6 @@ 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)
@@ -1478,7 +1300,6 @@ class MainActivity : AppCompatActivity() {
// Update data
setLoading(true)
logbook?.removedSinceLoad?.add(event.fingerprint())
logbook?.logs?.remove(event)
recyclerView.adapter?.notifyDataSetChanged()
saveLogbook()

View File

@@ -59,21 +59,6 @@ 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)

View File

@@ -5,7 +5,6 @@ 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

View File

@@ -31,20 +31,6 @@ 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
@@ -76,14 +62,6 @@ 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
@@ -160,29 +138,6 @@ 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
}

View File

@@ -133,125 +133,21 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p
}
private fun saveLogbook(context: Context, logbook: Logbook) {
// 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")
}
// 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())
val ja = JSONArray()
for (l in mergedEvents) {
for (l in logbook.logs) {
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.

View File

@@ -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 && !it.ongoing }
return events.filter { it.time >= startUnix && it.time < endUnix }
}
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 && !it.ongoing }
return events.filter { it.time >= startTime }
}
/**

View File

@@ -104,7 +104,6 @@
<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>
@@ -237,13 +236,6 @@
<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>

View File

@@ -104,7 +104,6 @@
<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>
@@ -205,13 +204,6 @@
<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>

View File

@@ -104,7 +104,6 @@
<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>
@@ -205,13 +204,6 @@
<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>

View File

@@ -162,7 +162,6 @@
<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>
@@ -263,13 +262,6 @@
<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>