feat(views): customizable quick-switch cycle + drawer order (#24)
Add a "Views" settings section that lets users pick which views the top-bar quick-switch button cycles through and drag to reorder them, plus an independent drag-to-reorder for the navigation drawer's view list. Two separate configs: a view turned off in the quick-switch cycle is still reachable from the drawer, which always lists every view. - QuickSwitchConfig (order + enabled set) and a drawer order persisted in SettingsPrefs (comma-joined enum names; "!" marks a disabled view). Missing views append enabled and unknown names drop, so a future view defaults into both lists. - The pill cycles through the configured enabled views in order; the drawer renders CalendarDrawer from the drawer order. Both threaded from CalendarHost via CalendarHostViewModel. - New ReorderableColumn: dependency-free, measurement-driven drag reordering for the short grouped-card settings lists (no LazyColumn, since settings are a single verticalScroll column). Commits one write per gesture. - The switch needs two targets, so the last two enabled views can't be turned off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,9 @@ import androidx.datastore.preferences.core.Preferences
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.jeanlucmakiola.calendula.domain.EventFormField
|
||||
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 kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
@@ -149,6 +152,78 @@ class SettingsPrefsTest {
|
||||
assertThat(prefs.defaultFormFields.first()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `quick-switch defaults to every view enabled in default order`(@TempDir tempDir: Path) = runTest {
|
||||
val prefs = SettingsPrefs(newDataStore(tempDir))
|
||||
val config = prefs.quickSwitchConfig.first()
|
||||
assertThat(config.order).containsExactlyElementsIn(IMPLEMENTED_VIEWS).inOrder()
|
||||
assertThat(config.enabled).containsExactlyElementsIn(IMPLEMENTED_VIEWS)
|
||||
assertThat(config.cycle).containsExactlyElementsIn(IMPLEMENTED_VIEWS).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `quick-switch config round-trips order and disabled views`(@TempDir tempDir: Path) = runTest {
|
||||
val prefs = SettingsPrefs(newDataStore(tempDir))
|
||||
val config = QuickSwitchConfig(
|
||||
order = listOf(CalendarView.Agenda, CalendarView.Month, CalendarView.Week, CalendarView.Day),
|
||||
enabled = setOf(CalendarView.Agenda, CalendarView.Month),
|
||||
)
|
||||
prefs.setQuickSwitchConfig(config)
|
||||
val loaded = prefs.quickSwitchConfig.first()
|
||||
assertThat(loaded.order).containsExactly(
|
||||
CalendarView.Agenda, CalendarView.Month, CalendarView.Week, CalendarView.Day,
|
||||
).inOrder()
|
||||
assertThat(loaded.enabled).containsExactly(CalendarView.Agenda, CalendarView.Month)
|
||||
assertThat(loaded.cycle).containsExactly(CalendarView.Agenda, CalendarView.Month).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `quick-switch parse appends missing views enabled and drops unknowns`(@TempDir tempDir: Path) = runTest {
|
||||
val store = newDataStore(tempDir)
|
||||
val prefs = SettingsPrefs(store)
|
||||
// Only Day (disabled) and Week stored, plus a bogus name; Month & Agenda absent.
|
||||
store.updateData { p ->
|
||||
val m = p.toMutablePreferences()
|
||||
m[SettingsPrefs.QUICK_SWITCH_VIEWS_KEY] = "!Day,Week,Hologram"
|
||||
m
|
||||
}
|
||||
val config = prefs.quickSwitchConfig.first()
|
||||
// Stored order first (Day, Week), then the omitted views in default order.
|
||||
assertThat(config.order).containsExactly(
|
||||
CalendarView.Day, CalendarView.Week, CalendarView.Month, CalendarView.Agenda,
|
||||
).inOrder()
|
||||
// Day was explicitly disabled; the appended Month & Agenda default enabled.
|
||||
assertThat(config.enabled).containsExactly(
|
||||
CalendarView.Week, CalendarView.Month, CalendarView.Agenda,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `drawer order defaults to the implemented order and round-trips`(@TempDir tempDir: Path) = runTest {
|
||||
val prefs = SettingsPrefs(newDataStore(tempDir))
|
||||
assertThat(prefs.drawerViewOrder.first()).containsExactlyElementsIn(IMPLEMENTED_VIEWS).inOrder()
|
||||
prefs.setDrawerViewOrder(
|
||||
listOf(CalendarView.Agenda, CalendarView.Day, CalendarView.Week, CalendarView.Month),
|
||||
)
|
||||
assertThat(prefs.drawerViewOrder.first()).containsExactly(
|
||||
CalendarView.Agenda, CalendarView.Day, CalendarView.Week, CalendarView.Month,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `drawer order parse appends missing views and drops unknowns`(@TempDir tempDir: Path) = runTest {
|
||||
val store = newDataStore(tempDir)
|
||||
val prefs = SettingsPrefs(store)
|
||||
store.updateData { p ->
|
||||
val m = p.toMutablePreferences()
|
||||
m[SettingsPrefs.DRAWER_VIEW_ORDER_KEY] = "Agenda,Nope,Week"
|
||||
m
|
||||
}
|
||||
assertThat(prefs.drawerViewOrder.first()).containsExactly(
|
||||
CalendarView.Agenda, CalendarView.Week, CalendarView.Month, CalendarView.Day,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown stored form-field names are dropped`(@TempDir tempDir: Path) = runTest {
|
||||
val store = newDataStore(tempDir)
|
||||
|
||||
@@ -91,4 +91,33 @@ class ViewBackStackTest {
|
||||
assertThat(viewBaseStack(CalendarView.Agenda, CalendarView.Agenda))
|
||||
.containsExactly(CalendarView.Agenda)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `next cycles through the configured views in order, wrapping around`() {
|
||||
val cycle = listOf(CalendarView.Month, CalendarView.Agenda)
|
||||
assertThat(CalendarView.Month.next(cycle)).isEqualTo(CalendarView.Agenda)
|
||||
// Wraps back to the first from the last.
|
||||
assertThat(CalendarView.Agenda.next(cycle)).isEqualTo(CalendarView.Month)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `next from a view outside the cycle lands on the first enabled view`() {
|
||||
// Day was disabled in the quick-switch cycle but is still the current view.
|
||||
val cycle = listOf(CalendarView.Week, CalendarView.Month)
|
||||
assertThat(CalendarView.Day.next(cycle)).isEqualTo(CalendarView.Week)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `next on an empty cycle stays put`() {
|
||||
assertThat(CalendarView.Week.next(emptyList())).isEqualTo(CalendarView.Week)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `quick-switch cycle keeps enabled views in their configured order`() {
|
||||
val config = QuickSwitchConfig(
|
||||
order = listOf(CalendarView.Agenda, CalendarView.Day, CalendarView.Month, CalendarView.Week),
|
||||
enabled = setOf(CalendarView.Agenda, CalendarView.Month),
|
||||
)
|
||||
assertThat(config.cycle).containsExactly(CalendarView.Agenda, CalendarView.Month).inOrder()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user