diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt index 45e5f03..b512f9d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt @@ -374,6 +374,7 @@ private fun AgendaList( ) { index, event -> AgendaEventRow( event = event, + day = day.date, position = positionOf(index, day.events.size), dimmed = dimPast && event.hasEnded(now), modifier = animateItemMotion(), @@ -452,6 +453,7 @@ private fun AgendaEmptyDayRow(onClick: () -> Unit) { @Composable private fun AgendaEventRow( event: EventInstance, + day: LocalDate, position: Position, dimmed: Boolean, modifier: Modifier = Modifier, @@ -463,7 +465,7 @@ private fun AgendaEventRow( GroupedRow( modifier = if (dimmed) modifier.alpha(EventDimAlpha) else modifier, title = title, - summary = agendaTimeSummary(event), + summary = agendaTimeSummary(event, day), position = position, minHeight = 64.dp, leading = { @@ -557,16 +559,41 @@ private fun agendaDayLabel(date: LocalDate, today: LocalDate): String { return if (relative != null) "$relative · $formatted" else formatted } -/** Time line under the title: "09:00 – 10:00 · Location", "All day", etc. */ +/** + * Time line under the title: "09:00 – 10:00 · Location", "All day", etc. + * + * A multi-day event shows only the part relevant to [day], with a "→" marking + * that it carries past this day's boundary: its first day shows the start + * ("14:00 →"), its last day the end ("→ 10:00"), and any whole day in between + * reads as an all-day span arriving from and continuing into its neighbours + * ("→ All day →"). + */ @Composable -private fun agendaTimeSummary(event: EventInstance): String { - val time = if (event.isAllDay) { - stringResource(R.string.event_detail_all_day) +private fun agendaTimeSummary(event: EventInstance, day: LocalDate): String { + val allDayLabel = stringResource(R.string.event_detail_all_day) + val is24Hour = LocalUse24HourFormat.current + val locale = currentLocale() + + val time = if (event.spansMultipleDays(zone)) { + val continuesBefore = day > event.spanFirstDay(zone) + val continuesAfter = day < event.spanLastDay(zone) + val core = when { + event.isAllDay -> allDayLabel + !continuesBefore -> formatTime(event.start, is24Hour, locale) // the first day + !continuesAfter -> formatTime(event.end, is24Hour, locale) // the last day + else -> allDayLabel // a full middle day + } + buildString { + if (continuesBefore) append("→ ") + append(core) + if (continuesAfter) append(" →") + } + } else if (event.isAllDay) { + allDayLabel } else { - val is24Hour = LocalUse24HourFormat.current - val locale = currentLocale() "${formatTime(event.start, is24Hour, locale)} – ${formatTime(event.end, is24Hour, locale)}" } + val location = event.location?.takeIf { it.isNotBlank() } return if (location != null) "$time · $location" else time } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaUiState.kt index 6419101..33438b7 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaUiState.kt @@ -9,6 +9,24 @@ import kotlinx.datetime.plus import kotlinx.datetime.toLocalDateTime import kotlin.time.Duration.Companion.milliseconds +/** The first calendar day this event occupies, in [zone]. */ +fun EventInstance.spanFirstDay(zone: TimeZone): LocalDate = + start.toLocalDateTime(zone).date + +/** + * The last calendar day this event actually occupies, in [zone]. An event ending + * exactly at midnight (all-day events end at the exclusive next-midnight) does + * not reach into that boundary day, so resolve the instant just before [end]. + */ +fun EventInstance.spanLastDay(zone: TimeZone): LocalDate { + val lastInstant = if (end > start) end - 1.milliseconds else start + return lastInstant.toLocalDateTime(zone).date +} + +/** Whether this event occupies more than one calendar day in [zone]. */ +fun EventInstance.spansMultipleDays(zone: TimeZone): Boolean = + spanFirstDay(zone) != spanLastDay(zone) + /** One calendar day with at least one event, for the agenda list. */ data class AgendaDay( val date: LocalDate, @@ -35,12 +53,8 @@ fun groupAgendaDays( ): List { val byDay = sortedMapOf>() for (instance in instances) { - val firstDay = instance.start.toLocalDateTime(zone).date.coerceAtLeast(anchor) - // The last day the event actually occupies. An event ending exactly at - // midnight (all-day events end at the exclusive next-midnight) does not - // reach into that boundary day, so resolve the instant just before [end]. - val lastInstant = if (instance.end > instance.start) instance.end - 1.milliseconds else instance.start - val lastDay = lastInstant.toLocalDateTime(zone).date.coerceAtMost(windowEnd) + val firstDay = instance.spanFirstDay(zone).coerceAtLeast(anchor) + val lastDay = instance.spanLastDay(zone).coerceAtMost(windowEnd) var day = firstDay while (day <= lastDay) { byDay.getOrPut(day) { mutableListOf() }.add(instance)