Merge pull request 'feat: week-of-year numbers in Month view (#25)' (!64) from feat/month-week-numbers into release/v2.14.0
All checks were successful
Translations / check (pull_request) Successful in 7s
CI / ci (pull_request) Successful in 10m43s

Reviewed-on: #64
This commit was merged in pull request #64.
This commit is contained in:
2026-07-06 20:26:13 +00:00
8 changed files with 230 additions and 113 deletions

View File

@@ -200,6 +200,19 @@ class SettingsPrefs @Inject constructor(
store.edit { it[DIM_COMPLETED_EVENTS_KEY] = enabled } store.edit { it[DIM_COMPLETED_EVENTS_KEY] = enabled }
} }
/**
* Whether the Month grid shows the calendar-week (ISO) number in a left
* gutter (#25). Defaults to OFF — users opt in, since it narrows the day
* cells slightly. The Week view shows its number unconditionally.
*/
val showWeekNumbers: Flow<Boolean> = store.data.map { prefs ->
prefs[SHOW_WEEK_NUMBERS_KEY] ?: false
}
suspend fun setShowWeekNumbers(enabled: Boolean) {
store.edit { it[SHOW_WEEK_NUMBERS_KEY] = enabled }
}
/** /**
* How far ahead the in-app Agenda screen shows events (v2.11). Defaults to * How far ahead the in-app Agenda screen shows events (v2.11). Defaults to
* [AgendaRange.Month] — a month of upcoming events. Independent of the * [AgendaRange.Month] — a month of upcoming events. Independent of the
@@ -659,6 +672,7 @@ class SettingsPrefs @Inject constructor(
internal val SHOW_HOUR_LINES_KEY = booleanPreferencesKey("show_hour_lines") internal val SHOW_HOUR_LINES_KEY = booleanPreferencesKey("show_hour_lines")
internal val PAST_EVENT_DISPLAY_KEY = stringPreferencesKey("agenda_past_event_display") internal val PAST_EVENT_DISPLAY_KEY = stringPreferencesKey("agenda_past_event_display")
internal val DIM_COMPLETED_EVENTS_KEY = booleanPreferencesKey("dim_completed_events") internal val DIM_COMPLETED_EVENTS_KEY = booleanPreferencesKey("dim_completed_events")
internal val SHOW_WEEK_NUMBERS_KEY = booleanPreferencesKey("show_week_numbers")
internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view") internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view")
internal val QUICK_SWITCH_VIEWS_KEY = stringPreferencesKey("quick_switch_views") internal val QUICK_SWITCH_VIEWS_KEY = stringPreferencesKey("quick_switch_views")
internal val DRAWER_VIEW_ORDER_KEY = stringPreferencesKey("drawer_view_order") internal val DRAWER_VIEW_ORDER_KEY = stringPreferencesKey("drawer_view_order")

View File

@@ -108,6 +108,7 @@ fun MonthScreen(
val month by viewModel.month.collectAsStateWithLifecycle() val month by viewModel.month.collectAsStateWithLifecycle()
val weekStart by viewModel.weekStart.collectAsStateWithLifecycle() val weekStart by viewModel.weekStart.collectAsStateWithLifecycle()
val dimCompleted by viewModel.dimCompletedEvents.collectAsStateWithLifecycle() val dimCompleted by viewModel.dimCompletedEvents.collectAsStateWithLifecycle()
val showWeekNumbers by viewModel.showWeekNumbers.collectAsStateWithLifecycle()
// The instant before which an event counts as completed, or null when dimming // The instant before which an event counts as completed, or null when dimming
// is off. derivedStateOf keeps the per-minute "now" from recomposing the // is off. derivedStateOf keeps the per-minute "now" from recomposing the
// screen while the setting is off (it stays null regardless of the tick). // screen while the setting is off (it stays null regardless of the tick).
@@ -211,11 +212,12 @@ fun MonthScreen(
.padding(innerPadding) .padding(innerPadding)
.fillMaxSize(), .fillMaxSize(),
) { ) {
WeekdayHeader(weekStart = weekStart) WeekdayHeader(weekStart = weekStart, showWeekNumbers = showWeekNumbers)
CompositionLocalProvider(LocalDimCutoff provides dimCutoff) { CompositionLocalProvider(LocalDimCutoff provides dimCutoff) {
MonthContent( MonthContent(
state = state, state = state,
slideDir = slideDir, slideDir = slideDir,
showWeekNumbers = showWeekNumbers,
onSwipeNext = goNext, onSwipeNext = goNext,
onSwipePrev = goPrev, onSwipePrev = goPrev,
onRetry = jumpToToday, onRetry = jumpToToday,
@@ -231,6 +233,7 @@ fun MonthScreen(
private fun MonthContent( private fun MonthContent(
state: MonthUiState, state: MonthUiState,
slideDir: Int, slideDir: Int,
showWeekNumbers: Boolean,
onSwipeNext: () -> Unit, onSwipeNext: () -> Unit,
onSwipePrev: () -> Unit, onSwipePrev: () -> Unit,
onRetry: () -> Unit, onRetry: () -> Unit,
@@ -276,6 +279,7 @@ private fun MonthContent(
is MonthUiState.Failure -> CalendarFailure(reason = s.reason, onRetry = onRetry) is MonthUiState.Failure -> CalendarFailure(reason = s.reason, onRetry = onRetry)
is MonthUiState.Success -> MonthGrid( is MonthUiState.Success -> MonthGrid(
state = s, state = s,
showWeekNumbers = showWeekNumbers,
onOpenDay = onOpenDay, onOpenDay = onOpenDay,
) )
} }
@@ -325,7 +329,7 @@ private fun MonthTopBar(
} }
@Composable @Composable
private fun WeekdayHeader(weekStart: DayOfWeek) { private fun WeekdayHeader(weekStart: DayOfWeek, showWeekNumbers: Boolean) {
val locale = currentLocale() val locale = currentLocale()
val days = remember(weekStart, locale) { val days = remember(weekStart, locale) {
(0 until 7).map { offset -> (0 until 7).map { offset ->
@@ -337,6 +341,8 @@ private fun WeekdayHeader(weekStart: DayOfWeek) {
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp), .padding(horizontal = 8.dp, vertical = 4.dp),
) { ) {
// Reserve the gutter so the weekday labels stay over their day columns.
if (showWeekNumbers) Spacer(Modifier.width(WEEK_NUMBER_GUTTER))
days.forEach { dow -> days.forEach { dow ->
val isWeekend = dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY val isWeekend = dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY
val javaDow = java.time.DayOfWeek.of(dow.ordinal + 1) val javaDow = java.time.DayOfWeek.of(dow.ordinal + 1)
@@ -354,6 +360,9 @@ private fun WeekdayHeader(weekStart: DayOfWeek) {
private val EVENT_ROW_HEIGHT = 20.dp private val EVENT_ROW_HEIGHT = 20.dp
private val DAY_NUMBER_HEIGHT = 22.dp private val DAY_NUMBER_HEIGHT = 22.dp
/** Width of the optional left calendar-week gutter (#25); narrow, since it only
* seats a one- or two-digit week number in a full-height tonal pill. */
private val WEEK_NUMBER_GUTTER = 40.dp
private val DAY_NUMBER_GAP = 4.dp private val DAY_NUMBER_GAP = 4.dp
private val CELL_TOP_PADDING = 6.dp private val CELL_TOP_PADDING = 6.dp
private val CELL_GAP = 2.dp private val CELL_GAP = 2.dp
@@ -363,6 +372,7 @@ private const val MAX_EVENT_ROWS = 3
@Composable @Composable
private fun MonthGrid( private fun MonthGrid(
state: MonthUiState.Success, state: MonthUiState.Success,
showWeekNumbers: Boolean,
onOpenDay: (LocalDate) -> Unit, onOpenDay: (LocalDate) -> Unit,
) { ) {
Column( Column(
@@ -376,6 +386,7 @@ private fun MonthGrid(
week = week, week = week,
today = state.today, today = state.today,
month = state.month, month = state.month,
showWeekNumbers = showWeekNumbers,
onOpenDay = onOpenDay, onOpenDay = onOpenDay,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -397,6 +408,7 @@ private fun MonthWeekRow(
week: MonthWeek, week: MonthWeek,
today: LocalDate, today: LocalDate,
month: YearMonth, month: YearMonth,
showWeekNumbers: Boolean,
onOpenDay: (LocalDate) -> Unit, onOpenDay: (LocalDate) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@@ -404,135 +416,182 @@ private fun MonthWeekRow(
val laneCount = (week.spans.maxOfOrNull { it.lane } ?: -1) + 1 val laneCount = (week.spans.maxOfOrNull { it.lane } ?: -1) + 1
val shownLanes = laneCount.coerceAtMost(MAX_EVENT_ROWS) val shownLanes = laneCount.coerceAtMost(MAX_EVENT_ROWS)
BoxWithConstraints(modifier) { Row(modifier) {
val colW = maxWidth / 7 // Optional calendar-week gutter, sized so the seven day columns below
// divide the remaining width — the absolute bar offsets stay correct
// Per-day background pills — same surfaceContainer rounded surface the // because they're measured inside the grid box, not the whole row.
// week/day views use, so the three views share one visual language. if (showWeekNumbers) {
// Spanning bars draw on top of these, bridging cells, so they still read WeekNumberGutter(
// as one continuous event. weekStart = week.days.first(),
Row(Modifier.matchParentSize()) {
week.days.forEach { d ->
val inMonth = d.month == month.month && d.year == month.year
Box(
Modifier
.weight(1f)
.fillMaxHeight()
.padding(horizontal = CELL_GAP, vertical = 1.dp)
.background(
color = if (inMonth) MaterialTheme.colorScheme.surfaceContainer
else MaterialTheme.colorScheme.surfaceContainerLow,
shape = CELL_SHAPE,
),
)
}
}
Column(Modifier.fillMaxSize().padding(top = CELL_TOP_PADDING)) {
Row(Modifier.fillMaxWidth()) {
week.days.forEach { d ->
DayNumberCell(
date = d,
isToday = d == today,
inMonth = d.month == month.month && d.year == month.year,
modifier = Modifier.weight(1f),
)
}
}
// Breathing room between the day number (and today's circle) and the
// first event row.
Spacer(Modifier.height(DAY_NUMBER_GAP))
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .width(WEEK_NUMBER_GUTTER)
.weight(1f) .fillMaxHeight(),
.clipToBounds(), )
) { }
// Spanning bars on their shared lanes. BoxWithConstraints(
week.spans.filter { it.lane < shownLanes }.forEach { span -> Modifier
val cols = span.endCol - span.startCol + 1 .weight(1f)
MonthBar( .fillMaxHeight(),
event = span.event, ) {
dark = dark, val colW = maxWidth / 7
continuesLeft = span.continuesLeft,
continuesRight = span.continuesRight, // Per-day background pills — same surfaceContainer rounded surface the
modifier = Modifier // week/day views use, so the three views share one visual language.
.offset( // Spanning bars draw on top of these, bridging cells, so they still read
x = colW * span.startCol, // as one continuous event.
y = EVENT_ROW_HEIGHT * span.lane, Row(Modifier.matchParentSize()) {
) week.days.forEach { d ->
.width(colW * cols) val inMonth = d.month == month.month && d.year == month.year
.height(EVENT_ROW_HEIGHT) Box(
.padding(horizontal = CELL_GAP + 1.dp, vertical = 1.dp), Modifier
.weight(1f)
.fillMaxHeight()
.padding(horizontal = CELL_GAP, vertical = 1.dp)
.background(
color = if (inMonth) MaterialTheme.colorScheme.surfaceContainer
else MaterialTheme.colorScheme.surfaceContainerLow,
shape = CELL_SHAPE,
),
) )
} }
// Single-day timed pills + overflow, per column. Pills fill the }
// lane slots no bar occupies on THIS day (top-most first), so a
// bar-free day isn't pushed down by a multi-day event that only Column(Modifier.fillMaxSize().padding(top = CELL_TOP_PADDING)) {
// sits on other days of the week. Row(Modifier.fillMaxWidth()) {
week.days.forEachIndexed { col, d -> week.days.forEach { d ->
val timed = week.timedByDay[d].orEmpty() DayNumberCell(
val occupied = week.spans date = d,
.filter { it.lane < shownLanes && col in it.startCol..it.endCol } isToday = d == today,
.map { it.lane } inMonth = d.month == month.month && d.year == month.year,
.toSet() modifier = Modifier.weight(1f),
val freeSlots = (0 until MAX_EVENT_ROWS).filter { it !in occupied } )
val pillsShown = timed.take(freeSlots.size) }
pillsShown.forEachIndexed { i, ev -> }
// Breathing room between the day number (and today's circle) and the
// first event row.
Spacer(Modifier.height(DAY_NUMBER_GAP))
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.clipToBounds(),
) {
// Spanning bars on their shared lanes.
week.spans.filter { it.lane < shownLanes }.forEach { span ->
val cols = span.endCol - span.startCol + 1
MonthBar( MonthBar(
event = ev, event = span.event,
dark = dark, dark = dark,
continuesLeft = false, continuesLeft = span.continuesLeft,
continuesRight = false, continuesRight = span.continuesRight,
modifier = Modifier modifier = Modifier
.offset( .offset(
x = colW * col, x = colW * span.startCol,
y = EVENT_ROW_HEIGHT * freeSlots[i], y = EVENT_ROW_HEIGHT * span.lane,
) )
.width(colW) .width(colW * cols)
.height(EVENT_ROW_HEIGHT) .height(EVENT_ROW_HEIGHT)
.padding(horizontal = CELL_GAP + 1.dp, vertical = 1.dp), .padding(horizontal = CELL_GAP + 1.dp, vertical = 1.dp),
) )
} }
val hidden = (week.countByDay[d] ?: 0) - occupied.size - pillsShown.size // Single-day timed pills + overflow, per column. Pills fill the
if (hidden > 0) { // lane slots no bar occupies on THIS day (top-most first), so a
val hiddenColors = buildList { // bar-free day isn't pushed down by a multi-day event that only
week.spans // sits on other days of the week.
.filter { it.lane >= shownLanes && col in it.startCol..it.endCol } week.days.forEachIndexed { col, d ->
.forEach { add(it.event.color) } val timed = week.timedByDay[d].orEmpty()
timed.drop(pillsShown.size).forEach { add(it.color) } val occupied = week.spans
}.distinct().take(3) .filter { it.lane < shownLanes && col in it.startCol..it.endCol }
OverflowDots( .map { it.lane }
colors = hiddenColors, .toSet()
extra = hidden - hiddenColors.size, val freeSlots = (0 until MAX_EVENT_ROWS).filter { it !in occupied }
dark = dark, val pillsShown = timed.take(freeSlots.size)
modifier = Modifier pillsShown.forEachIndexed { i, ev ->
.offset(x = colW * col, y = EVENT_ROW_HEIGHT * MAX_EVENT_ROWS) MonthBar(
.width(colW) event = ev,
.padding(horizontal = 3.dp), dark = dark,
) continuesLeft = false,
continuesRight = false,
modifier = Modifier
.offset(
x = colW * col,
y = EVENT_ROW_HEIGHT * freeSlots[i],
)
.width(colW)
.height(EVENT_ROW_HEIGHT)
.padding(horizontal = CELL_GAP + 1.dp, vertical = 1.dp),
)
}
val hidden = (week.countByDay[d] ?: 0) - occupied.size - pillsShown.size
if (hidden > 0) {
val hiddenColors = buildList {
week.spans
.filter { it.lane >= shownLanes && col in it.startCol..it.endCol }
.forEach { add(it.event.color) }
timed.drop(pillsShown.size).forEach { add(it.color) }
}.distinct().take(3)
OverflowDots(
colors = hiddenColors,
extra = hidden - hiddenColors.size,
dark = dark,
modifier = Modifier
.offset(x = colW * col, y = EVENT_ROW_HEIGHT * MAX_EVENT_ROWS)
.width(colW)
.padding(horizontal = 3.dp),
)
}
} }
} }
} }
}
// Tap layer: in month view a tap on any day opens that day. Padded and // Tap layer: in month view a tap on any day opens that day. Padded and
// clipped to the background pill so the ripple matches it. // clipped to the background pill so the ripple matches it.
Row(Modifier.matchParentSize()) { Row(Modifier.matchParentSize()) {
week.days.forEach { d -> week.days.forEach { d ->
Box( Box(
Modifier Modifier
.weight(1f) .weight(1f)
.fillMaxHeight() .fillMaxHeight()
.padding(horizontal = CELL_GAP, vertical = 1.dp) .padding(horizontal = CELL_GAP, vertical = 1.dp)
.clip(CELL_SHAPE) .clip(CELL_SHAPE)
.clickable { onOpenDay(d) }, .clickable { onOpenDay(d) },
) )
}
} }
} }
} }
} }
/**
* Left-gutter calendar-week cell (#25): a full-height tonal pill mirroring the
* day cells' geometry, set apart by the secondaryContainer tint (matching the
* Week view's badge), with the ISO week number centred like a day number. The
* week is computed on the row's first day — the same basis as the Week view — so
* the two agree.
*/
@Composable
private fun WeekNumberGutter(weekStart: LocalDate, modifier: Modifier = Modifier) {
val weekNumber = remember(weekStart) {
java.time.LocalDate.of(weekStart.year, weekStart.month.ordinal + 1, weekStart.day)
.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR)
}
val label = stringResource(R.string.week_number_label)
Box(
modifier = modifier
.padding(horizontal = CELL_GAP, vertical = 1.dp)
.background(MaterialTheme.colorScheme.secondaryContainer, CELL_SHAPE)
.semantics { contentDescription = "$label $weekNumber" },
contentAlignment = Alignment.Center,
) {
Text(
text = weekNumber.toString(),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
}
@Composable @Composable
private fun DayNumberCell( private fun DayNumberCell(
date: LocalDate, date: LocalDate,

View File

@@ -67,6 +67,14 @@ class MonthViewModel @Inject constructor(
initialValue = false, initialValue = false,
) )
/** Whether to show the calendar-week number gutter (#25; display only). */
val showWeekNumbers: StateFlow<Boolean> = settingsPrefs.showWeekNumbers
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000L),
initialValue = false,
)
private val todayDate: LocalDate private val todayDate: LocalDate
get() = Clock.System.now().toLocalDateTime(zone).date get() = Clock.System.now().toLocalDateTime(zone).date

