feat(settings): add the month view style picker (#38, #53)

Introduces MonthViewStyle{Paged, Continuous, Split} and the setting that
chooses it. Only Paged is wired up so far — the other two land next; this
commit is the pref, the state plumbing and the chooser.

One picker rather than two switches ("vertical scrolling" + "month with
agenda"): independent toggles would multiply into four combinations, most of
which nobody asked for. Split does not disable the Agenda view — that stays a
forward multi-day window, while the split pane lists one selected day.

The chooser lives on the Views settings screen (per-view layout belongs with
the other view configuration) and is a hand-rolled FullScreenPicker rather than
OptionPicker, which cannot render previews: the three options differ in shape,
which a word like "Continuous" does not convey. Each card carries a schematic
drawn from theme tokens, and selection reads three ways over — border weight,
container tint and a check — so it never rests on colour alone. The cards are a
selectableGroup with Role.RadioButton for screen readers.

Folded into the ViewCustomization holder, which had spare arity; the outer
settings combine is still at its five-flow limit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-20 16:45:42 +02:00
parent 498250650c
commit 7510e1f9af
8 changed files with 430 additions and 3 deletions

View File

@@ -21,6 +21,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.storageValue
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.QuickSwitchConfig import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
import de.jeanlucmakiola.calendula.ui.theme.FONT_SYSTEM_TOKEN import de.jeanlucmakiola.calendula.ui.theme.FONT_SYSTEM_TOKEN
import java.time.ZoneId import java.time.ZoneId
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -256,6 +257,18 @@ class SettingsPrefs @Inject constructor(
store.edit { it[SHOW_WEEK_NUMBERS_KEY] = enabled } store.edit { it[SHOW_WEEK_NUMBERS_KEY] = enabled }
} }
/**
* How the Month view lays itself out (#38, #53). Defaults to [MonthViewStyle.Paged]
* — the historical behaviour, so existing installs see no change until they opt in.
*/
val monthViewStyle: Flow<MonthViewStyle> = store.data.map { prefs ->
prefs[MONTH_VIEW_STYLE_KEY].toEnum(MonthViewStyle.Paged)
}
suspend fun setMonthViewStyle(style: MonthViewStyle) {
store.edit { it[MONTH_VIEW_STYLE_KEY] = style.name }
}
/** /**
* Where the jump-to-today control lives (issue #60). Default OFF — the * Where the jump-to-today control lives (issue #60). Default OFF — the
* historical layout, where it's an extended FAB that fades in above the "+" * historical layout, where it's an extended FAB that fades in above the "+"
@@ -775,6 +788,7 @@ class SettingsPrefs @Inject constructor(
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 SHOW_WEEK_NUMBERS_KEY = booleanPreferencesKey("show_week_numbers")
internal val MONTH_VIEW_STYLE_KEY = stringPreferencesKey("month_view_style")
internal val TODAY_BUTTON_IN_TOOLBAR_KEY = booleanPreferencesKey("today_button_in_toolbar") internal val TODAY_BUTTON_IN_TOOLBAR_KEY = booleanPreferencesKey("today_button_in_toolbar")
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")

View File

@@ -0,0 +1,45 @@
package de.jeanlucmakiola.calendula.ui.month
import androidx.annotation.StringRes
import de.jeanlucmakiola.calendula.R
/**
* How the Month view lays itself out (#38, #53).
*
* One setting rather than two independent toggles: "vertical scrolling" and
* "month plus agenda" would otherwise multiply into four combinations to build
* and test, most of which nobody asked for.
*
* [Split] does **not** replace or disable the Agenda view — that stays a forward
* multi-day window with its own range model, while the split pane lists a single
* selected day.
*/
enum class MonthViewStyle {
/** Month pages, swiped left/right. Full event bars and pills per day. */
Paged,
/**
* One uninterrupted vertical stream of weeks. Each date appears exactly once,
* which is the point — paging repeats a boundary week at both ends (#38).
*/
Continuous,
/** Compact dots-only grid over a list of the selected day's events (#53). */
Split,
}
@get:StringRes
val MonthViewStyle.labelRes: Int
get() = when (this) {
MonthViewStyle.Paged -> R.string.month_style_paged
MonthViewStyle.Continuous -> R.string.month_style_continuous
MonthViewStyle.Split -> R.string.month_style_split
}
@get:StringRes
val MonthViewStyle.descriptionRes: Int
get() = when (this) {
MonthViewStyle.Paged -> R.string.month_style_paged_summary
MonthViewStyle.Continuous -> R.string.month_style_continuous_summary
MonthViewStyle.Split -> R.string.month_style_split_summary
}

View File

@@ -0,0 +1,297 @@
package de.jeanlucmakiola.calendula.ui.settings
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.month.MonthViewStyle
import de.jeanlucmakiola.calendula.ui.month.descriptionRes
import de.jeanlucmakiola.calendula.ui.month.labelRes
import de.jeanlucmakiola.floret.components.FullScreenPicker
/**
* 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.
*
* Tapping applies immediately and the picker stays open, matching the App name
* picker: the change is worth seeing land before leaving.
*/
@Composable
internal fun MonthViewStylePicker(
selected: MonthViewStyle,
onSelect: (MonthViewStyle) -> Unit,
onDismiss: () -> Unit,
) {
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)
},
),
contentAlignment = Alignment.Center,
) {
if (selected) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(16.dp),
)
}
}
}
}
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)),
)
}
}
}
/** [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),
)
}
}
}

View File

@@ -125,6 +125,7 @@ import de.jeanlucmakiola.floret.components.GroupedRow
import de.jeanlucmakiola.floret.components.InlineTextField import de.jeanlucmakiola.floret.components.InlineTextField
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
import de.jeanlucmakiola.calendula.ui.month.labelRes
import de.jeanlucmakiola.floret.components.ReorderableColumn import de.jeanlucmakiola.floret.components.ReorderableColumn
import de.jeanlucmakiola.floret.components.ReorderableRowHeight import de.jeanlucmakiola.floret.components.ReorderableRowHeight
import de.jeanlucmakiola.calendula.ui.common.icon import de.jeanlucmakiola.calendula.ui.common.icon
@@ -847,12 +848,24 @@ private fun ViewsScreen(
viewModel: SettingsViewModel, viewModel: SettingsViewModel,
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
var showMonthStyle by remember { mutableStateOf(false) }
CollapsingScaffold( CollapsingScaffold(
title = stringResource(R.string.settings_section_views), title = stringResource(R.string.settings_section_views),
onBack = onBack, onBack = onBack,
) { ) {
val config = state.quickSwitchConfig val config = state.quickSwitchConfig
// Per-view layout, above the cross-view switcher/order settings below.
SectionHeader(stringResource(R.string.settings_month_header))
GroupedRow(
title = stringResource(R.string.settings_month_view_style),
summary = stringResource(state.monthViewStyle.labelRes),
position = Position.Alone,
onClick = { showMonthStyle = true },
)
Spacer(Modifier.height(24.dp))
SectionHeader(stringResource(R.string.settings_quick_switch_header)) SectionHeader(stringResource(R.string.settings_quick_switch_header))
SettingsHint(stringResource(R.string.settings_quick_switch_hint)) SettingsHint(stringResource(R.string.settings_quick_switch_hint))
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
@@ -898,6 +911,14 @@ private fun ViewsScreen(
) )
} }
} }
if (showMonthStyle) {
MonthViewStylePicker(
selected = state.monthViewStyle,
onSelect = viewModel::setMonthViewStyle,
onDismiss = { showMonthStyle = false },
)
}
} }
/** One reorderable view row: the view's icon and name, an optional [trailing] /** One reorderable view row: the view's icon and name, an optional [trailing]

View File

@@ -13,6 +13,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
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.QuickSwitchConfig import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
/** /**
* Settings screen state (M4). Persisted preferences are instant to read, so * Settings screen state (M4). Persisted preferences are instant to read, so
@@ -51,6 +52,8 @@ data class SettingsUiState(
val defaultView: CalendarView = CalendarView.Week, val defaultView: CalendarView = CalendarView.Week,
/** Which views the top-bar quick-switch button cycles through, and their order (#24). */ /** Which views the top-bar quick-switch button cycles through, and their order (#24). */
val quickSwitchConfig: QuickSwitchConfig = QuickSwitchConfig.Default, val quickSwitchConfig: QuickSwitchConfig = QuickSwitchConfig.Default,
/** How the Month view lays itself out: pages, continuous scroll, or split (#38, #53). */
val monthViewStyle: MonthViewStyle = MonthViewStyle.Paged,
/** Order of the views in the navigation drawer (#24); every view is always listed. */ /** Order of the views in the navigation drawer (#24); every view is always listed. */
val drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS, val drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
/** Optional event-form fields shown by default (rest behind "more fields"). */ /** Optional event-form fields shown by default (rest behind "more fields"). */

