Trim comments to the project's style (#141)
This commit is contained in:
@@ -52,11 +52,7 @@ fun EventForm.shiftedByDays(days: Int, deviceZone: TimeZone): EventForm {
|
||||
return copy(start = newStart, end = (newStart.toInstant(zone) + span).toLocalDateTime(zone))
|
||||
}
|
||||
|
||||
/**
|
||||
* The form with its start and end set independently — a timeline resize, which
|
||||
* drags one edge and leaves the other where it stands. Each edge is resolved in
|
||||
* the form's own zone, as [shiftedTo] does. All-day events are returned untouched.
|
||||
*/
|
||||
/** The form with new start and end, each in the form's own zone — a resize. All-day is untouched. */
|
||||
fun EventForm.resizedTo(newStart: Instant, newEnd: Instant, deviceZone: TimeZone): EventForm {
|
||||
if (isAllDay) return this
|
||||
val zone = resolvedZone(deviceZone)
|
||||
|
||||
@@ -539,8 +539,7 @@ fun CalendarHost(
|
||||
viewModel = eventActions,
|
||||
onEdit = onEditEvent,
|
||||
onDuplicate = { form ->
|
||||
// As a duplicate from the detail screen: a fresh event,
|
||||
// through the prefilled-create overlay (#52).
|
||||
// A fresh event, like a duplicate from the detail screen (#52).
|
||||
importFormSource = ImportSource.Insert
|
||||
importForm = form
|
||||
},
|
||||
|
||||
@@ -82,8 +82,7 @@ fun CalendarFabColumn(
|
||||
// Held past the selection clearing, so the bar has something to fold away.
|
||||
val heldBar = remember { mutableStateOf<(@Composable () -> Unit)?>(null) }
|
||||
selection?.let { heldBar.value = it.bar }
|
||||
// Held at the FAB's own height whatever the bar does: the bar is taller,
|
||||
// and a row grown to fit it moved the FAB up as the bar unfolded.
|
||||
// Held at the FAB's height so the taller bar can't shift the FAB.
|
||||
Row(
|
||||
modifier = Modifier.height(FAB_SIZE),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -121,11 +120,7 @@ fun CalendarFabColumn(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The FAB's glyph: "+", or with an event selected a chevron pointing where the
|
||||
* bar unfolds — turned half round once it has, to fold it back. The "+" and the
|
||||
* chevron spin past each other rather than fading.
|
||||
*/
|
||||
/** "+", or with an event selected a chevron that turns to open and fold the bar. */
|
||||
@Composable
|
||||
private fun FabGlyph(selecting: Boolean, expanded: Boolean) {
|
||||
val spatial = MaterialTheme.motionScheme.fastSpatialSpec<Float>()
|
||||
|
||||
@@ -35,11 +35,7 @@ data class DeletePrompt(val event: EventInstance, val isRecurring: Boolean)
|
||||
/** How an action on a selected event ended, when it has something to say. */
|
||||
enum class EventActionFailure { LoadFailed, ShareFailed, DeleteFailed, WriteDenied, Gone }
|
||||
|
||||
/**
|
||||
* The event actions a selected block offers outside the detail screen (#141):
|
||||
* duplicate, share and delete, each loading the event on demand. Hosted at
|
||||
* `CalendarHost` beside [RescheduleViewModel].
|
||||
*/
|
||||
/** Duplicate, share and delete for a selected event (#141), loading it on demand. */
|
||||
@HiltViewModel
|
||||
class EventActionsViewModel @Inject constructor(
|
||||
private val repository: CalendarRepository,
|
||||
|
||||
@@ -116,8 +116,7 @@ private fun rememberPickUpSurface(
|
||||
val currentDrop by rememberUpdatedState(onDrop)
|
||||
val currentCancel by rememberUpdatedState(onCancel)
|
||||
val currentRelease by rememberUpdatedState(onRelease)
|
||||
// Read per gesture rather than keyed on: the pickup itself can change it,
|
||||
// and a restart would cancel the drag it just began.
|
||||
// Not a key: the pickup changes it, and a restart would cancel the drag.
|
||||
val currentImmediate by rememberUpdatedState(immediate)
|
||||
|
||||
return Modifier
|
||||
@@ -128,9 +127,7 @@ private fun rememberPickUpSurface(
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
val origin = down.position
|
||||
val immediate = currentImmediate
|
||||
// Null on the timeout — held still — true once an [immediate]
|
||||
// block has been moved past slop, false when the press is
|
||||
// abandoned.
|
||||
// Null: held still. True: an immediate pickup past slop. False: abandoned.
|
||||
val outcome = withTimeoutOrNull(holdMillis) {
|
||||
var result: Boolean? = null
|
||||
while (result == null) {
|
||||
|
||||
@@ -80,8 +80,7 @@ fun EventMoveHost(
|
||||
val resizedLabel = moved?.endMillis?.let { end ->
|
||||
"$movedLabel–${formatTime(end, use24Hour, locale)}"
|
||||
}
|
||||
// One chip for every message down here: a failed selection action (#141)
|
||||
// shows when no move has anything to say, rather than stacking over it.
|
||||
// A failed selection action (#141) shares the chip rather than stacking on it.
|
||||
val message = when (outcome) {
|
||||
null -> actionFailure
|
||||
is MoveOutcome.Moved -> if (resizedLabel != null) {
|
||||
|
||||
@@ -16,12 +16,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
|
||||
/**
|
||||
* The action bar a selected event unrolls from the FAB (#141), in the FAB's own
|
||||
* colours so it stands clear of the calendar behind it. Edit and Delete are
|
||||
* offered only where the event's calendar allows them; a read-only event can
|
||||
* still be duplicated into a writable calendar, or shared.
|
||||
*/
|
||||
/** The selected event's action bar (#141); Edit and Delete only where its calendar allows. */
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun EventSelectionBar(
|
||||
|
||||
@@ -20,11 +20,7 @@ import kotlinx.coroutines.launch
|
||||
/** How far the bottom chips rise while the selection bar is open, to clear it. */
|
||||
val SelectionBarClearance = 64.dp + SnackChipMargin
|
||||
|
||||
/**
|
||||
* What the FAB becomes while an event is selected (#141): the toggle for its
|
||||
* action bar, folded while it would cover the selected event's bottom handle.
|
||||
* Null with nothing selected.
|
||||
*/
|
||||
/** The FAB's role while an event is selected (#141): its action bar's toggle. Null otherwise. */
|
||||
@Composable
|
||||
fun rememberFabSelection(
|
||||
host: ChromeHost,
|
||||
|
||||
@@ -29,13 +29,8 @@ private val HANDLE_DOT = 16.dp
|
||||
private val HANDLE_RING = 2.dp
|
||||
|
||||
/**
|
||||
* A grip on one edge of a selected event (#141). It takes the gesture from the
|
||||
* first movement, with no hold — the event is already selected — and hands the
|
||||
* finger on in root coordinates. A tap is swallowed rather than falling through
|
||||
* to the slot under it. While not [enabled] it takes no new touch.
|
||||
*
|
||||
* No semantics of its own: TalkBack reaches the same change through the event's
|
||||
* edit action.
|
||||
* A grip on one edge of a selected event (#141), dragged without a hold. No
|
||||
* semantics: TalkBack resizes through the event's edit action.
|
||||
*/
|
||||
@Composable
|
||||
fun ResizeHandle(
|
||||
@@ -52,8 +47,7 @@ fun ResizeHandle(
|
||||
val currentMove by rememberUpdatedState(onMove)
|
||||
val currentEnd by rememberUpdatedState(onEnd)
|
||||
val currentCancel by rememberUpdatedState(onCancel)
|
||||
// Read per gesture: a hidden handle takes no new touch, but turning it off
|
||||
// mid-resize must not cancel the one it is running.
|
||||
// Not a key: turning it off mid-resize must not cancel that resize.
|
||||
val currentEnabled by rememberUpdatedState(enabled)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
|
||||
@@ -275,10 +275,8 @@ enum class ResizeEdge { Top, Bottom }
|
||||
data class TimelineResizeDrop(val event: EventInstance, val start: Instant, val end: Instant)
|
||||
|
||||
/**
|
||||
* The span after dragging [edge] to [edgeMin] in day [edgeDay], all in minutes
|
||||
* from the start day's midnight.
|
||||
* The edge snaps to the grid, and never comes closer than one snap step to the
|
||||
* other end — an event can't be resized away to nothing, or turned inside out.
|
||||
* The span after dragging [edge] to [edgeMin] in day [edgeDay], in minutes from the
|
||||
* start day's midnight. Snapped, and at least one snap step long.
|
||||
*/
|
||||
internal fun resizedSpan(
|
||||
edge: ResizeEdge,
|
||||
@@ -287,9 +285,7 @@ internal fun resizedSpan(
|
||||
edgeMin: Float,
|
||||
edgeDay: Int? = null,
|
||||
): Pair<Int, Int> {
|
||||
// Dragged into a day past the other end — the start into a day after the
|
||||
// event ends, the end into one before it starts: no change, rather than an
|
||||
// event collapsed against its other end.
|
||||
// An edge dragged into a day past the other end changes nothing.
|
||||
if (edgeDay != null) {
|
||||
val endDay = (endMin - 1).floorDiv(MINUTES_PER_DAY).coerceAtLeast(0)
|
||||
val wrongWay = when (edge) {
|
||||
@@ -305,11 +301,7 @@ internal fun resizedSpan(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [minutes] snapped to the grid, kept at [held] until the finger is three
|
||||
* quarters of a step away from it: plain rounding flips between two slots
|
||||
* whenever the finger rests near the line between them.
|
||||
*/
|
||||
/** [minutes] snapped, kept at [held] until three quarters of a step away. */
|
||||
internal fun stickySnap(minutes: Float, held: Int?): Int {
|
||||
if (held != null && kotlin.math.abs(minutes - held) < DRAG_SNAP_MINUTES * 0.75f) return held
|
||||
return (minutes / DRAG_SNAP_MINUTES).roundToInt() * DRAG_SNAP_MINUTES
|
||||
@@ -318,12 +310,7 @@ internal fun stickySnap(minutes: Float, held: Int?): Int {
|
||||
/** How far past its column's side the finger has to go before a resize changes day. */
|
||||
private const val COLUMN_HYSTERESIS = 0.25f
|
||||
|
||||
/**
|
||||
* The column under a finger at [position] columns from the grid's start, kept at
|
||||
* [held] until the finger is a [COLUMN_HYSTERESIS] into the next. A resize drags
|
||||
* up and down: the finger drifts sideways as it goes, and in a narrow week column
|
||||
* every drift over a border moved the edge by a whole day and back.
|
||||
*/
|
||||
/** The column under [position], kept at [held] until [COLUMN_HYSTERESIS] into another. */
|
||||
internal fun stickyColumn(position: Float, held: Int?, lastIndex: Int): Int {
|
||||
val under = position.toInt().coerceIn(0, lastIndex)
|
||||
if (held == null) return under
|
||||
@@ -433,13 +420,8 @@ class TimelineDragController {
|
||||
private set
|
||||
|
||||
/**
|
||||
* Whether [block] is the selected event. Matched on the series and start too,
|
||||
* since the provider regenerates `Instances` rows and a re-read can bring the
|
||||
* same occurrence back under a new instance id — and, just after a move, on
|
||||
* what the event says, since moving one occurrence of a series writes it out
|
||||
* as an exception row with an id of its own. Only then: two copies of one
|
||||
* event would otherwise both match, and take the selection back and forth.
|
||||
* A blank title matches nothing that way.
|
||||
* Whether [block] is the selected event: by instance, by series and start, and
|
||||
* just after a move by title and times, since a moved occurrence gets new ids.
|
||||
*/
|
||||
fun isSelected(block: TimedBlock): Boolean {
|
||||
val current = selection ?: return false
|
||||
@@ -457,11 +439,7 @@ class TimelineDragController {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Take over [block]'s event as the selection once the grid shows it, so the
|
||||
* bar acts on the row that is there now — an exception written by a move has
|
||||
* its own id, and an abandoned move left the original times standing.
|
||||
*/
|
||||
/** Point the selection at [block]'s event, so the bar acts on the row now there. */
|
||||
fun adoptSelection(block: TimedBlock) {
|
||||
val current = selection ?: return
|
||||
if (current.event != block.event || current.moved) {
|
||||
@@ -539,8 +517,7 @@ class TimelineDragController {
|
||||
pointerInRoot: Offset,
|
||||
blockInRoot: Offset,
|
||||
) {
|
||||
// Picking up another event ends the selection; the selected one keeps
|
||||
// it through the drag, and the screen moves it along with the drop.
|
||||
// The selected event keeps its selection through a drag; any other ends it.
|
||||
if (!isSelected(block)) deselect()
|
||||
source = block
|
||||
this.clipOffsetMin = clipOffsetMin
|
||||
@@ -658,8 +635,7 @@ class TimelineDragController {
|
||||
resizeEdge = edge
|
||||
resizeDay = start.date
|
||||
resizeStartMin = start.time.toSecondOfDay() / 60
|
||||
// Wall clock at both ends, as the drop resolves them: across a DST
|
||||
// change the event's length in minutes isn't its span on the clock.
|
||||
// Wall clock at both ends, as the drop resolves them (DST-safe).
|
||||
resizeEndMin = maxOf(
|
||||
resizeStartMin,
|
||||
(end.date.toEpochDays() - start.date.toEpochDays()).toInt() * MINUTES_PER_DAY +
|
||||
@@ -767,11 +743,7 @@ class TimelineDragController {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Note where the selected block's bottom edge, [extraPx] of handle included,
|
||||
* sits against the action bar [clearancePx] tall over the bottom of the
|
||||
* viewport. Under it, the bar draws itself compact out of the way (#141).
|
||||
*/
|
||||
/** Note whether the action bar would cover the selected block's bottom handle (#141). */
|
||||
fun noteSelectedBottom(block: LayoutCoordinates, extraPx: Float, clearancePx: Float) {
|
||||
val viewport = geometry.viewport?.takeIf { it.isAttached } ?: return
|
||||
if (!block.isAttached) return
|
||||
@@ -938,12 +910,7 @@ fun TimedBlock.continuesAfter(day: LocalDate, zone: TimeZone): Boolean =
|
||||
fun TimelineDrop.startInstant(zone: TimeZone): Instant =
|
||||
wallClockInstant(date, startMin - clipOffsetMin, zone)
|
||||
|
||||
/**
|
||||
* [minutes] past [date]'s midnight as an instant, read as wall clock in [zone].
|
||||
* Resolved as a day plus a minute of it rather than as an offset from an
|
||||
* instant, which would shift by an hour across a DST boundary; a block can be
|
||||
* dragged above its midnight and past the next one, so either end may overflow.
|
||||
*/
|
||||
/** [minutes] past [date]'s midnight, either way past it, as wall clock in [zone] (DST-safe). */
|
||||
fun wallClockInstant(date: LocalDate, minutes: Int, zone: TimeZone): Instant {
|
||||
val day = date.plus(minutes.floorDiv(MINUTES_PER_DAY), DateTimeUnit.DAY)
|
||||
val minute = minutes.mod(MINUTES_PER_DAY)
|
||||
@@ -1207,11 +1174,7 @@ private fun DragCopy(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How far a selected block is lifted, between 0 and 1 (#141). [snap] takes the
|
||||
* lift over at once — from the floating copy it replaces — rather than raising
|
||||
* the block a second time.
|
||||
*/
|
||||
/** How far a selected block is lifted, 0 to 1 (#141); [snap] takes it over at once. */
|
||||
@Composable
|
||||
fun selectionLift(selected: Boolean, snap: Boolean = false): Float {
|
||||
val reduceMotion = rememberReduceMotion()
|
||||
|
||||
@@ -563,8 +563,7 @@ private fun WeekContent(
|
||||
)
|
||||
}
|
||||
|
||||
// A long-pressed event stays selected (#141) until something else is touched:
|
||||
// back, another page, a view switch, or any tap.
|
||||
// Selection (#141) ends on back, a page or view change, or any tap.
|
||||
val selection = dragController.selection
|
||||
PublishSelection(
|
||||
event = selection?.event,
|
||||
@@ -1562,9 +1561,7 @@ private fun EventBlock(
|
||||
val clipOffset = remember(block, date, zone) { block.clipOffsetMinutes(date, zone) }
|
||||
val shape = remember(cuts) { timedBlockShape(cuts.top, cuts.bottom) }
|
||||
val topInsetPx = with(density) { topInset.toPx() }
|
||||
// A long press lifts the block (#141): moved, it drags; let go where it was
|
||||
// picked up, it stays selected. A block that can't move is selected on the
|
||||
// spot. Once selected, it drags without another hold.
|
||||
// Long press (#141): move to drag, let go to select; selected blocks drag at once.
|
||||
val dragModifier = rememberEventDragSource(
|
||||
enabled = selectable,
|
||||
key = block.event.instanceId,
|
||||
@@ -1594,25 +1591,20 @@ private fun EventBlock(
|
||||
onRelease = { dragController.select(block.event, fromLift = draggable) },
|
||||
)
|
||||
val lifted = draggable && dragController.ghosts(block)
|
||||
// Let go of without moving, the block takes the floating copy's place as it
|
||||
// stands: whole and lifted at once, not faded back up and raised again.
|
||||
// Released in place, the block takes over the copy's lift without a dip.
|
||||
val takesOverLift = selected && dragController.selection?.fromLift == true
|
||||
val ghostFade = ghostAlpha(lifted)
|
||||
val ghost = if (takesOverLift && !lifted) 1f else ghostFade
|
||||
val raise = selectionLift(selected, snap = takesOverLift)
|
||||
// Where the selected event's bottom handle sits against the action bar, which
|
||||
// compacts itself rather than cover it. Read off the piece that ends the event.
|
||||
// Tells the action bar to fold when it would cover the bottom handle.
|
||||
val revealExtraPx = with(density) { (ResizeHandleTarget / 2).toPx() }
|
||||
val barClearancePx = with(density) { SelectionBarClearance.toPx() }
|
||||
val tracksBar = selected && !cuts.bottom
|
||||
// Not while the block is waiting on its own write: until then it still
|
||||
// shows the times from before it.
|
||||
// Not while its own write is pending: it still shows the old times.
|
||||
LaunchedEffect(selected, block.event, lifted) {
|
||||
if (selected && !lifted) dragController.adoptSelection(block)
|
||||
}
|
||||
// Handles on the selected event's own ends — not on a cut at midnight. They
|
||||
// stay composed for the whole selection and only hide while something is in
|
||||
// flight: removing them would cancel the resize they started.
|
||||
// Kept composed while hidden: removing them would cancel their own resize.
|
||||
DisposableEffect(selected) {
|
||||
if (selected) dragController.noteSelectedBlock(true)
|
||||
onDispose { if (selected) dragController.noteSelectedBlock(false) }
|
||||
|
||||
@@ -680,8 +680,7 @@ class RescheduleViewModelTest {
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
// The start changed day but not by a whole number of days, so the
|
||||
// series anchor can't follow it.
|
||||
// Not a whole-day shift, so the series anchor can't follow.
|
||||
assertThat(vm.scopePrompt.value)
|
||||
.isEqualTo(MoveScopePrompt(occurrenceOnly = true, resize = true))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user