diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt index c038dee..a5fcb2f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt @@ -198,7 +198,7 @@ private fun CustomReminderEditor( /** A short explanatory paragraph shown under a picker's title, above the rows. */ @Composable -private fun PickerDescription(text: String) { +internal fun PickerDescription(text: String) { Text( text = text, style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index 6a5e821..d6100b7 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -454,7 +454,7 @@ private fun MonthTopBar( } @Composable -private fun WeekdayHeader(weekStart: DayOfWeek, showWeekNumbers: Boolean) { +internal fun WeekdayHeader(weekStart: DayOfWeek, showWeekNumbers: Boolean) { val locale = currentLocale() val days = remember(weekStart, locale) { (0 until 7).map { offset -> @@ -503,7 +503,7 @@ private const val MAX_EVENT_ROWS = 3 private val CONTINUOUS_ROW_HEIGHT = 112.dp @Composable -private fun MonthGrid( +internal fun MonthGrid( state: MonthUiState.Success, showWeekNumbers: Boolean, onOpenDay: (LocalDate) -> Unit, @@ -545,7 +545,7 @@ private fun MonthGrid( * duplication #38 asks to be rid of. */ @Composable -private fun ContinuousMonthGrid( +internal fun ContinuousMonthGrid( state: ContinuousMonthUiState.Success, listState: LazyListState, showWeekNumbers: Boolean, @@ -682,7 +682,7 @@ private const val SPLIT_MAX_DOTS = 3 * the layout. The full Day view stays one tap away on the pane's date header. */ @Composable -private fun SplitMonthGrid( +internal fun SplitMonthGrid( state: MonthUiState.Success, selected: LocalDate, showWeekNumbers: Boolean, @@ -812,7 +812,7 @@ private fun SplitDots(events: List, dark: Boolean) { * the Week and Agenda headers (#37). */ @Composable -private fun SplitDayPane( +internal fun SplitDayPane( date: LocalDate, today: LocalDate, events: List, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthStylePreview.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthStylePreview.kt new file mode 100644 index 0000000..b27b867 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthStylePreview.kt @@ -0,0 +1,268 @@ +package de.jeanlucmakiola.calendula.ui.month + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.layout.layout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.jeanlucmakiola.calendula.domain.EventInstance +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.YearMonth +import kotlinx.datetime.atTime +import kotlinx.datetime.plus +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock +import kotlin.math.roundToInt + +/** + * A live, scaled-down Month view in a given [MonthViewStyle], for the settings + * chooser. + * + * It renders the *real* grid composables rather than a drawing of them, so the + * preview cannot drift from the thing it depicts: change a cell's shape or an + * event bar's colour and every preview follows automatically. The trick is + * [requiredSize] — it ignores the incoming constraints, so the grid lays itself + * out at a full phone width and a plausible viewport height, and a + * [graphicsLayer] scale shrinks the finished layout into the card. + * + * The month, today's position and the week start are all real; only the events + * are stand-ins, since the settings screen has no business querying the + * provider for a thumbnail. + */ +@Composable +internal fun MonthStylePreview( + style: MonthViewStyle, + weekStart: DayOfWeek, + height: Dp, + modifier: Modifier = Modifier, +) { + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val scale = height.value / VIRTUAL_HEIGHT.value + val zone = remember { TimeZone.currentSystemDefault() } + val today = remember(zone) { Clock.System.now().toLocalDateTime(zone).date } + val sample = remember(today, weekStart, zone) { sampleMonthState(today, weekStart, zone) } + + Box( + modifier = modifier + .clipToBounds() + .background(MaterialTheme.colorScheme.surface) + // A preview is a picture, not a control: swallow touches before the + // grid's own clickables see them, and give screen readers one label + // instead of six weeks of day cells. + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + awaitPointerEvent(PointerEventPass.Initial).changes + .forEach { it.consume() } + } + } + } + .clearAndSetSemantics { } + // Measure the grid at a full phone viewport but report the scaled + // size, so the node occupies exactly what it draws. + // + // Modifier.requiredSize would be the obvious way to force the larger + // measurement, but it *centres* content that overflows the incoming + // constraints — which pushed the grid to a negative offset and left + // only its bottom-right corner inside the clip. + .layout { measurable, _ -> + val fullWidth = screenWidth.roundToPx() + val fullHeight = VIRTUAL_HEIGHT.roundToPx() + val placeable = measurable.measure(Constraints.fixed(fullWidth, fullHeight)) + layout((fullWidth * scale).roundToInt(), (fullHeight * scale).roundToInt()) { + placeable.place(0, 0) + } + } + .graphicsLayer { + scaleX = scale + scaleY = scale + transformOrigin = TransformOrigin(0f, 0f) + }, + ) { + Column(Modifier.fillMaxSize()) { + WeekdayHeader(weekStart = weekStart, showWeekNumbers = false) + when (style) { + MonthViewStyle.Paged -> MonthGrid( + state = sample.month, + showWeekNumbers = false, + onOpenDay = {}, + ) + MonthViewStyle.Continuous -> ContinuousMonthGrid( + state = sample.continuous, + listState = rememberLazyListState( + initialFirstVisibleItemIndex = + weekIndexOf(LocalDate(today.year, today.month, 1), weekStart), + ), + showWeekNumbers = false, + onOpenDay = {}, + ) + MonthViewStyle.Split -> { + SplitMonthGrid( + state = sample.month, + selected = today, + showWeekNumbers = false, + onSelectDay = {}, + ) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + SplitDayPane( + date = today, + today = today, + events = sample.month.instancesByDay[today].orEmpty(), + zone = zone, + onOpenDay = {}, + onEventClick = {}, + onCreateEvent = {}, + modifier = Modifier.fillMaxWidth().height(SPLIT_PANE_HEIGHT), + ) + } + } + } + } +} + +/** + * The viewport the preview pretends to be: a phone's width by the height a + * calendar view gets under the top bar. Scaling from a fixed height keeps the + * three styles comparable — each shows the same slice of screen. + */ +private val VIRTUAL_HEIGHT = 440.dp +private val SPLIT_PANE_HEIGHT = 170.dp + +private class SampleMonth( + val month: MonthUiState.Success, + val continuous: ContinuousMonthUiState.Success, +) + +/** + * A month's worth of stand-in events, laid out through the same + * [layoutMonthWeeks] / [layoutCalendarWeek] the live views use — so the preview + * exercises the real span, lane and overflow logic rather than approximating it. + * + * Colours are raw ARGB on purpose: that is what the provider hands out for an + * event, so a token here would misrepresent what the grid actually renders. + */ +private fun sampleMonthState( + today: LocalDate, + weekStart: DayOfWeek, + zone: TimeZone, +): SampleMonth { + val ym = YearMonth(today.year, today.month) + val first = LocalDate(ym.year, ym.month, 1) + val events = sampleEvents(first, today, zone) + + val weeks = layoutMonthWeeks(ym, weekStart, events, zone) + val month = MonthUiState.Success( + month = ym, + today = today, + weeks = weeks, + instancesByDay = instancesByDay(weeks.flatMap { it.days }, events, zone), + zone = zone, + ) + + // Enough weeks either side of today for the continuous list to fill the + // viewport and still have somewhere to scroll. + val centre = weekIndexOf(today, weekStart) + val window = (centre - 8)..(centre + 8) + val continuous = ContinuousMonthUiState.Success( + today = today, + weeksByIndex = window.associateWith { index -> + val days = (0 until 7).map { + weekStartForIndex(index, weekStart).plus(it, DateTimeUnit.DAY) + } + layoutCalendarWeek(days, events, zone) + }, + weekStart = weekStart, + ) + return SampleMonth(month, continuous) +} + +private val SAMPLE_COLORS = listOf( + 0xFF3F7BD4.toInt(), + 0xFFCE5B4C.toInt(), + 0xFF4E9A6A.toInt(), + 0xFF8A63C7.toInt(), +) + +private fun sampleEvents( + firstOfMonth: LocalDate, + today: LocalDate, + zone: TimeZone, +): List { + var id = 0L + fun next() = ++id + + fun timed(day: LocalDate, hour: Int, length: Int, colorIndex: Int) = EventInstance( + instanceId = next(), + eventId = id, + calendarId = 1L, + title = SAMPLE_TITLES[(id.toInt() - 1) % SAMPLE_TITLES.size], + start = day.atTime(hour, 0).toInstant(zone), + end = day.atTime(hour + length, 0).toInstant(zone), + isAllDay = false, + color = SAMPLE_COLORS[colorIndex % SAMPLE_COLORS.size], + location = null, + ) + + fun allDay(from: LocalDate, days: Int, colorIndex: Int) = EventInstance( + instanceId = next(), + eventId = id, + calendarId = 1L, + title = SAMPLE_TITLES[(id.toInt() - 1) % SAMPLE_TITLES.size], + // All-day events sit at UTC midnights with an exclusive end. + start = from.atTime(0, 0).toInstant(TimeZone.UTC), + end = from.plus(days, DateTimeUnit.DAY).atTime(0, 0).toInstant(TimeZone.UTC), + isAllDay = true, + color = SAMPLE_COLORS[colorIndex % SAMPLE_COLORS.size], + location = null, + ) + + // Spread across the month so most weeks carry something, with one multi-day + // bar to show a span bridging cells and a busy today for the split pane. + return buildList { + add(allDay(firstOfMonth.plus(9, DateTimeUnit.DAY), days = 3, colorIndex = 2)) + add(timed(firstOfMonth.plus(1, DateTimeUnit.DAY), 9, 1, 0)) + add(timed(firstOfMonth.plus(4, DateTimeUnit.DAY), 14, 2, 1)) + add(timed(firstOfMonth.plus(7, DateTimeUnit.DAY), 11, 1, 3)) + add(timed(firstOfMonth.plus(15, DateTimeUnit.DAY), 10, 1, 0)) + add(timed(firstOfMonth.plus(18, DateTimeUnit.DAY), 16, 1, 2)) + add(timed(firstOfMonth.plus(22, DateTimeUnit.DAY), 8, 2, 1)) + add(timed(firstOfMonth.plus(25, DateTimeUnit.DAY), 13, 1, 3)) + // Today, so the split pane's list and the grid's dots both have content. + add(timed(today, 9, 1, 0)) + add(timed(today, 12, 1, 1)) + add(timed(today, 15, 2, 3)) + } +} + +private val SAMPLE_TITLES = listOf( + "Standup", + "Lunch", + "Review", + "Gym", + "Call", + "Workshop", + "Dentist", + "Trip", +) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/MonthViewStylePicker.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/MonthViewStylePicker.kt index ffb7a8e..e5802c6 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/MonthViewStylePicker.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/MonthViewStylePicker.kt @@ -1,297 +1,109 @@ package de.jeanlucmakiola.calendula.ui.settings +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import de.jeanlucmakiola.calendula.R +import de.jeanlucmakiola.calendula.ui.common.PickerDescription +import de.jeanlucmakiola.calendula.ui.month.MonthStylePreview import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle import de.jeanlucmakiola.calendula.ui.month.descriptionRes import de.jeanlucmakiola.calendula.ui.month.labelRes import de.jeanlucmakiola.floret.components.FullScreenPicker +import de.jeanlucmakiola.floret.components.GroupedRow +import de.jeanlucmakiola.floret.components.SelectedCheck +import de.jeanlucmakiola.floret.components.positionOf +import de.jeanlucmakiola.floret.identity.rememberReduceMotion +import kotlinx.datetime.DayOfWeek /** - * The Month view style chooser (#38, #53). Each option carries a schematic of the - * layout it produces rather than a bare label — the three differ in *shape*, which - * a word like "Continuous" doesn't convey on its own. + * The Month view style chooser (#38, #53). * - * Tapping applies immediately and the picker stays open, matching the App name - * picker: the change is worth seeing land before leaving. + * A live, scaled-down Month view sits above the options and changes as you pick + * one, so the choice is made by looking rather than by reading "Continuous" and + * guessing. Selecting therefore applies immediately and leaves the picker open — + * closing on tap would hide the very thing the screen is for. Back exits, the + * same as the App name picker, which stays open for the same reason. + * + * Below the preview it is the family's standard picker shape: connected grouped + * rows with a tonal highlight and [SelectedCheck] on the current one. */ @Composable internal fun MonthViewStylePicker( selected: MonthViewStyle, + weekStart: DayOfWeek, onSelect: (MonthViewStyle) -> Unit, onDismiss: () -> Unit, ) { + val options = MonthViewStyle.entries + val reduceMotion = rememberReduceMotion() FullScreenPicker( title = stringResource(R.string.settings_month_view_style), onDismiss = onDismiss, predictiveBack = true, ) { - Text( - text = stringResource(R.string.settings_month_view_style_summary), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - ) - Spacer(Modifier.height(24.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .selectableGroup(), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - MonthViewStyle.entries.forEach { style -> - MonthStyleOptionCard( - style = style, - selected = style == selected, - onClick = { onSelect(style) }, - ) - } - } - } -} - -/** - * One selectable style: its schematic, name and a line on what it does. Selection - * reads three ways over — border weight, container tint and a check — so it never - * rests on colour alone. - */ -@Composable -private fun MonthStyleOptionCard( - style: MonthViewStyle, - selected: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val shape = RoundedCornerShape(24.dp) - val borderColor = if (selected) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.outlineVariant - } - val containerColor = if (selected) { - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) - } else { - MaterialTheme.colorScheme.surfaceContainerHigh - } - Row( - modifier = modifier - .fillMaxWidth() - .clip(shape) - .background(containerColor) - .border(width = if (selected) 2.dp else 1.dp, color = borderColor, shape = shape) - .selectable(selected = selected, role = Role.RadioButton, onClick = onClick) - .padding(all = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - MonthStyleSchematic(style) - Column(modifier = Modifier.weight(1f)) { - Text( - text = stringResource(style.labelRes), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = stringResource(style.descriptionRes), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - // Selection indicator: a filled check when active, an empty ring otherwise. Box( modifier = Modifier - .size(24.dp) - .clip(CircleShape) - .background(if (selected) MaterialTheme.colorScheme.primary else Color.Transparent) - .then( - if (selected) { - Modifier - } else { - Modifier.border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape) - }, - ), + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .height(PREVIEW_HEIGHT), contentAlignment = Alignment.Center, ) { - if (selected) { - Icon( - imageVector = Icons.Filled.Check, - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.size(16.dp), - ) + Crossfade( + targetState = selected, + animationSpec = if (reduceMotion) snap() else tween(durationMillis = 250), + label = "month-style-preview", + ) { shown -> + // The preview renders the real grid at phone size and scales it + // down, so its own corners are square — the frame rounds it. + Box( + modifier = Modifier + .clip(PREVIEW_SHAPE) + .background(MaterialTheme.colorScheme.surface) + .clipToBounds(), + ) { + MonthStylePreview( + style = shown, + weekStart = weekStart, + height = PREVIEW_HEIGHT, + ) + } } } - } -} - -private val SCHEMATIC_WIDTH = 64.dp -private val SCHEMATIC_HEIGHT = 60.dp -private val SCHEMATIC_SHAPE = RoundedCornerShape(10.dp) - -/** - * A miniature of the layout, drawn from plain boxes on theme tokens — small - * enough to read as an icon, literal enough that the three are told apart at a - * glance: pages sit side by side, the continuous stream runs off both edges, the - * split style stacks a dotted grid over a list. - */ -@Composable -private fun MonthStyleSchematic(style: MonthViewStyle, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(width = SCHEMATIC_WIDTH, height = SCHEMATIC_HEIGHT) - .clip(SCHEMATIC_SHAPE) - .background(MaterialTheme.colorScheme.surfaceContainerLowest) - .clipToBounds(), - contentAlignment = Alignment.Center, - ) { - when (style) { - MonthViewStyle.Paged -> PagedSchematic() - MonthViewStyle.Continuous -> ContinuousSchematic() - MonthViewStyle.Split -> SplitSchematic() - } - } -} - -/** Two pages side by side, the second peeking in — a swipe away. */ -@Composable -private fun PagedSchematic() { - Row( - modifier = Modifier.fillMaxWidth().padding(start = 6.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - SchematicGrid(rows = 4, modifier = Modifier.weight(1f)) - // The next page, cropped by the container — the swipe affordance. - SchematicGrid(rows = 4, modifier = Modifier.width(20.dp), dim = true) - } -} - -/** One column of weeks running off both edges: no page breaks, no repeated days. */ -@Composable -private fun ContinuousSchematic() { - Column( - modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - // Six rows in a 60dp box overflow deliberately: the stream is clipped top - // and bottom rather than fitting neatly, which is the whole idea. - repeat(6) { index -> - SchematicWeekRow(dim = index == 0 || index == 5) - } - } -} - -/** A squished dotted grid over the selected day's list. */ -@Composable -private fun SplitSchematic() { - Column( - modifier = Modifier.fillMaxWidth().padding(6.dp), - verticalArrangement = Arrangement.spacedBy(5.dp), - ) { - SchematicGrid(rows = 3, dotted = true) - // The day pane: two stand-in event rows. - repeat(2) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(6.dp) - .clip(RoundedCornerShape(3.dp)) - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.55f)), + PickerDescription(stringResource(R.string.settings_month_view_style_summary)) + options.forEachIndexed { index, style -> + val isSelected = style == selected + GroupedRow( + title = stringResource(style.labelRes), + summary = stringResource(style.descriptionRes), + position = positionOf(index, options.size), + selected = isSelected, + trailing = if (isSelected) { + { SelectedCheck() } + } else { + null + }, + // Applies straight away; the preview above is the confirmation, + // so there is nothing to dismiss for. + onClick = { onSelect(style) }, ) } } } -/** [rows] week rows of seven cells; [dotted] shrinks the cells to event dots. */ -@Composable -private fun SchematicGrid( - rows: Int, - modifier: Modifier = Modifier, - dim: Boolean = false, - dotted: Boolean = false, -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(if (dotted) 4.dp else 3.dp), - ) { - repeat(rows) { - if (dotted) SchematicDotRow(dim) else SchematicWeekRow(dim) - } - } -} - -/** A week as seven filled cells. */ -@Composable -private fun SchematicWeekRow(dim: Boolean = false, modifier: Modifier = Modifier) { - val color = MaterialTheme.colorScheme.onSurfaceVariant - .copy(alpha = if (dim) 0.18f else 0.38f) - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - repeat(7) { - Box( - modifier = Modifier - .weight(1f) - .height(5.dp) - .clip(RoundedCornerShape(1.5.dp)) - .background(color), - ) - } - } -} - -/** A week as seven event dots — the split style's compact grid. */ -@Composable -private fun SchematicDotRow(dim: Boolean = false, modifier: Modifier = Modifier) { - val color = MaterialTheme.colorScheme.onSurfaceVariant - .copy(alpha = if (dim) 0.18f else 0.38f) - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(2.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - repeat(7) { - Box( - modifier = Modifier - .weight(1f) - .height(3.dp) - .clip(CircleShape) - .background(color), - ) - } - } -} +private val PREVIEW_HEIGHT = 240.dp +private val PREVIEW_SHAPE = RoundedCornerShape(12.dp) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index 6e983b9..c490375 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -104,6 +104,7 @@ import de.jeanlucmakiola.floret.reminders.reminderOverrideFor import de.jeanlucmakiola.calendula.data.prefs.ThemeMode import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref +import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay import de.jeanlucmakiola.calendula.domain.EventFormField import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType import de.jeanlucmakiola.calendula.qs.NewEventTileService @@ -915,6 +916,8 @@ private fun ViewsScreen( if (showMonthStyle) { MonthViewStylePicker( selected = state.monthViewStyle, + // The preview is a real grid, so it uses the real week start too. + weekStart = state.weekStart.resolveFirstDay(currentLocale()), onSelect = viewModel::setMonthViewStyle, onDismiss = { showMonthStyle = false }, )