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:
@@ -21,6 +21,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.storageValue
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
|
||||
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
|
||||
import de.jeanlucmakiola.calendula.ui.theme.FONT_SYSTEM_TOKEN
|
||||
import java.time.ZoneId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -256,6 +257,18 @@ class SettingsPrefs @Inject constructor(
|
||||
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
|
||||
* 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 DIM_COMPLETED_EVENTS_KEY = booleanPreferencesKey("dim_completed_events")
|
||||
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 DEFAULT_VIEW_KEY = stringPreferencesKey("default_view")
|
||||
internal val QUICK_SWITCH_VIEWS_KEY = stringPreferencesKey("quick_switch_views")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,7 @@ import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.InlineTextField
|
||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||
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.ReorderableRowHeight
|
||||
import de.jeanlucmakiola.calendula.ui.common.icon
|
||||
@@ -847,12 +848,24 @@ private fun ViewsScreen(
|
||||
viewModel: SettingsViewModel,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
var showMonthStyle by remember { mutableStateOf(false) }
|
||||
|
||||
CollapsingScaffold(
|
||||
title = stringResource(R.string.settings_section_views),
|
||||
onBack = onBack,
|
||||
) {
|
||||
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))
|
||||
SettingsHint(stringResource(R.string.settings_quick_switch_hint))
|
||||
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]
|
||||
|
||||
@@ -13,6 +13,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||
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
|
||||
@@ -51,6 +52,8 @@ data class SettingsUiState(
|
||||
val defaultView: CalendarView = CalendarView.Week,
|
||||
/** Which views the top-bar quick-switch button cycles through, and their order (#24). */
|
||||
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. */
|
||||
val drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
|
||||
/** Optional event-form fields shown by default (rest behind "more fields"). */
|
||||
|
||||
@@ -35,6 +35,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
||||
import de.jeanlucmakiola.calendula.ui.agenda.storageValue
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
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.FONT_CUSTOM_TOKEN
|
||||
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_PAST_DISPLAY_KEY
|
||||
@@ -150,8 +151,12 @@ class SettingsViewModel @Inject constructor(
|
||||
prefs.dimCompletedEvents,
|
||||
// View customisation (#24) folded into one flow so it fits this
|
||||
// group — the outer combine is already at its five-arg limit.
|
||||
combine(prefs.quickSwitchConfig, prefs.drawerViewOrder) { quickSwitch, drawer ->
|
||||
ViewCustomization(quickSwitch, drawer)
|
||||
combine(
|
||||
prefs.quickSwitchConfig,
|
||||
prefs.drawerViewOrder,
|
||||
prefs.monthViewStyle,
|
||||
) { quickSwitch, drawer, monthStyle ->
|
||||
ViewCustomization(quickSwitch, drawer, monthStyle)
|
||||
},
|
||||
) { showRangeBar, autofocus, pastEvents, dimCompleted, viewCustomization ->
|
||||
MiscSettings(showRangeBar, autofocus, pastEvents, dimCompleted, viewCustomization)
|
||||
@@ -173,6 +178,7 @@ class SettingsViewModel @Inject constructor(
|
||||
dimCompletedEvents = misc.dimCompletedEvents,
|
||||
quickSwitchConfig = misc.viewCustomization.quickSwitch,
|
||||
drawerViewOrder = misc.viewCustomization.drawerOrder,
|
||||
monthViewStyle = misc.viewCustomization.monthViewStyle,
|
||||
allowColorOnUnsupportedCalendars = defaults.allowColor,
|
||||
defaultReminderMinutes = defaults.defaultReminder,
|
||||
defaultAllDayReminderMinutes = defaults.allDayReminder,
|
||||
@@ -286,6 +292,7 @@ class SettingsViewModel @Inject constructor(
|
||||
private data class ViewCustomization(
|
||||
val quickSwitch: QuickSwitchConfig,
|
||||
val drawerOrder: List<CalendarView>,
|
||||
val monthViewStyle: MonthViewStyle,
|
||||
)
|
||||
|
||||
/** 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) } }
|
||||
}
|
||||
|
||||
fun setMonthViewStyle(style: MonthViewStyle) {
|
||||
viewModelScope.launch { prefs.setMonthViewStyle(style) }
|
||||
}
|
||||
|
||||
fun setDrawerViewOrder(order: List<CalendarView>) {
|
||||
viewModelScope.launch { prefs.setDrawerViewOrder(order) }
|
||||
}
|
||||
|
||||
@@ -372,6 +372,17 @@
|
||||
<item quantity="other">%d days</item>
|
||||
</plurals>
|
||||
<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_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>
|
||||
@@ -411,7 +422,7 @@
|
||||
<string name="settings_translate_hint">Add or improve a language on Weblate</string>
|
||||
<!-- Hub category subtitles -->
|
||||
<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_notifications_subtitle">Event reminders</string>
|
||||
<string name="settings_special_dates_subtitle">Contact birthdays & anniversaries</string>
|
||||
|
||||
@@ -13,6 +13,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
|
||||
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
@@ -125,6 +126,30 @@ class SettingsPrefsTest {
|
||||
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
|
||||
fun `garbage stored enum falls back to default`(@TempDir tempDir: Path) = runTest {
|
||||
val store = newDataStore(tempDir)
|
||||
|
||||
Reference in New Issue
Block a user