feat(views): optional jump-to-today button in the toolbar (#60)

Add a "Today button in toolbar" appearance toggle (default off). When on,
each calendar view (month/week/day/agenda) shows a persistent go-to-today
icon button in its top bar instead of the fade-in extended FAB, and the
FAB pill is suppressed. Same jump action, just always present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 12:12:54 +02:00
parent 60ef920b82
commit 46afa830b3
11 changed files with 120 additions and 6 deletions

View File

@@ -234,6 +234,21 @@ class SettingsPrefs @Inject constructor(
store.edit { it[SHOW_WEEK_NUMBERS_KEY] = enabled }
}
/**
* 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 "+"
* only while the view is off today. Turning it ON moves it to a persistent
* icon button in each calendar view's top bar (always shown, on today or
* not) and drops the FAB pill.
*/
val todayButtonInToolbar: Flow<Boolean> = store.data.map { prefs ->
prefs[TODAY_BUTTON_IN_TOOLBAR_KEY] ?: false
}
suspend fun setTodayButtonInToolbar(enabled: Boolean) {
store.edit { it[TODAY_BUTTON_IN_TOOLBAR_KEY] = enabled }
}
/**
* How far ahead the in-app Agenda screen shows events (v2.11). Defaults to
* [AgendaRange.Month] — a month of upcoming events. Independent of the
@@ -738,6 +753,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 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")
internal val DRAWER_VIEW_ORDER_KEY = stringPreferencesKey("drawer_view_order")

View File

@@ -88,6 +88,8 @@ fun CalendarHost(
// sensible non-empty initial values, so they're ready before the first frame.
val quickSwitchViews = viewModel.quickSwitchViews.collectAsStateWithLifecycle().value
val drawerViewOrder = viewModel.drawerViewOrder.collectAsStateWithLifecycle().value
// Whether the jump-to-today control sits in each view's top bar or the FAB (#60).
val todayInToolbar = viewModel.todayButtonInToolbar.collectAsStateWithLifecycle().value
var viewStack by rememberSaveable(stateSaver = viewStackSaver) {
mutableStateOf(listOf(defaultView))
@@ -318,6 +320,7 @@ fun CalendarHost(
onCreateEvent = onCreateEvent,
quickSwitchViews = quickSwitchViews,
drawerViewOrder = drawerViewOrder,
todayInToolbar = todayInToolbar,
)
CalendarView.Day -> DayScreen(
selectedView = currentView,
@@ -329,6 +332,7 @@ fun CalendarHost(
initialDateIso = pendingDayIso,
quickSwitchViews = quickSwitchViews,
drawerViewOrder = drawerViewOrder,
todayInToolbar = todayInToolbar,
)
CalendarView.Month -> MonthScreen(
selectedView = currentView,
@@ -339,6 +343,7 @@ fun CalendarHost(
onCreateEvent = onCreateEvent,
quickSwitchViews = quickSwitchViews,
drawerViewOrder = drawerViewOrder,
todayInToolbar = todayInToolbar,
)
CalendarView.Agenda -> AgendaScreen(
selectedView = currentView,
@@ -350,6 +355,7 @@ fun CalendarHost(
onCreateEvent = onCreateEvent,
quickSwitchViews = quickSwitchViews,
drawerViewOrder = drawerViewOrder,
todayInToolbar = todayInToolbar,
)
}
}

View File

@@ -45,4 +45,12 @@ class CalendarHostViewModel @Inject constructor(
started = SharingStarted.WhileSubscribed(5_000L),
initialValue = IMPLEMENTED_VIEWS,
)
/** Whether each view's jump-to-today control lives in the top bar, not the FAB (#60). */
val todayButtonInToolbar: StateFlow<Boolean> = prefs.todayButtonInToolbar
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000L),
initialValue = false,
)
}

View File