View File

@@ -35,6 +35,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
import de.jeanlucmakiola.calendula.ui.agenda.storageValue import de.jeanlucmakiola.calendula.ui.agenda.storageValue
import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.CalendarView
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
import de.jeanlucmakiola.calendula.ui.theme.AppFontSettings import de.jeanlucmakiola.calendula.ui.theme.AppFontSettings
import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_PAST_DISPLAY_KEY import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_PAST_DISPLAY_KEY
@@ -150,8 +151,12 @@ class SettingsViewModel @Inject constructor(
prefs.dimCompletedEvents, prefs.dimCompletedEvents,
// View customisation (#24) folded into one flow so it fits this // View customisation (#24) folded into one flow so it fits this
// group — the outer combine is already at its five-arg limit. // group — the outer combine is already at its five-arg limit.
combine(prefs.quickSwitchConfig, prefs.drawerViewOrder) { quickSwitch, drawer -> combine(
ViewCustomization(quickSwitch, drawer) prefs.quickSwitchConfig,
prefs.drawerViewOrder,
prefs.monthViewStyle,
) { quickSwitch, drawer, monthStyle ->
ViewCustomization(quickSwitch, drawer, monthStyle)
}, },
) { showRangeBar, autofocus, pastEvents, dimCompleted, viewCustomization -> ) { showRangeBar, autofocus, pastEvents, dimCompleted, viewCustomization ->
MiscSettings(showRangeBar, autofocus, pastEvents, dimCompleted, viewCustomization) MiscSettings(showRangeBar, autofocus, pastEvents, dimCompleted, viewCustomization)
@@ -173,6 +178,7 @@ class SettingsViewModel @Inject constructor(
dimCompletedEvents = misc.dimCompletedEvents, dimCompletedEvents = misc.dimCompletedEvents,
quickSwitchConfig = misc.viewCustomization.quickSwitch, quickSwitchConfig = misc.viewCustomization.quickSwitch,
drawerViewOrder = misc.viewCustomization.drawerOrder, drawerViewOrder = misc.viewCustomization.drawerOrder,
monthViewStyle = misc.viewCustomization.monthViewStyle,
allowColorOnUnsupportedCalendars = defaults.allowColor, allowColorOnUnsupportedCalendars = defaults.allowColor,
defaultReminderMinutes = defaults.defaultReminder, defaultReminderMinutes = defaults.defaultReminder,
defaultAllDayReminderMinutes = defaults.allDayReminder, defaultAllDayReminderMinutes = defaults.allDayReminder,
@@ -286,6 +292,7 @@ class SettingsViewModel @Inject constructor(
private data class ViewCustomization( private data class ViewCustomization(
val quickSwitch: QuickSwitchConfig, val quickSwitch: QuickSwitchConfig,
val drawerOrder: List<CalendarView>, val drawerOrder: List<CalendarView>,
val monthViewStyle: MonthViewStyle,
) )
/** Contact special-dates sub-page (issue #15); its own flow — the main state is full. */ /** Contact special-dates sub-page (issue #15); its own flow — the main state is full. */
@@ -562,6 +569,10 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch { prefs.updateQuickSwitch { it.copy(order = order) } } viewModelScope.launch { prefs.updateQuickSwitch { it.copy(order = order) } }
} }
fun setMonthViewStyle(style: MonthViewStyle) {
viewModelScope.launch { prefs.setMonthViewStyle(style) }
}
fun setDrawerViewOrder(order: List<CalendarView>) { fun setDrawerViewOrder(order: List<CalendarView>) {
viewModelScope.launch { prefs.setDrawerViewOrder(order) } viewModelScope.launch { prefs.setDrawerViewOrder(order) }
} }

View File

@@ -372,6 +372,17 @@
<item quantity="other">%d days</item> <item quantity="other">%d days</item>
</plurals> </plurals>
<string name="settings_section_views">Views</string> <string name="settings_section_views">Views</string>
<!-- Month view style (#38, #53) -->
<string name="settings_month_header">Month view</string>
<string name="settings_month_view_style">Month view style</string>
<string name="settings_month_view_style_summary">Choose how the month view is laid out and how you move through it.</string>
<string name="month_style_paged">Pages</string>
<string name="month_style_paged_summary">One month at a time. Swipe sideways to change month.</string>
<string name="month_style_continuous">Continuous</string>
<string name="month_style_continuous_summary">Scroll up and down through the weeks. No month is cut off and no day appears twice.</string>
<string name="month_style_split">Split</string>
<string name="month_style_split_summary">A compact grid with dots for events, and the day you tap listed underneath.</string>
<string name="month_split_no_events">Nothing scheduled</string>
<string name="settings_quick_switch_header">Quick-switch button</string> <string name="settings_quick_switch_header">Quick-switch button</string>
<string name="settings_quick_switch_hint">Choose which views the top-right button cycles through, and drag to reorder them. Turned-off views stay reachable from the navigation menu.</string> <string name="settings_quick_switch_hint">Choose which views the top-right button cycles through, and drag to reorder them. Turned-off views stay reachable from the navigation menu.</string>
<string name="settings_drawer_order_header">Navigation menu</string> <string name="settings_drawer_order_header">Navigation menu</string>
@@ -411,7 +422,7 @@
<string name="settings_translate_hint">Add or improve a language on Weblate</string> <string name="settings_translate_hint">Add or improve a language on Weblate</string>
<!-- Hub category subtitles --> <!-- Hub category subtitles -->
<string name="settings_appearance_subtitle">Theme, default view, week start</string> <string name="settings_appearance_subtitle">Theme, default view, week start</string>
<string name="settings_views_subtitle">Quick-switch button and menu order</string> <string name="settings_views_subtitle">Month layout, quick-switch button, menu order</string>
<string name="settings_event_form_subtitle">Default fields for new events</string> <string name="settings_event_form_subtitle">Default fields for new events</string>
<string name="settings_notifications_subtitle">Event reminders</string> <string name="settings_notifications_subtitle">Event reminders</string>
<string name="settings_special_dates_subtitle">Contact birthdays &amp; anniversaries</string> <string name="settings_special_dates_subtitle">Contact birthdays &amp; anniversaries</string>

View File

@@ -13,6 +13,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
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.QuickSwitchConfig import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.datetime.DayOfWeek import kotlinx.datetime.DayOfWeek
@@ -125,6 +126,30 @@ class SettingsPrefsTest {
assertThat(prefs.agendaWidgetRange.first()).isEqualTo(AgendaRange.Week) assertThat(prefs.agendaWidgetRange.first()).isEqualTo(AgendaRange.Week)
} }
@Test
fun `month view style defaults to paged and round-trips`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
assertThat(prefs.monthViewStyle.first()).isEqualTo(MonthViewStyle.Paged)
prefs.setMonthViewStyle(MonthViewStyle.Split)
assertThat(prefs.monthViewStyle.first()).isEqualTo(MonthViewStyle.Split)
prefs.setMonthViewStyle(MonthViewStyle.Continuous)
assertThat(prefs.monthViewStyle.first()).isEqualTo(MonthViewStyle.Continuous)
}
@Test
fun `garbage stored month view style falls back to paged`(@TempDir tempDir: Path) = runTest {
val store = newDataStore(tempDir)
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.MONTH_VIEW_STYLE_KEY] = "Carousel"
m
}
assertThat(prefs.monthViewStyle.first()).isEqualTo(MonthViewStyle.Paged)
}
@Test @Test
fun `garbage stored enum falls back to default`(@TempDir tempDir: Path) = runTest { fun `garbage stored enum falls back to default`(@TempDir tempDir: Path) = runTest {
val store = newDataStore(tempDir) val store = newDataStore(tempDir)