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 dd39503..ca949c1 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 c3ef279..c60b91c 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 @@ -46,8 +46,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 @@ -72,7 +73,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, + ) } } 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) }, + ) + } + } + } }