@@ -22,6 +22,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Coffee
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Today
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.DrawerValue
@@ -102,6 +103,7 @@ fun AgendaScreen(
onCreateEvent: (LocalDate, Int?) -> Unit,
quickSwitchViews: List<CalendarView> = IMPLEMENTED_VIEWS,
drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
todayInToolbar: Boolean = false,
modifier: Modifier = Modifier,
viewModel: AgendaViewModel = hiltViewModel(),
) {
@@ -151,12 +153,14 @@ fun AgendaScreen(
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
onOpenDrawer = { scope.launch { drawerState.open() } },
onOpenSearch = onOpenSearch,
showTodayButton = todayInToolbar,
onToday = viewModel::goToToday,
scrollBehavior = scrollBehavior,
)
},
floatingActionButton = {
CalendarFabColumn(
todayVisible = !isOnToday,
todayVisible = !isOnToday && !todayInToolbar,
todayText = stringResource(R.string.agenda_today_action),
onToday = viewModel::goToToday,
onCreate = { onCreateEvent(anchor, null) },
@@ -504,6 +508,8 @@ private fun AgendaTopBar(
onCycleView: () -> Unit,
onOpenDrawer: () -> Unit,
onOpenSearch: () -> Unit,
showTodayButton: Boolean,
onToday: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
) {
TopAppBar(
@@ -522,6 +528,14 @@ private fun AgendaTopBar(
}
},
actions = {
if (showTodayButton) {
IconButton(onClick = onToday) {
Icon(
imageVector = Icons.Default.Today,
contentDescription = stringResource(R.string.today_jump_action),
)
}
}
IconButton(onClick = onOpenSearch) {
Icon(
imageVector = Icons.Default.Search,

View File

@@ -27,6 +27,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Today
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DrawerValue
@@ -128,6 +129,7 @@ fun DayScreen(
onCreateEvent: (LocalDate, Int?) -> Unit,
quickSwitchViews: List<CalendarView> = IMPLEMENTED_VIEWS,
drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
todayInToolbar: Boolean = false,
modifier: Modifier = Modifier,
initialDateIso: String? = null,
viewModel: DayViewModel = hiltViewModel(),
@@ -220,12 +222,14 @@ fun DayScreen(
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
onOpenDrawer = { scope.launch { drawerState.open() } },
onOpenSearch = onOpenSearch,
showTodayButton = todayInToolbar,
onToday = jumpToToday,
scrollBehavior = scrollBehavior,
)
},
floatingActionButton = {
CalendarFabColumn(
todayVisible = !isOnToday,
todayVisible = !isOnToday && !todayInToolbar,
todayText = stringResource(R.string.day_today_action),
onToday = jumpToToday,
onCreate = { onCreateEvent(date, null) },
@@ -376,6 +380,8 @@ private fun DayTopBar(
onCycleView: () -> Unit,
onOpenDrawer: () -> Unit,
onOpenSearch: () -> Unit,
showTodayButton: Boolean,
onToday: () -> Unit,
scrollBehavior: androidx.compose.material3.TopAppBarScrollBehavior,
) {
val locale = currentLocale()
@@ -395,6 +401,14 @@ private fun DayTopBar(
}
},
actions = {
if (showTodayButton) {
IconButton(onClick = onToday) {
Icon(
imageVector = Icons.Default.Today,
contentDescription = stringResource(R.string.today_jump_action),
)
}
}
IconButton(onClick = onOpenSearch) {
Icon(
imageVector = Icons.Default.Search,

View File

@@ -24,6 +24,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Today
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@@ -106,6 +107,7 @@ fun MonthScreen(
onCreateEvent: (LocalDate, Int?) -> Unit,
quickSwitchViews: List<CalendarView> = IMPLEMENTED_VIEWS,
drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
todayInToolbar: Boolean = false,
modifier: Modifier = Modifier,
viewModel: MonthViewModel = hiltViewModel(),
) {
@@ -199,12 +201,14 @@ fun MonthScreen(
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
onOpenDrawer = { scope.launch { drawerState.open() } },
onOpenSearch = onOpenSearch,
showTodayButton = todayInToolbar,
onToday = jumpToToday,
scrollBehavior = scrollBehavior,
)
},
floatingActionButton = {
CalendarFabColumn(
todayVisible = !isOnCurrentMonth,
todayVisible = !isOnCurrentMonth && !todayInToolbar,
todayText = stringResource(R.string.month_today_action),
onToday = jumpToToday,
onCreate = {
@@ -308,6 +312,8 @@ private fun MonthTopBar(
onCycleView: () -> Unit,
onOpenDrawer: () -> Unit,
onOpenSearch: () -> Unit,
showTodayButton: Boolean,
onToday: () -> Unit,
scrollBehavior: androidx.compose.material3.TopAppBarScrollBehavior,
) {
val locale = currentLocale()
@@ -327,6 +333,14 @@ private fun MonthTopBar(
}
},
actions = {
if (showTodayButton) {
IconButton(onClick = onToday) {
Icon(
imageVector = Icons.Default.Today,
contentDescription = stringResource(R.string.today_jump_action),
)
}
}
IconButton(onClick = onOpenSearch) {
Icon(
imageVector = Icons.Default.Search,

View File

@@ -571,6 +571,18 @@ private fun AppearanceScreen(
position = Position.Top,
onClick = { showDefaultView = true },
)
GroupedRow(
title = stringResource(R.string.settings_today_toolbar),
summary = stringResource(R.string.settings_today_toolbar_summary),
position = Position.Middle,
trailing = {
Switch(
checked = state.todayButtonInToolbar,
onCheckedChange = viewModel::setTodayButtonInToolbar,
)
},
onClick = { viewModel.setTodayButtonInToolbar(!state.todayButtonInToolbar) },
)
GroupedRow(
title = stringResource(R.string.settings_week_start),
summary = weekStartLabel(state.weekStart),

View File

@@ -37,6 +37,8 @@ data class SettingsUiState(
val dimCompletedEvents: Boolean = false,
/** Whether the Month grid shows calendar-week numbers in a left gutter (#25). */
val showWeekNumbers: Boolean = false,
/** Whether the jump-to-today control sits in the top bar instead of the FAB (#60). */
val todayButtonInToolbar: Boolean = false,
/** How far ahead the in-app Agenda screen shows events (v2.11). */
val agendaScreenRange: AgendaRange = AgendaRange.Month,
/** How far ahead the agenda widget shows events (v2.11). */

View File

@@ -124,8 +124,9 @@ class SettingsViewModel @Inject constructor(
prefs.showWeekNumbers,
prefs.agendaShowToday,
prefs.softenCalendarColors,
) { hourLines, weekNumbers, showToday, soften ->
DisplayToggles(hourLines, weekNumbers, showToday, soften)
prefs.todayButtonInToolbar,
) { hourLines, weekNumbers, showToday, soften, todayInToolbar ->
DisplayToggles(hourLines, weekNumbers, showToday, soften, todayInToolbar)
},
) { view, screenRange, widgetRange, timeFormat, toggles ->
ViewSettings(
@@ -134,6 +135,7 @@ class SettingsViewModel @Inject constructor(
showWeekNumbers = toggles.showWeekNumbers,
agendaShowToday = toggles.agendaShowToday,
softenColors = toggles.softenColors,
todayButtonInToolbar = toggles.todayButtonInToolbar,
)
},
combine(
@@ -159,6 +161,7 @@ class SettingsViewModel @Inject constructor(
showWeekNumbers = views.showWeekNumbers,
agendaShowToday = views.agendaShowToday,
softenColors = views.softenColors,
todayButtonInToolbar = views.todayButtonInToolbar,
agendaShowRangeBar = misc.showRangeBar,
autofocusEventTitle = misc.autofocusEventTitle,
pastEventDisplay = misc.pastEventDisplay,
@@ -234,6 +237,7 @@ class SettingsViewModel @Inject constructor(
val showWeekNumbers: Boolean,
val agendaShowToday: Boolean,
val softenColors: Boolean,
val todayButtonInToolbar: Boolean,
)
private data class DisplayToggles(
@@ -241,6 +245,7 @@ class SettingsViewModel @Inject constructor(
val showWeekNumbers: Boolean,
val agendaShowToday: Boolean,
val softenColors: Boolean,
val todayButtonInToolbar: Boolean,
)
private data class MiscSettings(
@@ -460,6 +465,10 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch { prefs.setShowWeekNumbers(enabled) }
}
fun setTodayButtonInToolbar(enabled: Boolean) {
viewModelScope.launch { prefs.setTodayButtonInToolbar(enabled) }
}
fun setPastEventDisplay(mode: PastEventDisplay) {
viewModelScope.launch {
prefs.setPastEventDisplay(mode)

View File

@@ -30,6 +30,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Today
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DrawerValue
@@ -144,6 +145,7 @@ fun WeekScreen(
onCreateEvent: (LocalDate, Int?) -> Unit,
quickSwitchViews: List<CalendarView> = IMPLEMENTED_VIEWS,
drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
todayInToolbar: Boolean = false,
modifier: Modifier = Modifier,
viewModel: WeekViewModel = hiltViewModel(),
) {
@@ -241,12 +243,14 @@ fun WeekScreen(
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
onOpenDrawer = { scope.launch { drawerState.open() } },
onOpenSearch = onOpenSearch,
showTodayButton = todayInToolbar,
onToday = jumpToToday,
scrollBehavior = scrollBehavior,
)
},
floatingActionButton = {
CalendarFabColumn(
todayVisible = !isOnCurrentWeek,
todayVisible = !isOnCurrentWeek && !todayInToolbar,
todayText = stringResource(R.string.week_today_action),
onToday = jumpToToday,
onCreate = {
@@ -409,6 +413,8 @@ private fun WeekTopBar(
onCycleView: () -> Unit,
onOpenDrawer: () -> Unit,
onOpenSearch: () -> Unit,
showTodayButton: Boolean,
onToday: () -> Unit,
scrollBehavior: androidx.compose.material3.TopAppBarScrollBehavior,
) {
val locale = currentLocale()
@@ -428,6 +434,14 @@ private fun WeekTopBar(
}
},
actions = {
if (showTodayButton) {
IconButton(onClick = onToday) {
Icon(
imageVector = Icons.Default.Today,
contentDescription = stringResource(R.string.today_jump_action),
)
}
}
IconButton(onClick = onOpenSearch) {
Icon(
imageVector = Icons.Default.Search,

View File

@@ -271,6 +271,9 @@
<string name="agenda_empty_title">You\'re all caught up</string>
<string name="agenda_no_more_today">No more events today</string>
<!-- Jump to today (toolbar icon button; see settings_today_toolbar) -->
<string name="today_jump_action">Go to today</string>
<!-- Event search -->
<string name="search_action">Search</string>
<string name="search_hint">Search events</string>
@@ -321,6 +324,8 @@
<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_today_toolbar">Today button in toolbar</string>
<string name="settings_today_toolbar_summary">Show a jump-to-today button in the toolbar instead of a floating button</string>
<string name="settings_time_format">Time format</string>
<string name="settings_time_format_auto">Automatic</string>
<string name="settings_time_format_12h">12-hour (2:00 PM)</string>