From 0718afd5a62637c51b4f75d73d629fe6fe60fa00 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 23:13:27 +0200 Subject: [PATCH 1/2] fix(edit): make the new-event calendar picker a scrollable grouped list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The calendar picker was an AlertDialog holding a non-scrolling Column, so accounts with many calendars overflowed the dialog's fixed height and the entries past ~9 were unreachable (Codeberg #29). Replace it with the app's FullScreenPicker (collapsing scaffold + scrollable content), rendering calendars as connected GroupedRows grouped under their owning account — the same grouping the visibility filter and calendar manager use — with a colour chip per row and a check on the current selection. Co-Authored-By: Claude Opus 4.8 --- .../calendula/ui/edit/EventEditScreen.kt | 82 +++++++++++++------ 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt index 166c0fd..b23d04b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material.icons.automirrored.filled.Notes import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.CalendarMonth +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Contacts import androidx.compose.material.icons.filled.EventAvailable @@ -120,12 +121,15 @@ import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit import de.jeanlucmakiola.calendula.ui.common.calendarExpandEnter import de.jeanlucmakiola.calendula.ui.common.predictiveBack import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE +import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow import de.jeanlucmakiola.calendula.ui.common.DialogAmountField import de.jeanlucmakiola.calendula.ui.common.DialogUnitDropdown import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon import de.jeanlucmakiola.calendula.ui.common.eventFormFieldLabel +import de.jeanlucmakiola.calendula.ui.common.FullScreenPicker +import de.jeanlucmakiola.calendula.ui.common.GroupedRow import de.jeanlucmakiola.calendula.ui.common.MILLIS_PER_DAY import de.jeanlucmakiola.calendula.ui.common.InlineTextField import de.jeanlucmakiola.calendula.ui.common.OptionCard @@ -943,7 +947,7 @@ private fun EventEditContent( } if (showCalendarPicker) { - CalendarPickerDialog( + CalendarPicker( calendars = state.calendars, selectedId = form.calendarId, onSelect = { @@ -1931,34 +1935,64 @@ private fun ScheduleRow( } } +/** + * Full-screen calendar picker: a scrollable list of every writable calendar, + * grouped under its owning account like the visibility filter and calendar + * manager. Replaces the former fixed-height [AlertDialog], which clipped the + * list past ~9 rows so accounts with many calendars had unreachable entries + * (#29). Selecting a row calls [onSelect]; the caller closes the picker. + */ @Composable -private fun CalendarPickerDialog( +private fun CalendarPicker( calendars: List, selectedId: Long?, onSelect: (Long) -> Unit, onDismiss: () -> Unit, ) { - val dark = isSystemInDarkTheme() - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.event_detail_calendar)) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - calendars.forEach { calendar -> - OptionCard( - label = calendar.displayName, - onClick = { onSelect(calendar.id) }, - icon = Icons.Default.CalendarMonth, - // The calendar's own colour carries its identity. - iconTint = pastelize(calendar.color, dark), - supportingText = calendar.accountName.takeIf { it.isNotBlank() }, - selected = calendar.id == selectedId, - ) - } + // Group by owning account (name, else type, else the calendar's own name), + // preserving the provider's order within and across groups — the same + // grouping [groupByAccount] applies for the drawer filter. + val groups = remember(calendars) { + calendars + .groupBy { + it.accountName.takeIf(String::isNotBlank) + ?: it.accountType.takeIf(String::isNotBlank) + ?: it.displayName } - }, - confirmButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) } - }, - ) + .toList() + } + FullScreenPicker( + title = stringResource(R.string.event_detail_calendar), + onDismiss = onDismiss, + ) { + groups.forEach { (account, cals) -> + Text( + text = account, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 4.dp), + ) + cals.forEachIndexed { index, calendar -> + val isSelected = calendar.id == selectedId + GroupedRow( + title = calendar.displayName, + position = positionOf(index, cals.size), + selected = isSelected, + leading = { CalendarColorChip(calendar.color) }, + trailing = if (isSelected) { + { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + } else { + null + }, + onClick = { onSelect(calendar.id) }, + ) + } + } + } } -- 2.49.1 From 0a89ff9d0e8ccde91a9ac2a7376087e8296b0d07 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Jul 2026 15:22:18 +0200 Subject: [PATCH 2/2] fix(edit): pin the picker title instead of a large collapsing header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full-screen selection pickers opened with the LargeTopAppBar expanded, so the title started below the bar and only settled into it after a scroll — a tall empty header above a short option list. Give CollapsingScaffold a largeTopBar flag and have FullScreenPicker use a pinned single-line TopAppBar, so every picker (calendar, theme, week start, language, reminder, agenda range) shows its title in the bar from the start. Settings and the calendar manager keep the large collapsing header. Co-Authored-By: Claude Opus 4.8 --- .../calendula/ui/common/GroupedList.kt | 63 +++++++++++++------ .../calendula/ui/common/Picker.kt | 12 +++- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt index c183aab..de2a542 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt @@ -28,6 +28,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -57,10 +58,16 @@ fun positionOf(index: Int, count: Int): Position = when { } /** - * The app's standard full-screen list scaffold: a collapsing [LargeTopAppBar] - * whose title shrinks into the bar (next to the back button) as the content - * scrolls. Content is a scrollable column that feeds the toolbar via nested - * scroll. Used by Settings and the calendar manager so they share one shell. + * The app's standard full-screen list scaffold. By default a collapsing + * [LargeTopAppBar] whose title shrinks into the bar (next to the back button) as + * the content scrolls — used by Settings and the calendar manager, where the + * large header sets the page. Content is a scrollable column that feeds the + * toolbar via nested scroll. + * + * Set [largeTopBar] to false for a pinned, single-line [TopAppBar] instead: the + * title sits in the bar from the start, with no expanded header to scroll past. + * Preferred for selection pickers, where the tall header is just wasted space + * above the options. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -68,12 +75,16 @@ fun CollapsingScaffold( title: String, onBack: () -> Unit, modifier: Modifier = Modifier, + largeTopBar: Boolean = true, snackbarHost: @Composable () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, content: @Composable ColumnScope.() -> Unit, ) { - val scrollBehavior = + val scrollBehavior = if (largeTopBar) { TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) + } else { + TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + } Scaffold( modifier = modifier .predictiveBack(onBack = onBack) @@ -81,22 +92,34 @@ fun CollapsingScaffold( .background(MaterialTheme.colorScheme.surface) .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - LargeTopAppBar( - title = { Text(title) }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.settings_back), - ) - } - }, - actions = actions, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.topAppBarColors( - scrolledContainerColor = MaterialTheme.colorScheme.surface, - ), + val navigationIcon = @Composable { + IconButton(onClick = onBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.settings_back), + ) + } + } + val colors = TopAppBarDefaults.topAppBarColors( + scrolledContainerColor = MaterialTheme.colorScheme.surface, ) + if (largeTopBar) { + LargeTopAppBar( + title = { Text(title) }, + navigationIcon = navigationIcon, + actions = actions, + scrollBehavior = scrollBehavior, + colors = colors, + ) + } else { + TopAppBar( + title = { Text(title) }, + navigationIcon = navigationIcon, + actions = actions, + scrollBehavior = scrollBehavior, + colors = colors, + ) + } }, snackbarHost = snackbarHost, ) { innerPadding -> diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt index ce6abae..66b24df 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt @@ -43,8 +43,9 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange /** * Shared full-screen scaffold for selection pickers: a full-bleed [Dialog] that - * reuses the app's [CollapsingScaffold] (collapsing title + back button), so a - * picker is visually identical to a Settings sub-page and uses the full width. + * reuses the app's [CollapsingScaffold] (back button + full width), but with a + * pinned single-line title rather than the large collapsing header — a picker is + * a short list, so the tall header would only be empty space to scroll past. * [content] places the connected grouped rows; selecting one calls [onDismiss]. */ @Composable @@ -69,7 +70,12 @@ fun FullScreenPicker( (view.parent as? DialogWindowProvider)?.window ?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) } - CollapsingScaffold(title = title, onBack = onDismiss, content = content) + CollapsingScaffold( + title = title, + onBack = onDismiss, + largeTopBar = false, + content = content, + ) } } -- 2.49.1