From 5e397d88821630d07ef8b666df17232dfff2bbf2 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 9 Oct 2026 13:30:04 +0200 Subject: [PATCH] Show the selection in a contextual top bar (#141) --- .../calendula/ui/common/CalendarChrome.kt | 47 +++++++++- .../calendula/ui/common/SelectionTopBar.kt | 90 +++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/common/SelectionTopBar.kt diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarChrome.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarChrome.kt index 37e2282b..5cd603af 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarChrome.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarChrome.kt @@ -1,6 +1,10 @@ package de.jeanlucmakiola.calendula.ui.common +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn @@ -134,8 +138,10 @@ fun PublishSelection(event: EventInstance?, busy: Boolean, compact: Boolean, onD } } -/** The top bar every calendar screen shares. */ -@OptIn(ExperimentalMaterial3Api::class) +/** + * The top bar every calendar screen shares — or, while an event is selected, + * the selection's own bar in its place (#141). + */ @Composable fun CalendarTopBar( host: ChromeHost, @@ -145,6 +151,43 @@ fun CalendarTopBar( onOpenDrawer: () -> Unit, onOpenSearch: () -> Unit, showTodayButton: Boolean, +) { + val effects = MaterialTheme.motionScheme.fastEffectsSpec() + AnimatedContent( + targetState = host.selection, + contentKey = { it != null }, + transitionSpec = { + (fadeIn(effects) + slideInVertically { -it / 4 }) + .togetherWith(fadeOut(effects) + slideOutVertically { -it / 4 }) + }, + label = "top-bar", + ) { selection -> + if (selection != null) { + SelectionTopBar(selection) + } else { + ViewTopBar( + host = host, + selectedView = selectedView, + quickSwitchViews = quickSwitchViews, + onCycleView = onCycleView, + onOpenDrawer = onOpenDrawer, + onOpenSearch = onOpenSearch, + showTodayButton = showTodayButton, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ViewTopBar( + host: ChromeHost, + selectedView: CalendarView, + quickSwitchViews: List, + onCycleView: () -> Unit, + onOpenDrawer: () -> Unit, + onOpenSearch: () -> Unit, + showTodayButton: Boolean, ) { val chrome = host.chrome TopAppBar( diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/SelectionTopBar.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/SelectionTopBar.kt new file mode 100644 index 00000000..de144e90 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/SelectionTopBar.kt @@ -0,0 +1,90 @@ +package de.jeanlucmakiola.calendula.ui.common + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import de.jeanlucmakiola.calendula.R +import de.jeanlucmakiola.calendula.domain.EventInstance +import de.jeanlucmakiola.floret.locale.currentLocale +import de.jeanlucmakiola.floret.locale.localizedDateFormatter +import java.time.ZoneId +import java.util.Locale +import kotlin.time.toJavaInstant + +/** + * The top bar while an event is selected (#141): says which, and when, with a + * way out — the selection otherwise only shows on the grid and in the FAB. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SelectionTopBar(selection: EventSelection) { + val event = selection.event + val use24Hour = LocalUse24HourFormat.current + val locale = currentLocale() + val whenText = remember(event.start, event.end, use24Hour, locale) { + formatSelectionWhen(event, use24Hour, locale) + } + TopAppBar( + navigationIcon = { + IconButton(onClick = selection.dismiss) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.event_selection_close), + ) + } + }, + title = { + Column { + Text( + text = event.title.ifBlank { stringResource(R.string.event_untitled) }, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = whenText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + scrolledContainerColor = MaterialTheme.colorScheme.secondaryContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onSecondaryContainer, + titleContentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ), + ) +} + +/** + * "Fri, 9 Oct · 09:00–10:00", or with both days named for an event that runs + * into another: "Fri, 9 Oct, 22:00 – Sat, 10 Oct, 02:00". + */ +private fun formatSelectionWhen(event: EventInstance, use24Hour: Boolean, locale: Locale): String { + val zone = ZoneId.systemDefault() + val start = event.start.toJavaInstant().atZone(zone) + val end = event.end.toJavaInstant().atZone(zone) + val day = localizedDateFormatter(locale, "EEEdMMM") + val time = localizedDateFormatter(locale, if (use24Hour) "Hm" else "hm") + val endsSameDay = end.toLocalDate() == start.toLocalDate() || + (end.toLocalTime().toSecondOfDay() == 0 && end.toLocalDate() == start.toLocalDate().plusDays(1)) + return if (endsSameDay) { + "${day.format(start)} · ${time.format(start)}–${time.format(end)}" + } else { + "${day.format(start)}, ${time.format(start)} – ${day.format(end)}, ${time.format(end)}" + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 27b0d1ee..dfe552fc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -101,6 +101,7 @@ Change recurring event Event actions Hide event actions + Deselect event Couldn\'t open the event Undo Move undone