View File

@@ -613,6 +613,18 @@ private fun AppearanceScreen(
position = Position.Middle, position = Position.Middle,
onClick = { showWeekStart = true }, onClick = { showWeekStart = true },
) )
GroupedRow(
title = stringResource(R.string.settings_week_numbers),
summary = stringResource(R.string.settings_week_numbers_summary),
position = Position.Middle,
trailing = {
Switch(
checked = state.showWeekNumbers,
onCheckedChange = viewModel::setShowWeekNumbers,
)
},
onClick = { viewModel.setShowWeekNumbers(!state.showWeekNumbers) },
)
GroupedRow( GroupedRow(
title = stringResource(R.string.settings_time_format), title = stringResource(R.string.settings_time_format),
summary = timeFormatLabel(state.timeFormat), summary = timeFormatLabel(state.timeFormat),

View File

@@ -33,6 +33,8 @@ data class SettingsUiState(
val pastEventDisplay: PastEventDisplay = PastEventDisplay.SHOW, val pastEventDisplay: PastEventDisplay = PastEventDisplay.SHOW,
/** Whether the month/week grids fade events that have already finished. */ /** Whether the month/week grids fade events that have already finished. */
val dimCompletedEvents: Boolean = false, val dimCompletedEvents: Boolean = false,
/** Whether the Month grid shows calendar-week numbers in a left gutter (#25). */
val showWeekNumbers: Boolean = false,
/** How far ahead the in-app Agenda screen shows events (v2.11). */ /** How far ahead the in-app Agenda screen shows events (v2.11). */
val agendaScreenRange: AgendaRange = AgendaRange.Month, val agendaScreenRange: AgendaRange = AgendaRange.Month,
/** How far ahead the agenda widget shows events (v2.11). */ /** How far ahead the agenda widget shows events (v2.11). */

View File

@@ -116,9 +116,15 @@ class SettingsViewModel @Inject constructor(
prefs.agendaScreenRange, prefs.agendaScreenRange,
prefs.agendaWidgetRange, prefs.agendaWidgetRange,
prefs.timeFormat, prefs.timeFormat,
prefs.showHourLines, // Two grid-display toggles folded into one flow so they fit this
) { view, screenRange, widgetRange, timeFormat, showHourLines -> // group — the outer combine is already at its five-arg limit.
ViewSettings(view, screenRange, widgetRange, timeFormat, showHourLines) combine(prefs.showHourLines, prefs.showWeekNumbers, ::Pair),
) { view, screenRange, widgetRange, timeFormat, gridToggles ->
ViewSettings(
view, screenRange, widgetRange, timeFormat,
showHourLines = gridToggles.first,
showWeekNumbers = gridToggles.second,
)
}, },
combine( combine(
prefs.agendaShowRangeBar, prefs.agendaShowRangeBar,
@@ -140,6 +146,7 @@ class SettingsViewModel @Inject constructor(
agendaWidgetRange = views.agendaWidgetRange, agendaWidgetRange = views.agendaWidgetRange,
timeFormat = views.timeFormat, timeFormat = views.timeFormat,
showHourLines = views.showHourLines, showHourLines = views.showHourLines,
showWeekNumbers = views.showWeekNumbers,
agendaShowRangeBar = misc.showRangeBar, agendaShowRangeBar = misc.showRangeBar,
autofocusEventTitle = misc.autofocusEventTitle, autofocusEventTitle = misc.autofocusEventTitle,
pastEventDisplay = misc.pastEventDisplay, pastEventDisplay = misc.pastEventDisplay,
@@ -212,6 +219,7 @@ class SettingsViewModel @Inject constructor(
val agendaWidgetRange: AgendaRange, val agendaWidgetRange: AgendaRange,
val timeFormat: TimeFormatPref, val timeFormat: TimeFormatPref,
val showHourLines: Boolean, val showHourLines: Boolean,
val showWeekNumbers: Boolean,
) )
private data class MiscSettings( private data class MiscSettings(
@@ -398,6 +406,10 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch { prefs.setShowHourLines(enabled) } viewModelScope.launch { prefs.setShowHourLines(enabled) }
} }
fun setShowWeekNumbers(enabled: Boolean) {
viewModelScope.launch { prefs.setShowWeekNumbers(enabled) }
}
fun setPastEventDisplay(mode: PastEventDisplay) { fun setPastEventDisplay(mode: PastEventDisplay) {
viewModelScope.launch { viewModelScope.launch {
prefs.setPastEventDisplay(mode) prefs.setPastEventDisplay(mode)

View File

@@ -287,6 +287,8 @@
<string name="settings_font_import_failed">Couldn\'t read that file as a font</string> <string name="settings_font_import_failed">Couldn\'t read that file as a font</string>
<string name="settings_week_start">Week starts on</string> <string name="settings_week_start">Week starts on</string>
<string name="settings_week_start_auto">Automatic</string> <string name="settings_week_start_auto">Automatic</string>
<string name="settings_week_numbers">Week numbers</string>
<string name="settings_week_numbers_summary">Show calendar-week numbers in month view</string>
<string name="settings_time_format">Time format</string> <string name="settings_time_format">Time format</string>
<string name="settings_time_format_auto">Automatic</string> <string name="settings_time_format_auto">Automatic</string>
<string name="settings_time_format_12h">12-hour (2:00 PM)</string> <string name="settings_time_format_12h">12-hour (2:00 PM)</string>

View File

@@ -95,6 +95,14 @@ class SettingsPrefsTest {
assertThat(prefs.showHourLines.first()).isTrue() assertThat(prefs.showHourLines.first()).isTrue()
} }
@Test
fun `week numbers default off and round-trips`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
assertThat(prefs.showWeekNumbers.first()).isFalse()
prefs.setShowWeekNumbers(true)
assertThat(prefs.showWeekNumbers.first()).isTrue()
}
@Test @Test
fun `autofocus event title defaults on and round-trips`(@TempDir tempDir: Path) = runTest { fun `autofocus event title defaults on and round-trips`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir)) val prefs = SettingsPrefs(newDataStore(tempDir))