Add cross-device timer sync via WebDAV
When a sleep or breastfeeding timer is started, an "ongoing" event is immediately saved to the logbook and synced via WebDAV. Other devices detect this event on sync and can display/stop the timer. This allows partners to stop timers started on another device. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -329,18 +329,34 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startBreastfeedingTimer(eventType: String) {
|
fun startBreastfeedingTimer(eventType: String) {
|
||||||
// Check if timer already running
|
// Check if timer already running locally
|
||||||
if (bfTimerType != null) {
|
if (bfTimerType != null) {
|
||||||
Toast.makeText(this, R.string.breastfeeding_timer_already_running, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.breastfeeding_timer_already_running, Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save timer state
|
// Check logbook for existing ongoing breastfeeding event (from another device)
|
||||||
bfTimerStartTime = System.currentTimeMillis()
|
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
|
bfTimerType = eventType
|
||||||
saveBreastfeedingTimerState()
|
saveBreastfeedingTimerState()
|
||||||
|
|
||||||
// Show timer dialog
|
logbook?.logs?.add(0, event)
|
||||||
|
recyclerView.adapter?.notifyItemInserted(0)
|
||||||
|
saveLogbook()
|
||||||
|
|
||||||
showBreastfeedingTimerDialog(eventType)
|
showBreastfeedingTimerDialog(eventType)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,13 +399,19 @@ class MainActivity : AppCompatActivity() {
|
|||||||
fun stopBreastfeedingTimer() {
|
fun stopBreastfeedingTimer() {
|
||||||
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
|
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
|
||||||
|
|
||||||
val durationMillis = System.currentTimeMillis() - bfTimerStartTime
|
val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoingBreastfeeding(it) }
|
||||||
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
|
|
||||||
|
|
||||||
val eventType = bfTimerType
|
val eventType = bfTimerType
|
||||||
clearBreastfeedingTimerState()
|
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))
|
logEvent(LunaEvent(eventType, durationMinutes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,6 +419,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
fun cancelBreastfeedingTimer() {
|
fun cancelBreastfeedingTimer() {
|
||||||
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
|
bfTimerHandler?.removeCallbacks(bfTimerRunnable!!)
|
||||||
clearBreastfeedingTimerState()
|
clearBreastfeedingTimerState()
|
||||||
|
|
||||||
|
// Remove ongoing event from logbook
|
||||||
|
val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoingBreastfeeding(it) }
|
||||||
|
if (ongoingEvent != null) {
|
||||||
|
logbook?.logs?.remove(ongoingEvent)
|
||||||
|
recyclerView.adapter?.notifyDataSetChanged()
|
||||||
|
saveLogbook()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun askBreastfeedingDuration(eventType: String) {
|
fun askBreastfeedingDuration(eventType: String) {
|
||||||
@@ -443,17 +473,32 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// Sleep timer methods
|
// Sleep timer methods
|
||||||
fun startSleepTimer() {
|
fun startSleepTimer() {
|
||||||
// Check if timer already running
|
// Check if timer already running locally
|
||||||
if (sleepTimerStartTime > 0) {
|
if (sleepTimerStartTime > 0) {
|
||||||
Toast.makeText(this, R.string.sleep_timer_already_running, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.sleep_timer_already_running, Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save timer state
|
// Check logbook for existing ongoing sleep event (from another device)
|
||||||
sleepTimerStartTime = System.currentTimeMillis()
|
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()
|
saveSleepTimerState()
|
||||||
|
|
||||||
// Show timer dialog
|
logbook?.logs?.add(0, event)
|
||||||
|
recyclerView.adapter?.notifyItemInserted(0)
|
||||||
|
saveLogbook()
|
||||||
|
|
||||||
showSleepTimerDialog()
|
showSleepTimerDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,17 +544,33 @@ class MainActivity : AppCompatActivity() {
|
|||||||
fun stopSleepTimer() {
|
fun stopSleepTimer() {
|
||||||
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
|
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
|
||||||
|
|
||||||
val durationMillis = System.currentTimeMillis() - sleepTimerStartTime
|
val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoing(it, LunaEvent.TYPE_SLEEP) }
|
||||||
val durationMinutes = Math.max(1, (durationMillis / 60000).toInt()) // Minimum 1 minute
|
|
||||||
|
|
||||||
clearSleepTimerState()
|
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() {
|
fun cancelSleepTimer() {
|
||||||
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
|
sleepTimerHandler?.removeCallbacks(sleepTimerRunnable!!)
|
||||||
clearSleepTimerState()
|
clearSleepTimerState()
|
||||||
|
|
||||||
|
// Remove ongoing event from logbook
|
||||||
|
val ongoingEvent = logbook?.logs?.let { LunaEvent.findOngoing(it, LunaEvent.TYPE_SLEEP) }
|
||||||
|
if (ongoingEvent != null) {
|
||||||
|
logbook?.logs?.remove(ongoingEvent)
|
||||||
|
recyclerView.adapter?.notifyDataSetChanged()
|
||||||
|
saveLogbook()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun askSleepDuration() {
|
fun askSleepDuration() {
|
||||||
@@ -829,6 +890,25 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showEventDetailDialog(event: LunaEvent, items: ArrayList<LunaEvent>) {
|
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 (bfTimerType == null) {
|
||||||
|
bfTimerStartTime = event.time * 1000
|
||||||
|
bfTimerType = event.type
|
||||||
|
saveBreastfeedingTimerState()
|
||||||
|
}
|
||||||
|
showBreastfeedingTimerDialog(event.type)
|
||||||
|
} else if (event.type == LunaEvent.TYPE_SLEEP) {
|
||||||
|
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
|
// Do not update list while the detail is shown, to avoid changing the object below while it is changed by the user
|
||||||
pauseLogbookUpdate = true
|
pauseLogbookUpdate = true
|
||||||
val d = AlertDialog.Builder(this)
|
val d = AlertDialog.Builder(this)
|
||||||
@@ -1225,6 +1305,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
findViewById<View>(R.id.no_connection_screen).visibility = View.GONE
|
findViewById<View>(R.id.no_connection_screen).visibility = View.GONE
|
||||||
logbook = lb
|
logbook = lb
|
||||||
showLogbook()
|
showLogbook()
|
||||||
|
checkForOngoingEvents()
|
||||||
|
|
||||||
if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) {
|
if (DEBUG_CHECK_LOGBOOK_CONSISTENCY) {
|
||||||
for (e in logbook?.logs ?: listOf()) {
|
for (e in logbook?.logs ?: listOf()) {
|
||||||
@@ -1283,6 +1364,59 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for ongoing breastfeeding from another device
|
||||||
|
if (bfTimerType == null) {
|
||||||
|
val ongoingBf = LunaEvent.findOngoingBreastfeeding(logs)
|
||||||
|
if (ongoingBf != null) {
|
||||||
|
bfTimerStartTime = ongoingBf.time * 1000
|
||||||
|
bfTimerType = ongoingBf.type
|
||||||
|
saveBreastfeedingTimerState()
|
||||||
|
showBreastfeedingTimerDialog(ongoingBf.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for ongoing sleep from another device
|
||||||
|
if (sleepTimerStartTime == 0L) {
|
||||||
|
val ongoingSleep = LunaEvent.findOngoing(logs, LunaEvent.TYPE_SLEEP)
|
||||||
|
if (ongoingSleep != null) {
|
||||||
|
sleepTimerStartTime = ongoingSleep.time * 1000
|
||||||
|
saveSleepTimerState()
|
||||||
|
showSleepTimerDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.logs?.remove(event)
|
||||||
|
recyclerView.adapter?.notifyDataSetChanged()
|
||||||
|
saveLogbook()
|
||||||
|
}
|
||||||
|
d.create().show()
|
||||||
|
}
|
||||||
|
|
||||||
fun logEvent(event: LunaEvent) {
|
fun logEvent(event: LunaEvent) {
|
||||||
savingEvent(true)
|
savingEvent(true)
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,21 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
|
|||||||
LunaEvent.TYPE_CUSTOM -> item.notes
|
LunaEvent.TYPE_CUSTOM -> item.notes
|
||||||
else -> item.getTypeDescription(context)
|
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)
|
holder.time.text = DateUtils.formatTimeAgo(context, item.time)
|
||||||
var quantityText = numericUtils.formatEventQuantity(item)
|
var quantityText = numericUtils.formatEventQuantity(item)
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ class LunaEvent: Comparable<LunaEvent> {
|
|||||||
const val TYPE_PUKE = "PUKE"
|
const val TYPE_PUKE = "PUKE"
|
||||||
const val TYPE_BATH = "BATH"
|
const val TYPE_BATH = "BATH"
|
||||||
const val TYPE_SLEEP = "SLEEP"
|
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
|
private val jo: JSONObject
|
||||||
@@ -62,6 +76,14 @@ class LunaEvent: Comparable<LunaEvent> {
|
|||||||
if (value.isNotEmpty())
|
if (value.isNotEmpty())
|
||||||
jo.put("signature", value)
|
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) {
|
constructor(jo: JSONObject) {
|
||||||
this.jo = jo
|
this.jo = jo
|
||||||
@@ -138,6 +160,16 @@ class LunaEvent: Comparable<LunaEvent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
fun toJson(): JSONObject {
|
||||||
return jo
|
return jo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,14 +79,14 @@ class StatisticsCalculator(private val events: List<LunaEvent>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getEventsInRange(startUnix: Long, endUnix: Long): 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> {
|
private fun getEventsForDays(days: Int): List<LunaEvent> {
|
||||||
val now = System.currentTimeMillis() / 1000
|
val now = System.currentTimeMillis() / 1000
|
||||||
val startOfToday = getStartOfDay(now)
|
val startOfToday = getStartOfDay(now)
|
||||||
val startTime = startOfToday - (days - 1) * 24 * 60 * 60
|
val startTime = startOfToday - (days - 1) * 24 * 60 * 60
|
||||||
return events.filter { it.time >= startTime }
|
return events.filter { it.time >= startTime && !it.ongoing }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -236,6 +236,13 @@
|
|||||||
<string name="import_success">%d Ereignisse importiert</string>
|
<string name="import_success">%d Ereignisse importiert</string>
|
||||||
<string name="import_error">Import fehlgeschlagen</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 -->
|
<!-- Logbook-Verwaltung -->
|
||||||
<string name="edit_logbook_title">Logbook bearbeiten</string>
|
<string name="edit_logbook_title">Logbook bearbeiten</string>
|
||||||
<string name="edit_logbook_name_hint">Logbook-Name</string>
|
<string name="edit_logbook_name_hint">Logbook-Name</string>
|
||||||
|
|||||||
@@ -204,6 +204,13 @@
|
|||||||
<string name="import_success">%d événements importés</string>
|
<string name="import_success">%d événements importés</string>
|
||||||
<string name="import_error">Échec de l\'import</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 -->
|
<!-- Gestion du journal -->
|
||||||
<string name="edit_logbook_title">Modifier le journal</string>
|
<string name="edit_logbook_title">Modifier le journal</string>
|
||||||
<string name="edit_logbook_name_hint">Nom du journal</string>
|
<string name="edit_logbook_name_hint">Nom du journal</string>
|
||||||
|
|||||||
@@ -204,6 +204,13 @@
|
|||||||
<string name="import_success">%d eventi importati</string>
|
<string name="import_success">%d eventi importati</string>
|
||||||
<string name="import_error">Importazione fallita</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 -->
|
<!-- Gestione diario -->
|
||||||
<string name="edit_logbook_title">Modifica diario</string>
|
<string name="edit_logbook_title">Modifica diario</string>
|
||||||
<string name="edit_logbook_name_hint">Nome del diario</string>
|
<string name="edit_logbook_name_hint">Nome del diario</string>
|
||||||
|
|||||||
@@ -262,6 +262,13 @@
|
|||||||
<string name="import_success">Imported %d events</string>
|
<string name="import_success">Imported %d events</string>
|
||||||
<string name="import_error">Import failed</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 -->
|
<!-- Logbook management -->
|
||||||
<string name="edit_logbook_title">Edit Logbook</string>
|
<string name="edit_logbook_title">Edit Logbook</string>
|
||||||
<string name="edit_logbook_name_hint">Logbook name</string>
|
<string name="edit_logbook_name_hint">Logbook name</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user