Compare commits
1 Commits
b61dfa441c
...
fix/180-de
| Author | SHA1 | Date | |
|---|---|---|---|
| bc805a9a3a |
@@ -1,6 +1,7 @@
|
|||||||
package de.jeanlucmakiola.calendula.data.calendar
|
package de.jeanlucmakiola.calendula.data.calendar
|
||||||
|
|
||||||
import de.jeanlucmakiola.floret.time.toKotlinInstantFromEpochMillis
|
import de.jeanlucmakiola.floret.time.toKotlinInstantFromEpochMillis
|
||||||
|
import android.provider.CalendarContract
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
|
||||||
@@ -38,5 +39,7 @@ internal fun ColumnReader.toEventInstance(): EventInstance? {
|
|||||||
isAllDay = getInt(InstanceProjection.IDX_ALL_DAY) != 0,
|
isAllDay = getInt(InstanceProjection.IDX_ALL_DAY) != 0,
|
||||||
color = color,
|
color = color,
|
||||||
location = getString(InstanceProjection.IDX_LOCATION),
|
location = getString(InstanceProjection.IDX_LOCATION),
|
||||||
|
isDeclined = getInt(InstanceProjection.IDX_SELF_ATTENDEE_STATUS) ==
|
||||||
|
CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ internal object InstanceProjection {
|
|||||||
CalendarContract.Instances.EVENT_COLOR,
|
CalendarContract.Instances.EVENT_COLOR,
|
||||||
CalendarContract.Instances.CALENDAR_COLOR,
|
CalendarContract.Instances.CALENDAR_COLOR,
|
||||||
CalendarContract.Instances.EVENT_LOCATION,
|
CalendarContract.Instances.EVENT_LOCATION,
|
||||||
|
CalendarContract.Instances.SELF_ATTENDEE_STATUS,
|
||||||
)
|
)
|
||||||
|
|
||||||
const val IDX_INSTANCE_ID = 0
|
const val IDX_INSTANCE_ID = 0
|
||||||
@@ -64,6 +65,7 @@ internal object InstanceProjection {
|
|||||||
const val IDX_EVENT_COLOR = 7
|
const val IDX_EVENT_COLOR = 7
|
||||||
const val IDX_CALENDAR_COLOR = 8
|
const val IDX_CALENDAR_COLOR = 8
|
||||||
const val IDX_LOCATION = 9
|
const val IDX_LOCATION = 9
|
||||||
|
const val IDX_SELF_ATTENDEE_STATUS = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object EventDetailProjection {
|
internal object EventDetailProjection {
|
||||||
@@ -182,6 +184,7 @@ internal object SearchProjection {
|
|||||||
CalendarContract.Events.RDATE,
|
CalendarContract.Events.RDATE,
|
||||||
// Excerpted, not just filtered on: a hit has to show what it matched.
|
// Excerpted, not just filtered on: a hit has to show what it matched.
|
||||||
CalendarContract.Events.DESCRIPTION,
|
CalendarContract.Events.DESCRIPTION,
|
||||||
|
CalendarContract.Events.SELF_ATTENDEE_STATUS,
|
||||||
)
|
)
|
||||||
|
|
||||||
const val IDX_ID = 0
|
const val IDX_ID = 0
|
||||||
@@ -197,6 +200,7 @@ internal object SearchProjection {
|
|||||||
const val IDX_RRULE = 10
|
const val IDX_RRULE = 10
|
||||||
const val IDX_RDATE = 11
|
const val IDX_RDATE = 11
|
||||||
const val IDX_DESCRIPTION = 12
|
const val IDX_DESCRIPTION = 12
|
||||||
|
const val IDX_SELF_ATTENDEE_STATUS = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.jeanlucmakiola.calendula.data.calendar
|
package de.jeanlucmakiola.calendula.data.calendar
|
||||||
|
|
||||||
|
import android.provider.CalendarContract
|
||||||
import de.jeanlucmakiola.floret.time.toKotlinInstantFromEpochMillis
|
import de.jeanlucmakiola.floret.time.toKotlinInstantFromEpochMillis
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.domain.ics.parseRfc2445DurationMillis
|
import de.jeanlucmakiola.calendula.domain.ics.parseRfc2445DurationMillis
|
||||||
@@ -42,5 +43,7 @@ internal fun ColumnReader.toSearchResult(): EventInstance? {
|
|||||||
location = getString(SearchProjection.IDX_LOCATION),
|
location = getString(SearchProjection.IDX_LOCATION),
|
||||||
isRecurring = !getString(SearchProjection.IDX_RRULE).isNullOrEmpty() ||
|
isRecurring = !getString(SearchProjection.IDX_RRULE).isNullOrEmpty() ||
|
||||||
!getString(SearchProjection.IDX_RDATE).isNullOrEmpty(),
|
!getString(SearchProjection.IDX_RDATE).isNullOrEmpty(),
|
||||||
|
isDeclined = getInt(SearchProjection.IDX_SELF_ATTENDEE_STATUS) ==
|
||||||
|
CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,16 @@ class ProviderReminderInstanceSource @Inject constructor(
|
|||||||
// `visible` is the flag the app's one visibility model writes (#75).
|
// `visible` is the flag the app's one visibility model writes (#75).
|
||||||
// The status clause mirrors CalendarDataSource.instances: NULL means
|
// The status clause mirrors CalendarDataSource.instances: NULL means
|
||||||
// "normal", so a bare `!= CANCELED` would drop every ordinary event.
|
// "normal", so a bare `!= CANCELED` would drop every ordinary event.
|
||||||
|
// An invitation the user declined is answered — it stays on the calendar
|
||||||
|
// struck through, but it plans nothing (#180). NULL again means "no
|
||||||
|
// answer recorded", which is not a "no".
|
||||||
val selection = "${CalendarContract.Calendars.VISIBLE} = 1 AND " +
|
val selection = "${CalendarContract.Calendars.VISIBLE} = 1 AND " +
|
||||||
"(${CalendarContract.Instances.STATUS} IS NULL OR " +
|
"(${CalendarContract.Instances.STATUS} IS NULL OR " +
|
||||||
"${CalendarContract.Instances.STATUS} != ${CalendarContract.Events.STATUS_CANCELED})"
|
"${CalendarContract.Instances.STATUS} != " +
|
||||||
|
"${CalendarContract.Events.STATUS_CANCELED}) AND " +
|
||||||
|
"(${CalendarContract.Instances.SELF_ATTENDEE_STATUS} IS NULL OR " +
|
||||||
|
"${CalendarContract.Instances.SELF_ATTENDEE_STATUS} != " +
|
||||||
|
"${CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED})"
|
||||||
return context.contentResolver.query(
|
return context.contentResolver.query(
|
||||||
uri, OCCURRENCE_PROJECTION, selection, null, null,
|
uri, OCCURRENCE_PROJECTION, selection, null, null,
|
||||||
)?.use { c ->
|
)?.use { c ->
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ data class EventInstance(
|
|||||||
* Instances query already yields one row per occurrence.
|
* Instances query already yields one row per occurrence.
|
||||||
*/
|
*/
|
||||||
val isRecurring: Boolean = false,
|
val isRecurring: Boolean = false,
|
||||||
|
/**
|
||||||
|
* This device user answered "no" to the invitation
|
||||||
|
* (`Events.SELF_ATTENDEE_STATUS`). The event stays on the calendar — it is
|
||||||
|
* still an appointment someone expects an answer about — but every surface
|
||||||
|
* strikes it through, and it plans no reminders (#180).
|
||||||
|
*/
|
||||||
|
val isDeclined: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.declinedTitle
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventAccent
|
import de.jeanlucmakiola.calendula.ui.common.eventAccent
|
||||||
@@ -129,8 +131,8 @@ internal fun AgendaEventRow(
|
|||||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
modifier = if (dimmed) modifier.alpha(EventDimAlpha) else modifier,
|
modifier = if (dimmed) modifier.alpha(EventDimAlpha) else modifier,
|
||||||
title = title,
|
title = declinedTitle(title, event.isDeclined),
|
||||||
summary = agendaTimeSummary(event, day, zone),
|
summary = AnnotatedString(agendaTimeSummary(event, day, zone)),
|
||||||
position = position,
|
position = position,
|
||||||
minHeight = 64.dp,
|
minHeight = 64.dp,
|
||||||
leading = {
|
leading = {
|
||||||
|
|||||||
@@ -36,14 +36,6 @@ val GUTTER_WIDTH = 48.dp
|
|||||||
*/
|
*/
|
||||||
val GUTTER_CONTENT_START_INSET = 8.dp
|
val GUTTER_CONTENT_START_INSET = 8.dp
|
||||||
|
|
||||||
/**
|
|
||||||
* End inset for everything that lines up with the day columns — the week's day
|
|
||||||
* header, the all-day strip and the timeline itself. Without it the last column
|
|
||||||
* runs flush into the screen edge while the gutter gives the other side room
|
|
||||||
* (#192); 8dp mirrors [GUTTER_CONTENT_START_INSET].
|
|
||||||
*/
|
|
||||||
val TIMELINE_CONTENT_END_INSET = 8.dp
|
|
||||||
|
|
||||||
private val BADGE_HEIGHT = 20.dp
|
private val BADGE_HEIGHT = 20.dp
|
||||||
|
|
||||||
/** How far the fixed hour labels recede while a block is being dragged. */
|
/** How far the fixed hour labels recede while a block is being dragged. */
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,3 +18,30 @@ val LocalDimCutoff = compositionLocalOf<Instant?> { null }
|
|||||||
|
|
||||||
/** Opacity applied to a completed/past event chip when it is dimmed. */
|
/** Opacity applied to a completed/past event chip when it is dimmed. */
|
||||||
const val EventDimAlpha = 0.4f
|
const val EventDimAlpha = 0.4f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a declined invitation's title is struck through (#180) — the mark every
|
||||||
|
* surface uses to say "you answered no", chosen over hiding the event because it
|
||||||
|
* is still something the organiser expects you at. Null for everything else, so
|
||||||
|
* it drops straight into a `Text`'s `textDecoration`.
|
||||||
|
*/
|
||||||
|
fun declinedDecoration(isDeclined: Boolean): TextDecoration? =
|
||||||
|
if (isDeclined) TextDecoration.LineThrough else null
|
||||||
|
|
||||||
|
/** [declinedDecoration] for the rows that take styled text rather than a `String`. */
|
||||||
|
fun declinedTitle(title: String, isDeclined: Boolean): AnnotatedString = if (isDeclined) {
|
||||||
|
AnnotatedString(title, SpanStyle(textDecoration = TextDecoration.LineThrough))
|
||||||
|
} else {
|
||||||
|
AnnotatedString(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** [declinedTitle] over already-styled text, e.g. a search hit's marked runs. */
|
||||||
|
fun declinedTitle(title: AnnotatedString, isDeclined: Boolean): AnnotatedString =
|
||||||
|
if (!isDeclined) {
|
||||||
|
title
|
||||||
|
} else {
|
||||||
|
buildAnnotatedString {
|
||||||
|
append(title)
|
||||||
|
addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough), 0, title.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ import de.jeanlucmakiola.calendula.ui.common.next
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventInk
|
import de.jeanlucmakiola.calendula.ui.common.eventInk
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.declinedDecoration
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
import de.jeanlucmakiola.floret.locale.currentLocale
|
import de.jeanlucmakiola.floret.locale.currentLocale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
@@ -118,7 +119,6 @@ import de.jeanlucmakiola.calendula.ui.common.rememberTimelinePinchZoom
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
||||||
import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH
|
import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH
|
||||||
import de.jeanlucmakiola.calendula.ui.common.HourGutter
|
import de.jeanlucmakiola.calendula.ui.common.HourGutter
|
||||||
import de.jeanlucmakiola.calendula.ui.common.TIMELINE_CONTENT_END_INSET
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
|
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.tappedMinuteOfDay
|
import de.jeanlucmakiola.calendula.ui.common.tappedMinuteOfDay
|
||||||
import de.jeanlucmakiola.calendula.ui.week.TimedBlock
|
import de.jeanlucmakiola.calendula.ui.week.TimedBlock
|
||||||
@@ -472,11 +472,7 @@ private fun AllDayStrip(
|
|||||||
// Height is hoisted + animated so it resizes smoothly; padding sits
|
// Height is hoisted + animated so it resizes smoothly; padding sits
|
||||||
// inside it so the content area is lanes * row height.
|
// inside it so the content area is lanes * row height.
|
||||||
.height(height)
|
.height(height)
|
||||||
.padding(
|
.padding(vertical = ALL_DAY_VERTICAL_PADDING),
|
||||||
top = ALL_DAY_VERTICAL_PADDING,
|
|
||||||
bottom = ALL_DAY_VERTICAL_PADDING,
|
|
||||||
end = TIMELINE_CONTENT_END_INSET,
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
// Keep the gutter-width offset so the bars line up with the day column.
|
// Keep the gutter-width offset so the bars line up with the day column.
|
||||||
Spacer(Modifier.width(GUTTER_WIDTH))
|
Spacer(Modifier.width(GUTTER_WIDTH))
|
||||||
@@ -530,6 +526,7 @@ private fun AllDayBar(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = eventInk(fill),
|
color = eventInk(fill),
|
||||||
|
textDecoration = declinedDecoration(event.isDeclined),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,7 +572,6 @@ private fun Timeline(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(end = TIMELINE_CONTENT_END_INSET)
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.onGloballyPositioned { dragController.geometry.viewport = it },
|
.onGloballyPositioned { dragController.geometry.viewport = it },
|
||||||
@@ -778,6 +774,7 @@ private fun EventBlock(
|
|||||||
maxLines = if (showTime) 1 else 2,
|
maxLines = if (showTime) 1 else 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = 0.85f),
|
||||||
|
textDecoration = declinedDecoration(block.event.isDeclined),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showTime) {
|
if (showTime) {
|
||||||
@@ -798,12 +795,7 @@ private fun DayLoading() {
|
|||||||
// Same scale resolution as the loaded timeline, so the skeleton's column
|
// Same scale resolution as the loaded timeline, so the skeleton's column
|
||||||
// doesn't resize the moment the real day arrives.
|
// doesn't resize the moment the real day arrives.
|
||||||
val totalHeight = scale.hourHeight(maxHeight) * 24
|
val totalHeight = scale.hourHeight(maxHeight) * 24
|
||||||
Row(
|
Row(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(end = TIMELINE_CONTENT_END_INSET)
|
|
||||||
.verticalScroll(scrollState),
|
|
||||||
) {
|
|
||||||
Spacer(Modifier.width(GUTTER_WIDTH))
|
Spacer(Modifier.width(GUTTER_WIDTH))
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ import androidx.compose.animation.togetherWith
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -113,7 +111,6 @@ import androidx.compose.ui.geometry.Size
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -141,6 +138,7 @@ import de.jeanlucmakiola.calendula.ui.common.CALENDAR_SWIPE_THRESHOLD
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.declinedDecoration
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventAccent
|
import de.jeanlucmakiola.calendula.ui.common.eventAccent
|
||||||
@@ -442,7 +440,6 @@ fun MonthScreen(
|
|||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onRetry = jumpToToday,
|
onRetry = jumpToToday,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
|
||||||
)
|
)
|
||||||
} else if (viewStyle == MonthViewStyle.Split) {
|
} else if (viewStyle == MonthViewStyle.Split) {
|
||||||
SplitMonthContent(
|
SplitMonthContent(
|
||||||
@@ -467,7 +464,6 @@ fun MonthScreen(
|
|||||||
onSwipePrev = goPrev,
|
onSwipePrev = goPrev,
|
||||||
onRetry = jumpToToday,
|
onRetry = jumpToToday,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -578,7 +574,6 @@ private fun MonthContent(
|
|||||||
onSwipePrev: () -> Unit,
|
onSwipePrev: () -> Unit,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val slideSpec = rememberCalendarSlideSpec()
|
val slideSpec = rememberCalendarSlideSpec()
|
||||||
val fadeSpec = rememberCalendarFadeSpec()
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
@@ -605,7 +600,6 @@ private fun MonthContent(
|
|||||||
state = s,
|
state = s,
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -624,7 +618,6 @@ private fun ContinuousMonthContent(
|
|||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
when (state) {
|
when (state) {
|
||||||
// The scrolling styles get their own skeleton rather than the paged
|
// The scrolling styles get their own skeleton rather than the paged
|
||||||
@@ -639,7 +632,6 @@ private fun ContinuousMonthContent(
|
|||||||
listState = listState,
|
listState = listState,
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ContinuousMonthGrid(
|
ContinuousMonthGrid(
|
||||||
@@ -647,7 +639,6 @@ private fun ContinuousMonthContent(
|
|||||||
listState = listState,
|
listState = listState,
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -746,8 +737,7 @@ private val CELL_SHAPE = RoundedCornerShape(CELL_CORNER)
|
|||||||
|
|
||||||
/** Width of the split style's selected-day outline. */
|
/** Width of the split style's selected-day outline. */
|
||||||
private val SPLIT_SELECTION_STROKE = 1.5.dp
|
private val SPLIT_SELECTION_STROKE = 1.5.dp
|
||||||
/** Lanes of bars/pills a day cell draws before the rest become overflow dots. */
|
private const val MAX_EVENT_ROWS = 3
|
||||||
internal const val MAX_EVENT_ROWS = 3
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Row height in the continuous grid. The paged grid divides the viewport between
|
* Row height in the continuous grid. The paged grid divides the viewport between
|
||||||
@@ -765,15 +755,11 @@ private val CONTINUOUS_ROW_HEIGHT = 112.dp
|
|||||||
*/
|
*/
|
||||||
private val CONTINUOUS_MONTH_GAP = 20.dp
|
private val CONTINUOUS_MONTH_GAP = 20.dp
|
||||||
|
|
||||||
/** Gap between the weekday header and the seamless stream's first week row. */
|
|
||||||
private val DENSE_HEADER_GAP = 4.dp
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MonthGrid(
|
internal fun MonthGrid(
|
||||||
state: MonthUiState.Success,
|
state: MonthUiState.Success,
|
||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
|
||||||
/** See [MonthWeekRow]'s `selected`: an anchor for the morph, never a mark. */
|
/** See [MonthWeekRow]'s `selected`: an anchor for the morph, never a mark. */
|
||||||
selected: LocalDate? = null,
|
selected: LocalDate? = null,
|
||||||
) {
|
) {
|
||||||
@@ -794,7 +780,6 @@ internal fun MonthGrid(
|
|||||||
inMonth = { it.month == month.month && it.year == month.year },
|
inMonth = { it.month == month.month && it.year == month.year },
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
|
||||||
selected = selected,
|
selected = selected,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -825,7 +810,6 @@ internal fun ContinuousMonthGrid(
|
|||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val monthCount = remember { continuousMonthCount() }
|
val monthCount = remember { continuousMonthCount() }
|
||||||
@@ -853,7 +837,6 @@ internal fun ContinuousMonthGrid(
|
|||||||
today = state.today,
|
today = state.today,
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -873,7 +856,6 @@ private fun ContinuousMonthBlock(
|
|||||||
today: LocalDate,
|
today: LocalDate,
|
||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val rowCount = remember(month, weekStart) { weekRowsInMonth(month, weekStart) }
|
val rowCount = remember(month, weekStart) { weekRowsInMonth(month, weekStart) }
|
||||||
Column(
|
Column(
|
||||||
@@ -896,7 +878,6 @@ private fun ContinuousMonthBlock(
|
|||||||
blankOutside = true,
|
blankOutside = true,
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(CONTINUOUS_ROW_HEIGHT),
|
.height(CONTINUOUS_ROW_HEIGHT),
|
||||||
@@ -955,7 +936,6 @@ internal fun DenseMonthGrid(
|
|||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val weekCount = remember(state.weekStart) { continuousWeekCount(state.weekStart) }
|
val weekCount = remember(state.weekStart) { continuousWeekCount(state.weekStart) }
|
||||||
@@ -963,15 +943,10 @@ internal fun DenseMonthGrid(
|
|||||||
state = listState,
|
state = listState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
// The gap under the weekday header is a real margin, not content
|
.padding(horizontal = 8.dp),
|
||||||
// padding: a lazy list scrolls its rows *through* the before-content
|
|
||||||
// padding, so scrolling to a week landed its row that far down with
|
|
||||||
// the tail of the previous one showing above it (#191).
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = DENSE_HEADER_GAP),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
// Bottom inset clears the FAB stack so the last row stays tappable.
|
// Bottom inset clears the FAB stack so the last row stays tappable.
|
||||||
contentPadding = PaddingValues(bottom = 96.dp),
|
contentPadding = PaddingValues(top = 4.dp, bottom = 96.dp),
|
||||||
) {
|
) {
|
||||||
items(count = weekCount, key = { it }) { index ->
|
items(count = weekCount, key = { it }) { index ->
|
||||||
val week = state.weeksByIndex[index]
|
val week = state.weeksByIndex[index]
|
||||||
@@ -986,7 +961,6 @@ internal fun DenseMonthGrid(
|
|||||||
inMonth = { true },
|
inMonth = { true },
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
|
||||||
labelMonthOnFirst = true,
|
labelMonthOnFirst = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -1214,9 +1188,6 @@ private fun SplitMonthBody(
|
|||||||
onSelectDay(it)
|
onSelectDay(it)
|
||||||
onSetExpanded(false)
|
onSetExpanded(false)
|
||||||
},
|
},
|
||||||
// A tapped chip is asking for that event, not for its day, so it
|
|
||||||
// opens the detail from here too rather than collapsing (#187).
|
|
||||||
onEventClick = onEventClick,
|
|
||||||
onCollapse = { onSetExpanded(false) },
|
onCollapse = { onSetExpanded(false) },
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -1317,7 +1288,6 @@ private fun SplitMonthExpanded(
|
|||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
swipeModifier: Modifier,
|
swipeModifier: Modifier,
|
||||||
onPickDay: (LocalDate) -> Unit,
|
onPickDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
|
||||||
onCollapse: () -> Unit,
|
onCollapse: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val slideSpec = rememberCalendarSlideSpec()
|
val slideSpec = rememberCalendarSlideSpec()
|
||||||
@@ -1340,7 +1310,6 @@ private fun SplitMonthExpanded(
|
|||||||
state = s,
|
state = s,
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onPickDay,
|
onOpenDay = onPickDay,
|
||||||
onEventClick = onEventClick,
|
|
||||||
selected = sel,
|
selected = sel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1821,7 +1790,7 @@ private fun ContinuousMonthSkeleton(dense: Boolean) {
|
|||||||
modifier = Modifier.padding(bottom = 8.dp),
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Spacer(Modifier.height(DENSE_HEADER_GAP))
|
Spacer(Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
// More rows than a viewport holds; the clip takes the overflow.
|
// More rows than a viewport holds; the clip takes the overflow.
|
||||||
repeat(6) {
|
repeat(6) {
|
||||||
@@ -1869,7 +1838,6 @@ private fun MonthWeekRow(
|
|||||||
inMonth: (LocalDate) -> Boolean,
|
inMonth: (LocalDate) -> Boolean,
|
||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
blankOutside: Boolean = false,
|
blankOutside: Boolean = false,
|
||||||
labelMonthOnFirst: Boolean = false,
|
labelMonthOnFirst: Boolean = false,
|
||||||
@@ -2168,17 +2136,11 @@ private fun MonthWeekRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tap layer: a tap on a chip opens that event, anything else opens the
|
// Tap layer: in month view a tap on any day opens that day. Padded and
|
||||||
// day (#187). The chips take no pointer input of their own — this
|
|
||||||
// layer covers them — so the lane under the finger is resolved
|
|
||||||
// geometrically, exactly as the drag pickup above does it; the down
|
|
||||||
// position is read on the initial pass, which consumes nothing and so
|
|
||||||
// leaves both the click and a pickup in flight untouched. Padded and
|
|
||||||
// clipped to the background pill so the ripple matches it. A blanked
|
// clipped to the background pill so the ripple matches it. A blanked
|
||||||
// cell isn't part of this month, so it takes no taps either.
|
// cell isn't part of this month, so it takes no taps either.
|
||||||
val downY = remember(week.days.size) { FloatArray(week.days.size) }
|
|
||||||
Row(Modifier.matchParentSize()) {
|
Row(Modifier.matchParentSize()) {
|
||||||
week.days.forEachIndexed { col, d ->
|
week.days.forEach { d ->
|
||||||
if (blankOutside && !inMonth(d)) {
|
if (blankOutside && !inMonth(d)) {
|
||||||
Spacer(Modifier.weight(1f).fillMaxHeight())
|
Spacer(Modifier.weight(1f).fillMaxHeight())
|
||||||
} else {
|
} else {
|
||||||
@@ -2186,28 +2148,9 @@ private fun MonthWeekRow(
|
|||||||
Modifier
|
Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.pointerInput(col) {
|
|
||||||
awaitEachGesture {
|
|
||||||
downY[col] = awaitFirstDown(
|
|
||||||
requireUnconsumed = false,
|
|
||||||
pass = PointerEventPass.Initial,
|
|
||||||
).position.y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = CELL_GAP, vertical = 1.dp)
|
.padding(horizontal = CELL_GAP, vertical = 1.dp)
|
||||||
.clip(CELL_SHAPE)
|
.clip(CELL_SHAPE)
|
||||||
.clickable {
|
.clickable { onOpenDay(d) },
|
||||||
val chip = week.chipAtCellY(
|
|
||||||
col = col,
|
|
||||||
cellY = downY[col],
|
|
||||||
bandTopInCell = bandTopInCell(
|
|
||||||
cellCoordinates,
|
|
||||||
bandCoordinates,
|
|
||||||
),
|
|
||||||
rowHeightPx = rowHeightPx,
|
|
||||||
)
|
|
||||||
if (chip != null) onEventClick(chip) else onOpenDay(d)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2216,38 +2159,6 @@ private fun MonthWeekRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* How far the event band sits below the top of the row's day-column box, or null
|
|
||||||
* while either is unmeasured. Read off the live coordinates rather than summed
|
|
||||||
* from the padding constants, so it can't drift from what the row actually drew.
|
|
||||||
*/
|
|
||||||
private fun bandTopInCell(
|
|
||||||
cell: Array<LayoutCoordinates?>,
|
|
||||||
band: Array<LayoutCoordinates?>,
|
|
||||||
): Float? {
|
|
||||||
val cellTop = cell[0]?.takeIf { it.isAttached }?.positionInRoot()?.y ?: return null
|
|
||||||
val bandTop = band[0]?.takeIf { it.isAttached }?.positionInRoot()?.y ?: return null
|
|
||||||
return bandTop - cellTop
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The chip at [cellY] in column [col], where [cellY] is measured from the top of
|
|
||||||
* the row's day-column box. Null for a tap above the band (the day number), on an
|
|
||||||
* empty lane, or on the overflow dots — all of which mean "open the day", the
|
|
||||||
* dots included: their point is that the day holds more than fits.
|
|
||||||
*/
|
|
||||||
internal fun MonthWeek.chipAtCellY(
|
|
||||||
col: Int,
|
|
||||||
cellY: Float,
|
|
||||||
bandTopInCell: Float?,
|
|
||||||
rowHeightPx: Float,
|
|
||||||
): EventInstance? {
|
|
||||||
if (bandTopInCell == null || rowHeightPx <= 0f) return null
|
|
||||||
val bandY = cellY - bandTopInCell
|
|
||||||
if (bandY < 0f) return null
|
|
||||||
return chipAt(col, (bandY / rowHeightPx).toInt(), MAX_EVENT_ROWS)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The row-level pickup for month chips: resolves which chip the press landed on
|
* The row-level pickup for month chips: resolves which chip the press landed on
|
||||||
* from the geometry the row just laid out, and abandons the gesture on empty
|
* from the geometry the row just laid out, and abandons the gesture on empty
|
||||||
@@ -2448,6 +2359,7 @@ private fun MonthBar(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = eventInk(fill),
|
color = eventInk(fill),
|
||||||
|
textDecoration = declinedDecoration(event.isDeclined),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ internal fun MonthStylePreview(
|
|||||||
state = sample.month,
|
state = sample.month,
|
||||||
showWeekNumbers = false,
|
showWeekNumbers = false,
|
||||||
onOpenDay = {},
|
onOpenDay = {},
|
||||||
onEventClick = {},
|
|
||||||
)
|
)
|
||||||
MonthViewStyle.Continuous -> ContinuousMonthGrid(
|
MonthViewStyle.Continuous -> ContinuousMonthGrid(
|
||||||
state = sample.continuous,
|
state = sample.continuous,
|
||||||
@@ -56,7 +55,6 @@ internal fun MonthStylePreview(
|
|||||||
),
|
),
|
||||||
showWeekNumbers = false,
|
showWeekNumbers = false,
|
||||||
onOpenDay = {},
|
onOpenDay = {},
|
||||||
onEventClick = {},
|
|
||||||
)
|
)
|
||||||
MonthViewStyle.Dense -> DenseMonthGrid(
|
MonthViewStyle.Dense -> DenseMonthGrid(
|
||||||
state = sample.continuous,
|
state = sample.continuous,
|
||||||
@@ -68,7 +66,6 @@ internal fun MonthStylePreview(
|
|||||||
),
|
),
|
||||||
showWeekNumbers = false,
|
showWeekNumbers = false,
|
||||||
onOpenDay = {},
|
onOpenDay = {},
|
||||||
onEventClick = {},
|
|
||||||
)
|
)
|
||||||
MonthViewStyle.Split -> {
|
MonthViewStyle.Split -> {
|
||||||
SplitMonthGrid(
|
SplitMonthGrid(
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import de.jeanlucmakiola.calendula.domain.SearchMonth
|
|||||||
import de.jeanlucmakiola.floret.identity.animateItemMotion
|
import de.jeanlucmakiola.floret.identity.animateItemMotion
|
||||||
import de.jeanlucmakiola.floret.identity.fadeThrough
|
import de.jeanlucmakiola.floret.identity.fadeThrough
|
||||||
import de.jeanlucmakiola.floret.identity.predictiveBack
|
import de.jeanlucmakiola.floret.identity.predictiveBack
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.declinedTitle
|
||||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||||
import de.jeanlucmakiola.floret.components.InlineTextField
|
import de.jeanlucmakiola.floret.components.InlineTextField
|
||||||
import de.jeanlucmakiola.floret.components.Position
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
@@ -519,7 +520,7 @@ private fun SearchResultRow(
|
|||||||
// Faded like a past event anywhere else in the app — search reaches back
|
// Faded like a past event anywhere else in the app — search reaches back
|
||||||
// through the whole history.
|
// through the whole history.
|
||||||
modifier = if (hit.isPast) modifier.alpha(EventDimAlpha) else modifier,
|
modifier = if (hit.isPast) modifier.alpha(EventDimAlpha) else modifier,
|
||||||
title = marked(event.title, hit.titleSpans, highlight),
|
title = declinedTitle(marked(event.title, hit.titleSpans, highlight), event.isDeclined),
|
||||||
summary = searchSummary(hit, highlight),
|
summary = searchSummary(hit, highlight),
|
||||||
position = position,
|
position = position,
|
||||||
minHeight = 64.dp,
|
minHeight = 64.dp,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.declinedDecoration
|
||||||
import de.jeanlucmakiola.calendula.ui.common.BlockTimeLabel
|
import de.jeanlucmakiola.calendula.ui.common.BlockTimeLabel
|
||||||
import de.jeanlucmakiola.calendula.ui.common.animatedBlockPlacement
|
import de.jeanlucmakiola.calendula.ui.common.animatedBlockPlacement
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ghostAlpha
|
import de.jeanlucmakiola.calendula.ui.common.ghostAlpha
|
||||||
@@ -129,7 +130,6 @@ import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.GUTTER_CONTENT_START_INSET
|
import de.jeanlucmakiola.calendula.ui.common.GUTTER_CONTENT_START_INSET
|
||||||
import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH
|
import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH
|
||||||
import de.jeanlucmakiola.calendula.ui.common.HourGutter
|
import de.jeanlucmakiola.calendula.ui.common.HourGutter
|
||||||
import de.jeanlucmakiola.calendula.ui.common.TIMELINE_CONTENT_END_INSET
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
|
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.tappedMinuteOfDay
|
import de.jeanlucmakiola.calendula.ui.common.tappedMinuteOfDay
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
@@ -507,7 +507,7 @@ private fun WeekDayHeader(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 4.dp, bottom = 8.dp, end = TIMELINE_CONTENT_END_INSET),
|
.padding(top = 4.dp, bottom = 8.dp),
|
||||||
) {
|
) {
|
||||||
// Mirror the day-column layout (empty weekday line + spacer) so the
|
// Mirror the day-column layout (empty weekday line + spacer) so the
|
||||||
// badge lines up vertically with the date numbers. The start inset centres
|
// badge lines up vertically with the date numbers. The start inset centres
|
||||||
@@ -603,11 +603,7 @@ private fun AllDayStrip(
|
|||||||
// Height is hoisted + animated so it slides and resizes smoothly;
|
// Height is hoisted + animated so it slides and resizes smoothly;
|
||||||
// padding sits inside it so the content area is lanes * row height.
|
// padding sits inside it so the content area is lanes * row height.
|
||||||
.height(height)
|
.height(height)
|
||||||
.padding(
|
.padding(vertical = ALL_DAY_VERTICAL_PADDING),
|
||||||
top = ALL_DAY_VERTICAL_PADDING,
|
|
||||||
bottom = ALL_DAY_VERTICAL_PADDING,
|
|
||||||
end = TIMELINE_CONTENT_END_INSET,
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
// Keep the gutter-width offset so the bars line up with the day columns.
|
// Keep the gutter-width offset so the bars line up with the day columns.
|
||||||
Spacer(Modifier.width(GUTTER_WIDTH))
|
Spacer(Modifier.width(GUTTER_WIDTH))
|
||||||
@@ -667,6 +663,7 @@ private fun AllDayBar(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = eventInk(fill),
|
color = eventInk(fill),
|
||||||
|
textDecoration = declinedDecoration(event.isDeclined),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -713,7 +710,6 @@ private fun Timeline(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(end = TIMELINE_CONTENT_END_INSET)
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.onGloballyPositioned { dragController.geometry.viewport = it },
|
.onGloballyPositioned { dragController.geometry.viewport = it },
|
||||||
@@ -946,6 +942,7 @@ private fun EventBlock(
|
|||||||
maxLines = titleMaxLines,
|
maxLines = titleMaxLines,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = 0.85f),
|
||||||
|
textDecoration = declinedDecoration(block.event.isDeclined),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showTime) {
|
if (showTime) {
|
||||||
@@ -983,12 +980,7 @@ private fun WeekLoading() {
|
|||||||
// Same scale resolution as the loaded timeline, so the skeleton's
|
// Same scale resolution as the loaded timeline, so the skeleton's
|
||||||
// columns don't resize the moment the real week arrives.
|
// columns don't resize the moment the real week arrives.
|
||||||
val totalHeight = scale.hourHeight(maxHeight) * 24
|
val totalHeight = scale.hourHeight(maxHeight) * 24
|
||||||
Row(
|
Row(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(end = TIMELINE_CONTENT_END_INSET)
|
|
||||||
.verticalScroll(scrollState),
|
|
||||||
) {
|
|
||||||
Spacer(Modifier.width(GUTTER_WIDTH))
|
Spacer(Modifier.width(GUTTER_WIDTH))
|
||||||
repeat(7) {
|
repeat(7) {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.Build
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.glance.GlanceTheme
|
import androidx.glance.GlanceTheme
|
||||||
import androidx.glance.material3.ColorProviders
|
import androidx.glance.material3.ColorProviders
|
||||||
|
import androidx.glance.text.TextDecoration
|
||||||
import de.jeanlucmakiola.calendula.ui.theme.CalendulaDarkFallback
|
import de.jeanlucmakiola.calendula.ui.theme.CalendulaDarkFallback
|
||||||
import de.jeanlucmakiola.calendula.ui.theme.CalendulaLightFallback
|
import de.jeanlucmakiola.calendula.ui.theme.CalendulaLightFallback
|
||||||
|
|
||||||
@@ -34,3 +35,10 @@ fun CalendulaGlanceTheme(content: @Composable () -> Unit) {
|
|||||||
}
|
}
|
||||||
GlanceTheme(colors = colors, content = content)
|
GlanceTheme(colors = colors, content = content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Glance's counterpart to the app's `declinedDecoration`: a declined invitation
|
||||||
|
* reads the same on the home screen as it does inside the app (#180).
|
||||||
|
*/
|
||||||
|
fun glanceDeclinedDecoration(isDeclined: Boolean): TextDecoration =
|
||||||
|
if (isDeclined) TextDecoration.LineThrough else TextDecoration.None
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.jeanlucmakiola.calendula.widget.agenda
|
package de.jeanlucmakiola.calendula.widget.agenda
|
||||||
|
|
||||||
|
import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -24,6 +25,7 @@ import androidx.glance.appwidget.cornerRadius
|
|||||||
import androidx.glance.appwidget.lazy.LazyColumn
|
import androidx.glance.appwidget.lazy.LazyColumn
|
||||||
import androidx.glance.appwidget.lazy.items
|
import androidx.glance.appwidget.lazy.items
|
||||||
import androidx.glance.appwidget.provideContent
|
import androidx.glance.appwidget.provideContent
|
||||||
|
import androidx.glance.appwidget.updateAll
|
||||||
import androidx.glance.background
|
import androidx.glance.background
|
||||||
import androidx.glance.currentState
|
import androidx.glance.currentState
|
||||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||||
@@ -134,14 +136,10 @@ class AgendaWidget : GlanceAppWidget() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Re-reads the calendar and redraws the widget (header refresh button). */
|
||||||
* Re-reads the calendar and redraws the widget (header refresh button). Targets
|
|
||||||
* the tapped widget's own id rather than `updateAll`, whose provider-name lookup
|
|
||||||
* is empty in a process a tap woke from cold — see `ShiftMonthAction` (#18).
|
|
||||||
*/
|
|
||||||
class RefreshAgendaAction : ActionCallback {
|
class RefreshAgendaAction : ActionCallback {
|
||||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||||
AgendaWidget().update(context.applicationContext, glanceId)
|
AgendaWidget().updateAll(context.applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +417,11 @@ private fun EventRow(
|
|||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = TextStyle(color = titleColor, fontSize = metrics.eventTitle),
|
style = TextStyle(
|
||||||
|
color = titleColor,
|
||||||
|
fontSize = metrics.eventTitle,
|
||||||
|
textDecoration = glanceDeclinedDecoration(event.isDeclined),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = eventTimeSummary(context, event, day, is24Hour),
|
text = eventTimeSummary(context, event, day, is24Hour),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.jeanlucmakiola.calendula.widget.month
|
package de.jeanlucmakiola.calendula.widget.month
|
||||||
|
|
||||||
|
import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -25,6 +26,7 @@ import androidx.glance.appwidget.action.actionStartActivity
|
|||||||
import androidx.glance.appwidget.cornerRadius
|
import androidx.glance.appwidget.cornerRadius
|
||||||
import androidx.glance.appwidget.provideContent
|
import androidx.glance.appwidget.provideContent
|
||||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||||
|
import androidx.glance.appwidget.updateAll
|
||||||
import androidx.glance.background
|
import androidx.glance.background
|
||||||
import androidx.glance.currentState
|
import androidx.glance.currentState
|
||||||
import androidx.glance.layout.Alignment
|
import androidx.glance.layout.Alignment
|
||||||
@@ -117,17 +119,7 @@ class MonthWidget : GlanceAppWidget() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Step the displayed month by the `delta` action parameter (±1). */
|
||||||
* Step the displayed month by the `delta` action parameter (±1).
|
|
||||||
*
|
|
||||||
* Redrawn through [GlanceAppWidget.update] with the id the callback was handed,
|
|
||||||
* never `updateAll`. `updateAll` resolves its targets through the provider-name
|
|
||||||
* -> app-widget-id map Glance persists for the *receiver*, and in a process that
|
|
||||||
* has done nothing else yet — the one a tap wakes after a reboot — that lookup
|
|
||||||
* comes back empty and the redraw is dropped: the state write lands, nothing
|
|
||||||
* recomposes, and the arrows read as dead until something else opens the app and
|
|
||||||
* starts a session (#18). The tapped widget's own id needs no lookup.
|
|
||||||
*/
|
|
||||||
class ShiftMonthAction : ActionCallback {
|
class ShiftMonthAction : ActionCallback {
|
||||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||||
val delta = parameters[deltaKey] ?: 0
|
val delta = parameters[deltaKey] ?: 0
|
||||||
@@ -135,7 +127,7 @@ class ShiftMonthAction : ActionCallback {
|
|||||||
val cur = prefs[MONTH_INDEX_KEY] ?: currentMonthIndex(systemZone())
|
val cur = prefs[MONTH_INDEX_KEY] ?: currentMonthIndex(systemZone())
|
||||||
prefs[MONTH_INDEX_KEY] = cur + delta
|
prefs[MONTH_INDEX_KEY] = cur + delta
|
||||||
}
|
}
|
||||||
MonthWidget().update(context.applicationContext, glanceId)
|
MonthWidget().updateAll(context.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -143,11 +135,11 @@ class ShiftMonthAction : ActionCallback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Jump the displayed month back to the current month. See [ShiftMonthAction]. */
|
/** Jump the displayed month back to the current month. */
|
||||||
class ResetMonthAction : ActionCallback {
|
class ResetMonthAction : ActionCallback {
|
||||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||||
updateAppWidgetState(context, glanceId) { prefs -> prefs.remove(MONTH_INDEX_KEY) }
|
updateAppWidgetState(context, glanceId) { prefs -> prefs.remove(MONTH_INDEX_KEY) }
|
||||||
MonthWidget().update(context.applicationContext, glanceId)
|
MonthWidget().updateAll(context.applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +415,11 @@ private fun SpanBar(event: EventInstance, dark: Boolean, soften: Boolean, width:
|
|||||||
Text(
|
Text(
|
||||||
text = event.title.ifBlank { context.getString(R.string.event_untitled) },
|
text = event.title.ifBlank { context.getString(R.string.event_untitled) },
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = TextStyle(color = ColorProvider(eventInk(fill)), fontSize = 9.sp),
|
style = TextStyle(
|
||||||
|
color = ColorProvider(eventInk(fill)),
|
||||||
|
fontSize = 9.sp,
|
||||||
|
textDecoration = glanceDeclinedDecoration(event.isDeclined),
|
||||||
|
),
|
||||||
modifier = GlanceModifier.padding(horizontal = 3.dp),
|
modifier = GlanceModifier.padding(horizontal = 3.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.jeanlucmakiola.calendula.data.calendar
|
package de.jeanlucmakiola.calendula.data.calendar
|
||||||
|
|
||||||
|
import android.provider.CalendarContract
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -17,6 +18,7 @@ class InstanceMapperTest {
|
|||||||
eventColor: Any? = null,
|
eventColor: Any? = null,
|
||||||
calendarColor: Int = 0xFFAABBCC.toInt(),
|
calendarColor: Int = 0xFFAABBCC.toInt(),
|
||||||
location: String? = null,
|
location: String? = null,
|
||||||
|
selfAttendeeStatus: Int = CalendarContract.Attendees.ATTENDEE_STATUS_NONE,
|
||||||
): MapColumnReader = MapColumnReader(
|
): MapColumnReader = MapColumnReader(
|
||||||
InstanceProjection.IDX_INSTANCE_ID to instanceId,
|
InstanceProjection.IDX_INSTANCE_ID to instanceId,
|
||||||
InstanceProjection.IDX_EVENT_ID to eventId,
|
InstanceProjection.IDX_EVENT_ID to eventId,
|
||||||
@@ -28,6 +30,7 @@ class InstanceMapperTest {
|
|||||||
InstanceProjection.IDX_EVENT_COLOR to eventColor,
|
InstanceProjection.IDX_EVENT_COLOR to eventColor,
|
||||||
InstanceProjection.IDX_CALENDAR_COLOR to calendarColor,
|
InstanceProjection.IDX_CALENDAR_COLOR to calendarColor,
|
||||||
InstanceProjection.IDX_LOCATION to location,
|
InstanceProjection.IDX_LOCATION to location,
|
||||||
|
InstanceProjection.IDX_SELF_ATTENDEE_STATUS to selfAttendeeStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -90,4 +93,20 @@ class InstanceMapperTest {
|
|||||||
val inst = reader(location = "Berlin").toEventInstance()
|
val inst = reader(location = "Berlin").toEventInstance()
|
||||||
assertThat(inst!!.location).isEqualTo("Berlin")
|
assertThat(inst!!.location).isEqualTo("Berlin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a declined invitation is marked, any other answer is not`() {
|
||||||
|
assertThat(reader().toEventInstance()!!.isDeclined).isFalse()
|
||||||
|
assertThat(
|
||||||
|
reader(selfAttendeeStatus = CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED)
|
||||||
|
.toEventInstance()!!.isDeclined,
|
||||||
|
).isTrue()
|
||||||
|
listOf(
|
||||||
|
CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED,
|
||||||
|
CalendarContract.Attendees.ATTENDEE_STATUS_TENTATIVE,
|
||||||
|
CalendarContract.Attendees.ATTENDEE_STATUS_INVITED,
|
||||||
|
).forEach { status ->
|
||||||
|
assertThat(reader(selfAttendeeStatus = status).toEventInstance()!!.isDeclined).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.month
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
|
||||||
import kotlinx.datetime.DateTimeUnit
|
|
||||||
import kotlinx.datetime.DayOfWeek
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.Month
|
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlinx.datetime.YearMonth
|
|
||||||
import kotlinx.datetime.atTime
|
|
||||||
import kotlinx.datetime.plus
|
|
||||||
import kotlinx.datetime.toInstant
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Which chip a tap in a month cell lands on (#187) — the geometry the tap layer
|
|
||||||
* uses to tell "open this event" from "open this day", given that the chips take
|
|
||||||
* no pointer input of their own.
|
|
||||||
*/
|
|
||||||
class ChipAtCellYTest {
|
|
||||||
|
|
||||||
private val zone = TimeZone.UTC
|
|
||||||
private val jul26 = YearMonth(2026, Month.JULY)
|
|
||||||
|
|
||||||
/** Band starts 40px down the cell; each lane is 20px tall. */
|
|
||||||
private val bandTop = 40f
|
|
||||||
private val laneHeight = 20f
|
|
||||||
|
|
||||||
/** July 2026 starts on a Wednesday, so this row — Jul 6–12 — sits wholly inside it. */
|
|
||||||
private fun rowOfJuly6(events: List<EventInstance>) =
|
|
||||||
layoutMonthWeeks(jul26, DayOfWeek.MONDAY, events, zone)[1]
|
|
||||||
|
|
||||||
private fun allDay(from: LocalDate, toInclusive: LocalDate, id: Long) = EventInstance(
|
|
||||||
instanceId = id,
|
|
||||||
eventId = id,
|
|
||||||
calendarId = 1L,
|
|
||||||
title = "A$id",
|
|
||||||
start = from.atTime(0, 0).toInstant(zone),
|
|
||||||
end = toInclusive.plus(1, DateTimeUnit.DAY).atTime(0, 0).toInstant(zone),
|
|
||||||
isAllDay = true,
|
|
||||||
color = 0xFF2196F3.toInt(),
|
|
||||||
location = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun timed(date: LocalDate, hour: Int, id: Long) = EventInstance(
|
|
||||||
instanceId = id,
|
|
||||||
eventId = id,
|
|
||||||
calendarId = 1L,
|
|
||||||
title = "T$id",
|
|
||||||
start = date.atTime(hour, 0).toInstant(zone),
|
|
||||||
end = date.atTime(hour + 1, 0).toInstant(zone),
|
|
||||||
isAllDay = false,
|
|
||||||
color = 0xFFF44336.toInt(),
|
|
||||||
location = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun MonthWeek.chipAt(col: Int, cellY: Float) =
|
|
||||||
chipAtCellY(col = col, cellY = cellY, bandTopInCell = bandTop, rowHeightPx = laneHeight)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a tap on a lane resolves to the chip seated there`() {
|
|
||||||
val bar = allDay(LocalDate(2026, 7, 7), LocalDate(2026, 7, 9), id = 1L)
|
|
||||||
val meeting = timed(LocalDate(2026, 7, 7), hour = 9, id = 2L)
|
|
||||||
val week = rowOfJuly6(listOf(bar, meeting))
|
|
||||||
|
|
||||||
// Jul 7 is column 1 of a Monday-anchored row starting Jul 6.
|
|
||||||
assertThat(week.chipAt(col = 1, cellY = bandTop + 5f)?.eventId).isEqualTo(1L)
|
|
||||||
assertThat(week.chipAt(col = 1, cellY = bandTop + laneHeight + 5f)?.eventId).isEqualTo(2L)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a multi-day bar answers on every column it covers`() {
|
|
||||||
val bar = allDay(LocalDate(2026, 7, 7), LocalDate(2026, 7, 9), id = 1L)
|
|
||||||
val week = rowOfJuly6(listOf(bar))
|
|
||||||
|
|
||||||
(1..3).forEach { col ->
|
|
||||||
assertThat(week.chipAt(col = col, cellY = bandTop + 5f)?.eventId).isEqualTo(1L)
|
|
||||||
}
|
|
||||||
// Jul 10 is past the bar's last day.
|
|
||||||
assertThat(week.chipAt(col = 4, cellY = bandTop + 5f)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a tap above the band is the day number, not a chip`() {
|
|
||||||
val week = rowOfJuly6(listOf(timed(LocalDate(2026, 7, 7), hour = 9, id = 2L)))
|
|
||||||
|
|
||||||
assertThat(week.chipAt(col = 1, cellY = bandTop - 1f)).isNull()
|
|
||||||
assertThat(week.chipAt(col = 1, cellY = 0f)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a tap on an empty lane of a day that has chips falls through to the day`() {
|
|
||||||
val week = rowOfJuly6(listOf(timed(LocalDate(2026, 7, 7), hour = 9, id = 2L)))
|
|
||||||
|
|
||||||
assertThat(week.chipAt(col = 1, cellY = bandTop + laneHeight * 2 + 5f)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a tap on the overflow row opens the day rather than a hidden event`() {
|
|
||||||
val events = (1..MAX_EVENT_ROWS + 2).map {
|
|
||||||
timed(LocalDate(2026, 7, 7), hour = it, id = it.toLong())
|
|
||||||
}
|
|
||||||
val week = rowOfJuly6(events)
|
|
||||||
|
|
||||||
// The dots sit one lane below the last one the row draws.
|
|
||||||
val overflowY = bandTop + laneHeight * MAX_EVENT_ROWS + 2f
|
|
||||||
assertThat(week.chipAt(col = 1, cellY = overflowY)).isNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `unmeasured geometry resolves to no chip`() {
|
|
||||||
val week = rowOfJuly6(listOf(timed(LocalDate(2026, 7, 7), hour = 9, id = 2L)))
|
|
||||||
|
|
||||||
assertThat(
|
|
||||||
week.chipAtCellY(col = 1, cellY = 45f, bandTopInCell = null, rowHeightPx = laneHeight),
|
|
||||||
).isNull()
|
|
||||||
assertThat(
|
|
||||||
week.chipAtCellY(col = 1, cellY = 45f, bandTopInCell = bandTop, rowHeightPx = 0f),
|
|
||||||
).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user