Fold the selection bar while a chip shows and match it to the FAB (#141)
This commit is contained in:
@@ -391,6 +391,8 @@ fun CalendarHost(
|
|||||||
val reschedule: RescheduleViewModel = hiltViewModel()
|
val reschedule: RescheduleViewModel = hiltViewModel()
|
||||||
val movableCalendarIds by reschedule.movableCalendarIds.collectAsStateWithLifecycle()
|
val movableCalendarIds by reschedule.movableCalendarIds.collectAsStateWithLifecycle()
|
||||||
val eventActions: EventActionsViewModel = hiltViewModel()
|
val eventActions: EventActionsViewModel = hiltViewModel()
|
||||||
|
val moveOutcome by reschedule.outcome.collectAsStateWithLifecycle()
|
||||||
|
val selectionFailure by eventActions.failure.collectAsStateWithLifecycle()
|
||||||
val onEditEvent: (EventInstance) -> Unit = { event ->
|
val onEditEvent: (EventInstance) -> Unit = { event ->
|
||||||
val key = longArrayOf(
|
val key = longArrayOf(
|
||||||
event.eventId,
|
event.eventId,
|
||||||
@@ -543,6 +545,11 @@ fun CalendarHost(
|
|||||||
importFormSource = ImportSource.Insert
|
importFormSource = ImportSource.Insert
|
||||||
importForm = form
|
importForm = form
|
||||||
},
|
},
|
||||||
|
chipShown = moveOutcome != null || selectionFailure != null,
|
||||||
|
onDismissChip = {
|
||||||
|
reschedule.consumeOutcome()
|
||||||
|
eventActions.consumeFailure()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
CalendarFab(chromeHost, todayInToolbar, fabSelection)
|
CalendarFab(chromeHost, todayInToolbar, fabSelection)
|
||||||
},
|
},
|
||||||
@@ -600,12 +607,10 @@ fun CalendarHost(
|
|||||||
|
|
||||||
// Scope prompt + confirmation/undo snackbar for a dropped event, declared
|
// Scope prompt + confirmation/undo snackbar for a dropped event, declared
|
||||||
// right after the calendar views so later overlays cover it.
|
// right after the calendar views so later overlays cover it.
|
||||||
val actionFailure by eventActions.failure.collectAsStateWithLifecycle()
|
|
||||||
EventMoveHost(
|
EventMoveHost(
|
||||||
reschedule,
|
reschedule,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
raised = chromeHost.selectionBarShown && chromeHost.selectionBarExpanded,
|
actionFailure = eventActionFailureMessage(selectionFailure),
|
||||||
actionFailure = eventActionFailureMessage(actionFailure),
|
|
||||||
onActionFailureShown = eventActions::consumeFailure,
|
onActionFailureShown = eventActions::consumeFailure,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -90,10 +90,6 @@ class ChromeHost {
|
|||||||
var selectionCompact by mutableStateOf(false)
|
var selectionCompact by mutableStateOf(false)
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
/** Whether the selection's bar is unfolded, rather than folded into its button. */
|
|
||||||
var selectionBarExpanded by mutableStateOf(false)
|
|
||||||
internal set
|
|
||||||
|
|
||||||
/** Whether the selection's action bar is showing — it steps aside mid-gesture. */
|
/** Whether the selection's action bar is showing — it steps aside mid-gesture. */
|
||||||
val selectionBarShown: Boolean get() = selection != null && !selectionBusy
|
val selectionBarShown: Boolean get() = selection != null && !selectionBusy
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
@@ -90,8 +89,6 @@ fun CalendarFabColumn(
|
|||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = selection?.expanded == true,
|
visible = selection?.expanded == true,
|
||||||
// Overflows the row evenly above and below, centred on the FAB.
|
|
||||||
modifier = Modifier.wrapContentHeight(unbounded = true),
|
|
||||||
enter = expandHorizontally(
|
enter = expandHorizontally(
|
||||||
animationSpec = MaterialTheme.motionScheme.fastSpatialSpec(),
|
animationSpec = MaterialTheme.motionScheme.fastSpatialSpec(),
|
||||||
expandFrom = Alignment.End,
|
expandFrom = Alignment.End,
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ private val FAB_BAND = 88.dp
|
|||||||
fun EventMoveHost(
|
fun EventMoveHost(
|
||||||
viewModel: RescheduleViewModel,
|
viewModel: RescheduleViewModel,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
raised: Boolean = false,
|
|
||||||
actionFailure: String? = null,
|
actionFailure: String? = null,
|
||||||
onActionFailureShown: () -> Unit = {},
|
onActionFailureShown: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
@@ -126,11 +125,7 @@ fun EventMoveHost(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomStart)
|
.align(Alignment.BottomStart)
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
// Above the selection bar while it is up (#141).
|
.padding(start = SnackChipMargin, bottom = SnackChipMargin)
|
||||||
.padding(
|
|
||||||
start = SnackChipMargin,
|
|
||||||
bottom = SnackChipMargin + if (raised) SelectionBarClearance else 0.dp,
|
|
||||||
)
|
|
||||||
.height(SnackChipHeight),
|
.height(SnackChipHeight),
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|
||||||
import androidx.compose.material3.FloatingToolbarDefaults
|
|
||||||
import androidx.compose.material3.HorizontalFloatingToolbar
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
|
|
||||||
/** The selected event's action bar (#141); Edit and Delete only where its calendar allows. */
|
/** The selected event's action bar (#141); Edit and Delete only where its calendar allows. */
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EventSelectionBar(
|
fun EventSelectionBar(
|
||||||
canEdit: Boolean,
|
canEdit: Boolean,
|
||||||
@@ -28,15 +32,23 @@ fun EventSelectionBar(
|
|||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
HorizontalFloatingToolbar(
|
// The FAB's height and colours, so the two read as one control.
|
||||||
expanded = true,
|
Surface(
|
||||||
colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors(),
|
shape = CircleShape,
|
||||||
modifier = modifier,
|
color = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
shadowElevation = 6.dp,
|
||||||
|
modifier = modifier.height(56.dp),
|
||||||
) {
|
) {
|
||||||
if (canEdit) BarAction(Icons.Default.Edit, R.string.event_detail_edit, onEdit)
|
Row(
|
||||||
BarAction(Icons.Default.ContentCopy, R.string.event_detail_duplicate, onDuplicate)
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
BarAction(Icons.Default.Share, R.string.event_detail_share, onShare)
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
if (canDelete) BarAction(Icons.Default.Delete, R.string.event_detail_delete, onDelete)
|
) {
|
||||||
|
if (canEdit) BarAction(Icons.Default.Edit, R.string.event_detail_edit, onEdit)
|
||||||
|
BarAction(Icons.Default.ContentCopy, R.string.event_detail_duplicate, onDuplicate)
|
||||||
|
BarAction(Icons.Default.Share, R.string.event_detail_share, onShare)
|
||||||
|
if (canDelete) BarAction(Icons.Default.Delete, R.string.event_detail_delete, onDelete)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -17,8 +16,8 @@ import de.jeanlucmakiola.calendula.domain.EventInstance
|
|||||||
import de.jeanlucmakiola.floret.components.SnackChipMargin
|
import de.jeanlucmakiola.floret.components.SnackChipMargin
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/** How far the bottom chips rise while the selection bar is open, to clear it. */
|
/** The bottom band the selection bar and FAB cover, margin included. */
|
||||||
val SelectionBarClearance = 64.dp + SnackChipMargin
|
val SelectionBarClearance = 56.dp + SnackChipMargin
|
||||||
|
|
||||||
/** The FAB's role while an event is selected (#141): its action bar's toggle. Null otherwise. */
|
/** The FAB's role while an event is selected (#141): its action bar's toggle. Null otherwise. */
|
||||||
@Composable
|
@Composable
|
||||||
@@ -27,6 +26,8 @@ fun rememberFabSelection(
|
|||||||
viewModel: EventActionsViewModel,
|
viewModel: EventActionsViewModel,
|
||||||
onEdit: (EventInstance) -> Unit,
|
onEdit: (EventInstance) -> Unit,
|
||||||
onDuplicate: (EventForm) -> Unit,
|
onDuplicate: (EventForm) -> Unit,
|
||||||
|
chipShown: Boolean,
|
||||||
|
onDismissChip: () -> Unit,
|
||||||
): FabSelection? {
|
): FabSelection? {
|
||||||
val calendars by viewModel.calendars.collectAsStateWithLifecycle()
|
val calendars by viewModel.calendars.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -36,8 +37,8 @@ fun rememberFabSelection(
|
|||||||
|
|
||||||
// What the FAB has been told for this selection, over what the coverage says.
|
// What the FAB has been told for this selection, over what the coverage says.
|
||||||
var chosen by remember(host.selection?.event) { mutableStateOf<Boolean?>(null) }
|
var chosen by remember(host.selection?.event) { mutableStateOf<Boolean?>(null) }
|
||||||
val expanded = chosen ?: !host.selectionCompact
|
// Folded while a chip is up, which then sits beside the FAB as usual.
|
||||||
SideEffect { host.selectionBarExpanded = expanded && host.selection != null }
|
val expanded = !chipShown && (chosen ?: !host.selectionCompact)
|
||||||
|
|
||||||
val selection = host.selection ?: return null
|
val selection = host.selection ?: return null
|
||||||
val event = selection.event
|
val event = selection.event
|
||||||
@@ -45,7 +46,10 @@ fun rememberFabSelection(
|
|||||||
val canEdit = calendar?.canModifyContents == true
|
val canEdit = calendar?.canModifyContents == true
|
||||||
return FabSelection(
|
return FabSelection(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onToggle = { chosen = !expanded },
|
onToggle = {
|
||||||
|
if (chipShown) onDismissChip()
|
||||||
|
chosen = !expanded
|
||||||
|
},
|
||||||
bar = {
|
bar = {
|
||||||
EventSelectionBar(
|
EventSelectionBar(
|
||||||
canEdit = canEdit,
|
canEdit = canEdit,
|
||||||
|
|||||||
Reference in New Issue
Block a user