From 2b9d8a2d3c84d82e99c0e463cdd37ff8bfe2ea52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milosch=20F=C3=BCllgraf?= Date: Thu, 7 Aug 2025 21:25:55 +0200 Subject: [PATCH] Add support for syncing estimated and elapsed fields using CalDAV (#3780) --- .../main/java/org/tasks/caldav/iCalendar.kt | 37 +++++++++++++++++++ .../java/org/tasks/caldav/iCalendarMerge.kt | 16 ++++++++ 2 files changed, 53 insertions(+) diff --git a/app/src/main/java/org/tasks/caldav/iCalendar.kt b/app/src/main/java/org/tasks/caldav/iCalendar.kt index a98c3148a..654ce9ea7 100644 --- a/app/src/main/java/org/tasks/caldav/iCalendar.kt +++ b/app/src/main/java/org/tasks/caldav/iCalendar.kt @@ -296,11 +296,16 @@ class iCalendar @Inject constructor( it === RelType.PARENT || it == null || it.value.isNullOrBlank() } } + // custom X- properties to sync internal fields + private const val TASKS_ESTIMATED_SECONDS = "X-TASKS-ESTIMATED-SECONDS" + private const val TASKS_ELAPSED_SECONDS = "X-TASKS-ELAPSED-SECONDS" internal val IS_APPLE_SORT_ORDER = { x: Property? -> x?.name.equals(APPLE_SORT_ORDER, true) } private val IS_OC_HIDESUBTASKS = { x: Property? -> x?.name.equals(OC_HIDESUBTASKS, true) } private val IS_MOZ_SNOOZE_TIME = { x: Property? -> x?.name.equals(MOZ_SNOOZE_TIME, true) } private val IS_MOZ_LASTACK = { x: Property? -> x?.name.equals(MOZ_LASTACK, true) } + private val IS_TASKS_ESTIMATED_SECONDS = { x: Property? -> x?.name.equals(TASKS_ESTIMATED_SECONDS, true) } + private val IS_TASKS_ELAPSED_SECONDS = { x: Property? -> x?.name.equals(TASKS_ELAPSED_SECONDS, true) } fun Due?.apply(task: org.tasks.data.entity.Task) { task.dueDate = toMillis() @@ -442,6 +447,36 @@ class iCalendar @Inject constructor( ?: unknownProperties.removeIf(IS_MOZ_SNOOZE_TIME) } + var Task.estimatedSeconds: Int? + get() = unknownProperties.find(IS_TASKS_ESTIMATED_SECONDS)?.value?.toInt() + set(value) { + value + ?.takeIf { it != 0 } + ?.let { dur -> + unknownProperties.find(IS_TASKS_ESTIMATED_SECONDS) + ?.let { it.value = dur.toString() } + ?: unknownProperties.add( + XProperty(TASKS_ESTIMATED_SECONDS, dur.toString()) + ) + } + ?: unknownProperties.removeIf(IS_TASKS_ESTIMATED_SECONDS) + } + + var Task.elapsedSeconds: Int? + get() = unknownProperties.find(IS_TASKS_ELAPSED_SECONDS)?.value?.toInt() + set(value) { + value + ?.takeIf { it != 0 } + ?.let { dur -> + unknownProperties.find(IS_TASKS_ELAPSED_SECONDS) + ?.let { it.value = dur.toString() } + ?: unknownProperties.add( + XProperty(TASKS_ELAPSED_SECONDS, dur.toString()) + ) + } + ?: unknownProperties.removeIf(IS_TASKS_ELAPSED_SECONDS) + } + fun Task.applyLocal(caldavTask: CaldavTask, task: org.tasks.data.entity.Task) { createdAt = newDateTime(task.creationDate).toUTC().millis summary = task.title @@ -494,6 +529,8 @@ class iCalendar @Inject constructor( parent = if (task.parent == 0L) null else caldavTask.remoteParent order = task.order collapsed = task.isCollapsed + estimatedSeconds = task.estimatedSeconds + elapsedSeconds = task.elapsedSeconds } val List.filtered: List diff --git a/app/src/main/java/org/tasks/caldav/iCalendarMerge.kt b/app/src/main/java/org/tasks/caldav/iCalendarMerge.kt index a9070be91..64d8571f9 100644 --- a/app/src/main/java/org/tasks/caldav/iCalendarMerge.kt +++ b/app/src/main/java/org/tasks/caldav/iCalendarMerge.kt @@ -3,6 +3,8 @@ package org.tasks.caldav import at.bitfire.ical4android.Task import net.fortuna.ical4j.model.property.Status import org.tasks.caldav.iCalendar.Companion.collapsed +import org.tasks.caldav.iCalendar.Companion.elapsedSeconds +import org.tasks.caldav.iCalendar.Companion.estimatedSeconds import org.tasks.caldav.iCalendar.Companion.getLocal import org.tasks.caldav.iCalendar.Companion.order import org.tasks.caldav.iCalendar.Companion.parent @@ -32,6 +34,8 @@ fun org.tasks.data.entity.Task.applyRemote( applyStart(remote, local) applyCollapsed(remote, local) applyOrder(remote, local) + applyEstimatedSeconds(remote, local) + applyElapsedSeconds(remote, local) return this } @@ -121,6 +125,18 @@ private fun CaldavTask.applyParent(remote: Task, local: Task?) { } } +private fun org.tasks.data.entity.Task.applyEstimatedSeconds(remote: Task, local: Task?) { + if (local == null || local.estimatedSeconds == estimatedSeconds) { + estimatedSeconds = remote.estimatedSeconds ?: 0 + } +} + +private fun org.tasks.data.entity.Task.applyElapsedSeconds(remote: Task, local: Task?) { + if (local == null || local.elapsedSeconds == elapsedSeconds) { + elapsedSeconds = remote.elapsedSeconds ?: 0 + } +} + private val Task.tasksPriority: Int get() = when (this.priority) { // https://tools.ietf.org/html/rfc5545#section-3.8.1.9