feat(nav): jump-to-date action in the navigation drawer
All checks were successful
CI / ci (push) Successful in 6m17s
All checks were successful
CI / ci (push) Successful in 6m17s
Add a "Jump to date" row to the drawer (under the View switcher) that opens an M3 date picker and navigates the active view to the chosen day, sliding in from the correct side. Wired across Month/Week/Day, each seeding the picker with its visible anchor (day / week-start / 1st-of-month). Extract the form's private date-picker into a shared ui/common/CalendarDatePickerDialog so the event form and the drawer share one picker; add goToDate() to the Month and Week view models. Reprioritises the roadmap: jump-to-date is now next; duplicate-event drops to the bottom as low-importance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -169,7 +169,9 @@ unblock a later item. Order is a plan, not a contract — revisit after each lan
|
||||
4. ~~Per-event color~~ *(shipped v2.4.0)* — palette calendars write
|
||||
`EVENT_COLOR_KEY` (sync-safe); local/opted-in calendars write a raw
|
||||
`EVENT_COLOR`; off-by-default setting for no-palette synced calendars
|
||||
5. **Duplicate event** *(next)* — detail action → prefilled create form; near-free on the tap-to-create prefill infra
|
||||
Tier 1's create/edit + calendars arc is effectively closed. **Duplicate event**
|
||||
was deprioritised (2026-06-17) as low-importance and dropped to the bottom of
|
||||
the sequence; the next item is now **Jump-to-date** (formerly Tier 2).
|
||||
|
||||
(Tier 2+ numbering below shifts accordingly; ranking unchanged.)
|
||||
|
||||
@@ -223,7 +225,7 @@ Out of scope (no new settings *features* here) — this is a structure + style
|
||||
pass on the existing controls; new toggles ride in with their own features.
|
||||
|
||||
**Tier 2 — navigation & daily-driver completeness**
|
||||
5. Jump-to-date — drawer date picker (un-cut from V1); cheap, fills the nav gap
|
||||
5. Jump-to-date — drawer date picker (un-cut from V1); cheap, fills the nav gap *(next)*
|
||||
6. Agenda view — the missing 4th view; serves daily-driver users *and* becomes the data source for the widget
|
||||
|
||||
**Tier 3 — platform reach (depends on Tier 2)**
|
||||
@@ -240,6 +242,10 @@ pass on the existing controls; new toggles ride in with their own features.
|
||||
- Locations & People — contact address picker (no-permission, one-shot) is the safe entry; OSM autocomplete needs INTERNET
|
||||
- Move event to another calendar — sync-adapter minefield (copy+delete model)
|
||||
|
||||
**Bottom — deprioritised, not important**
|
||||
- Duplicate event (detail action → prefilled create form) — moved here
|
||||
2026-06-17; cheap but low value, pick up only if asked
|
||||
|
||||
**Unranked / fill-in** — pinch-to-zoom time scale, tablet/foldable layouts,
|
||||
full-text search, ICS file import. Pulled in opportunistically, not sequenced.
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package de.jeanlucmakiola.calendula.ui.common
|
||||
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberDatePickerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/** One UTC day in milliseconds — the unit the M3 [DatePicker] speaks. */
|
||||
const val MILLIS_PER_DAY: Long = 86_400_000L
|
||||
|
||||
/**
|
||||
* The app's standard Material 3 date picker, opened on [initial] and reporting
|
||||
* the chosen day through [onConfirm]. Shared by the event form (start/end date,
|
||||
* RRULE until) and the drawer's jump-to-date action.
|
||||
*
|
||||
* DatePicker speaks UTC-midnight millis; epoch-day arithmetic keeps the
|
||||
* conversion zone-proof in both directions.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CalendarDatePickerDialog(
|
||||
initial: LocalDate,
|
||||
onConfirm: (LocalDate) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val state = rememberDatePickerState(
|
||||
initialSelectedDateMillis = initial.toEpochDays() * MILLIS_PER_DAY,
|
||||
)
|
||||
DatePickerDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
state.selectedDateMillis?.let { millis ->
|
||||
onConfirm(LocalDate.fromEpochDays((millis / MILLIS_PER_DAY).toInt()))
|
||||
}
|
||||
},
|
||||
) { Text(stringResource(R.string.dialog_ok)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
||||
},
|
||||
) {
|
||||
DatePicker(state = state)
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,17 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DateRange
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -32,23 +37,31 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.ui.filter.CalendarFilterList
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Navigation drawer shared by every top-level calendar screen.
|
||||
*
|
||||
* Uses the app's grouped-card design system (see [GroupedRow]): a branded
|
||||
* header, the View switcher as a grouped card (the active view highlighted),
|
||||
* the per-calendar visibility filter (M3) inline, and a pinned Settings row.
|
||||
* The "View" section mirrors the top-bar switcher pill — tapping a view here
|
||||
* selects it (and closes the drawer) rather than cycling. The host screen owns
|
||||
* the drawer state.
|
||||
* a jump-to-date action, the per-calendar visibility filter (M3) inline, and a
|
||||
* pinned Settings row. The "View" section mirrors the top-bar switcher pill —
|
||||
* tapping a view here selects it (and closes the drawer) rather than cycling.
|
||||
* The host screen owns the drawer state.
|
||||
*
|
||||
* [currentDate] seeds the jump-to-date picker (the visible day/week-start/month
|
||||
* anchor); [onJumpToDate] navigates the active view to the chosen day.
|
||||
*/
|
||||
@Composable
|
||||
fun CalendarDrawer(
|
||||
currentView: CalendarView,
|
||||
currentDate: LocalDate,
|
||||
onSelectView: (CalendarView) -> Unit,
|
||||
onJumpToDate: (LocalDate) -> Unit,
|
||||
onSettings: () -> Unit,
|
||||
) {
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
|
||||
ModalDrawerSheet {
|
||||
// The whole sidebar scrolls as one — header, views, the calendar filter
|
||||
// and Settings all flow in a single scroll container.
|
||||
@@ -71,6 +84,15 @@ fun CalendarDrawer(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.drawer_jump_to_date),
|
||||
position = Position.Alone,
|
||||
minHeight = 56.dp,
|
||||
leading = { Icon(Icons.Filled.DateRange, contentDescription = null) },
|
||||
onClick = { showDatePicker = true },
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
DrawerSectionHeader(stringResource(R.string.filter_title))
|
||||
@@ -87,6 +109,17 @@ fun CalendarDrawer(
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (showDatePicker) {
|
||||
CalendarDatePickerDialog(
|
||||
initial = currentDate,
|
||||
onConfirm = {
|
||||
showDatePicker = false
|
||||
onJumpToDate(it)
|
||||
},
|
||||
onDismiss = { showDatePicker = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Branded header: the app-icon chip beside the app name. */
|
||||
|
||||
@@ -151,6 +151,11 @@ fun DayScreen(
|
||||
}
|
||||
viewModel.goToToday()
|
||||
}
|
||||
// Drawer jump-to-date: slide from the side the target lies on.
|
||||
val jumpToDate: (LocalDate) -> Unit = { target ->
|
||||
slideDir = if (target < date) -1 else 1
|
||||
viewModel.goToDate(target)
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
@@ -159,10 +164,15 @@ fun DayScreen(
|
||||
drawerContent = {
|
||||
CalendarDrawer(
|
||||
currentView = selectedView,
|
||||
currentDate = date,
|
||||
onSelectView = { view ->
|
||||
onSelectView(view)
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
onJumpToDate = { target ->
|
||||
jumpToDate(target)
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
onSettings = {
|
||||
onOpenSettings()
|
||||
scope.launch { drawerState.close() }
|
||||
|
||||
@@ -51,8 +51,6 @@ import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -73,7 +71,6 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TimePicker
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberDatePickerState
|
||||
import androidx.compose.material3.rememberTimePickerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -113,7 +110,9 @@ import de.jeanlucmakiola.calendula.domain.SimpleRecurrence
|
||||
import de.jeanlucmakiola.calendula.domain.parseSimpleRecurrence
|
||||
import de.jeanlucmakiola.calendula.domain.toRRule
|
||||
import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog
|
||||
import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
||||
import de.jeanlucmakiola.calendula.ui.common.MILLIS_PER_DAY
|
||||
import de.jeanlucmakiola.calendula.ui.common.InlineTextField
|
||||
import de.jeanlucmakiola.calendula.ui.common.OptionCard
|
||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||
@@ -786,12 +785,12 @@ private fun EventEditContent(
|
||||
}
|
||||
|
||||
when (picker) {
|
||||
PickerTarget.StartDate -> DatePickerAlert(
|
||||
PickerTarget.StartDate -> CalendarDatePickerDialog(
|
||||
initial = form.start.date,
|
||||
onConfirm = { viewModel.setStartDate(it); picker = null },
|
||||
onDismiss = { picker = null },
|
||||
)
|
||||
PickerTarget.EndDate -> DatePickerAlert(
|
||||
PickerTarget.EndDate -> CalendarDatePickerDialog(
|
||||
initial = form.end.date,
|
||||
onConfirm = { viewModel.setEndDate(it); picker = null },
|
||||
onDismiss = { picker = null },
|
||||
@@ -1178,7 +1177,7 @@ private fun RecurrencePickerDialog(
|
||||
)
|
||||
|
||||
if (showUntilPicker) {
|
||||
DatePickerAlert(
|
||||
CalendarDatePickerDialog(
|
||||
initial = untilDate ?: LocalDate.fromEpochDays(
|
||||
(Clock.System.now().toEpochMilliseconds() / MILLIS_PER_DAY).toInt(),
|
||||
),
|
||||
@@ -1688,37 +1687,6 @@ private fun ScheduleRow(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun DatePickerAlert(
|
||||
initial: LocalDate,
|
||||
onConfirm: (LocalDate) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
// DatePicker speaks UTC-midnight millis; epoch-day arithmetic keeps the
|
||||
// conversion zone-proof in both directions.
|
||||
val state = rememberDatePickerState(
|
||||
initialSelectedDateMillis = initial.toEpochDays() * MILLIS_PER_DAY,
|
||||
)
|
||||
DatePickerDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
state.selectedDateMillis?.let { millis ->
|
||||
onConfirm(LocalDate.fromEpochDays((millis / MILLIS_PER_DAY).toInt()))
|
||||
}
|
||||
},
|
||||
) { Text(stringResource(R.string.dialog_ok)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
||||
},
|
||||
) {
|
||||
DatePicker(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TimePickerAlert(
|
||||
@@ -1775,5 +1743,3 @@ private fun CalendarPickerDialog(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private const val MILLIS_PER_DAY = 86_400_000L
|
||||
|
||||
@@ -124,6 +124,11 @@ fun MonthScreen(
|
||||
}
|
||||
viewModel.goToToday()
|
||||
}
|
||||
// Drawer jump-to-date: slide from the side the target month lies on.
|
||||
val jumpToDate: (LocalDate) -> Unit = { target ->
|
||||
slideDir = if (YearMonth(target.year, target.month) < month) -1 else 1
|
||||
viewModel.goToDate(target)
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
@@ -132,10 +137,15 @@ fun MonthScreen(
|
||||
drawerContent = {
|
||||
CalendarDrawer(
|
||||
currentView = selectedView,
|
||||
currentDate = LocalDate(month.year, month.month, 1),
|
||||
onSelectView = { view ->
|
||||
onSelectView(view)
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
onJumpToDate = { target ->
|
||||
jumpToDate(target)
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
onSettings = {
|
||||
onOpenSettings()
|
||||
scope.launch { drawerState.close() }
|
||||
|
||||
@@ -96,6 +96,11 @@ class MonthViewModel @Inject constructor(
|
||||
_month.value = YearMonth(todayDate.year, todayDate.month)
|
||||
}
|
||||
|
||||
/** Jump to the month containing [date] (drawer jump-to-date). */
|
||||
fun goToDate(date: LocalDate) {
|
||||
_month.value = YearMonth(date.year, date.month)
|
||||
}
|
||||
|
||||
private fun buildState(
|
||||
ym: YearMonth,
|
||||
weekStart: DayOfWeek,
|
||||
|
||||
@@ -156,6 +156,11 @@ fun WeekScreen(
|
||||
}
|
||||
viewModel.goToToday()
|
||||
}
|
||||
// Drawer jump-to-date: slide from the side the target week lies on.
|
||||
val jumpToDate: (LocalDate) -> Unit = { target ->
|
||||
slideDir = if (target < weekStart) -1 else 1
|
||||
viewModel.goToDate(target)
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
@@ -164,10 +169,15 @@ fun WeekScreen(
|
||||
drawerContent = {
|
||||
CalendarDrawer(
|
||||
currentView = selectedView,
|
||||
currentDate = weekStart,
|
||||
onSelectView = { view ->
|
||||
onSelectView(view)
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
onJumpToDate = { target ->
|
||||
jumpToDate(target)
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
onSettings = {
|
||||
onOpenSettings()
|
||||
scope.launch { drawerState.close() }
|
||||
|
||||
@@ -107,6 +107,11 @@ class WeekViewModel @Inject constructor(
|
||||
_anchor.value = todayDate
|
||||
}
|
||||
|
||||
/** Jump to the week containing [date] (drawer jump-to-date). */
|
||||
fun goToDate(date: LocalDate) {
|
||||
_anchor.value = date
|
||||
}
|
||||
|
||||
private fun buildState(
|
||||
start: LocalDate,
|
||||
calendars: List<CalendarSource>,
|
||||
|
||||
@@ -198,6 +198,9 @@
|
||||
<string name="view_day">Tag</string>
|
||||
<string name="view_section">Ansicht</string>
|
||||
|
||||
<!-- Zu Datum springen (Navigationsleiste) -->
|
||||
<string name="drawer_jump_to_date">Zu Datum springen</string>
|
||||
|
||||
<!-- Kalender-Filter (M3) -->
|
||||
<string name="filter_title">Kalender</string>
|
||||
|
||||
|
||||
@@ -199,6 +199,9 @@
|
||||
<string name="view_day">Day</string>
|
||||
<string name="view_section">View</string>
|
||||
|
||||
<!-- Jump to date (drawer) -->
|
||||
<string name="drawer_jump_to_date">Jump to date</string>
|
||||
|
||||
<!-- Calendar filter (M3) -->
|
||||
<string name="filter_title">Calendars</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user