Merge pull request 'fix(edit): scrollable, grouped, pinned-header calendar picker (#29)' (!56) from fix/calendar-picker-scrollable into release/v2.13.0
Reviewed-on: #56
This commit was merged in pull request #56.
This commit is contained in:
@@ -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 ->
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<CalendarSource>,
|
||||
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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user