feat(widget): header navigation + fix R8-broken month arrows (#18, #20) #53

Merged
makiolaj merged 4 commits from feat/widget-header-navigation into release/v2.13.0 2026-07-01 10:58:07 +00:00
6 changed files with 73 additions and 7 deletions
Showing only changes of commit 40d70d9f2b - Show all commits

View File

@@ -14,8 +14,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
a reminder a week before *and* one on the day. The reminder pickers are now
multi-select; per-calendar overrides can still inherit the global default or
turn reminders off entirely. Thanks to @moonj for the suggestion ([#14]).
- Widget headers now open the app. Tapping the month/year title on the month
widget opens the app on the month view, and tapping the "Upcoming" title on
the agenda widget opens it on your default view — so there's a one-tap way
back into the app that lands where you'd expect, instead of only through a day
or event. The month widget's "today" button now snaps the grid back to the
current month in place. Thanks to @rgz46vic and @ptab for the suggestions
([#18], [#20]).
### Fixed
- Month widget arrows and "today" button work again. On release builds the
prev/next-month arrows and the jump-to-today control on the month widget did
nothing when tapped — code shrinking had stripped the tap handlers behind
them. They respond again. Thanks to @rgz46vic for the report ([#18]).
- Disabled calendars no longer notify. Reminders for events in a calendar you
have disabled (Settings → Calendars) are now suppressed instead of still
popping up — matching how a disabled calendar's events already stay hidden
@@ -754,3 +765,5 @@ automatically, with zero telemetry and no internet permission.
[#14]: https://codeberg.org/jlmakiola/calendula/issues/14
[#16]: https://codeberg.org/jlmakiola/calendula/issues/16
[#17]: https://codeberg.org/jlmakiola/calendula/issues/17
[#18]: https://codeberg.org/jlmakiola/calendula/issues/18
[#20]: https://codeberg.org/jlmakiola/calendula/issues/20

View File

@@ -193,6 +193,12 @@ class MainActivity : AppCompatActivity() {
// An external date tap (launcher/clock) has no widget source, so it opens
// the day view rooted over the default home view (OpenDate source = null).
calendarTimeDateOrNull()?.let { return WidgetNavRequest.OpenDate(it.toString(), source = null) }
// A widget header tap: open a top-level view with no date drill-in. The
// empty string carried by [openViewIntent] means "the default home view".
if (hasExtra(EXTRA_OPEN_VIEW)) {
val name = getStringExtra(EXTRA_OPEN_VIEW).orEmpty()
return WidgetNavRequest.OpenView(CalendarView.entries.firstOrNull { it.name == name })
}
val source = sourceViewOrNull()
val eventId = getLongExtra(EXTRA_EVENT_ID, -1L)
return when {
@@ -244,6 +250,10 @@ class MainActivity : AppCompatActivity() {
private const val EXTRA_DATE_ISO = "de.jeanlucmakiola.calendula.extra.DATE_ISO"
private const val EXTRA_CREATE = "de.jeanlucmakiola.calendula.extra.CREATE"
// A widget header tap asking to open a top-level view (no date drill-in).
// Its value is the target [CalendarView] name, or "" for the default view.
private const val EXTRA_OPEN_VIEW = "de.jeanlucmakiola.calendula.extra.OPEN_VIEW"
// The [CalendarView] (by name) of the widget a launch came from. Roots the
// in-app back stack in that view; absent for non-widget launches (reminders).
private const val EXTRA_SOURCE_VIEW = "de.jeanlucmakiola.calendula.extra.SOURCE_VIEW"
@@ -307,5 +317,19 @@ class MainActivity : AppCompatActivity() {
putExtra(EXTRA_DATE_ISO, date.toString())
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
/**
* Open the app on a top-level [view] with no date drill-in — a widget
* header tap. A null [view] opens the user's default home view (the agenda
* widget's "Upcoming" title); a concrete view roots there over the default
* home (the month widget's month/year title → [CalendarView.Month]). The
* per-view data URI keeps distinct headers' PendingIntents from collapsing.
*/
fun openViewIntent(context: Context, view: CalendarView?): Intent =
Intent(context, MainActivity::class.java).apply {
data = "calendula://view/${view?.name ?: "default"}".toUri()
putExtra(EXTRA_OPEN_VIEW, view?.name ?: "")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
}

View File

@@ -212,6 +212,17 @@ fun CalendarHost(
detailKey = key
onWidgetNavConsumed()
}
is WidgetNavRequest.OpenView -> {
// A widget header tap: land on a top-level view with no date
// drill-in. Reveal it by dropping any covering overlay, then root
// the stack on the target (null → the default home) over the
// default home — so backing out returns to the default, then exits.
dismissCoveringOverlays()
createDateIso = null
pendingDayIso = null
viewStack = viewBaseStack(defaultView, req.view ?: defaultView)
onWidgetNavConsumed()
}
is WidgetNavRequest.Create -> {
// External "new event" entries (QS tile / launcher shortcut /
// widget) must land on top of whatever is open — the form overlay

View File

@@ -31,4 +31,13 @@ sealed interface WidgetNavRequest {
/** Open the create-event form prefilled for [dateIso] (today when null). */
data class Create(val dateIso: String?) : WidgetNavRequest
/**
* Open the app rooted on a top-level [view] with no date drill-in — a widget
* header tap. A null [view] means "the user's default home view" (the agenda
* widget's "Upcoming" title, issue #20); a concrete view roots there over the
* default home (the month widget's month/year title → [CalendarView.Month],
* issue #18), so backing out returns to the default view, then exits.
*/
data class OpenView(val view: CalendarView?) : WidgetNavRequest
}

View File

@@ -193,6 +193,8 @@ private fun AgendaHeader() {
modifier = GlanceModifier.fillMaxWidth().padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Tap the "Upcoming" title to open the app on the user's default view
// (issue #20) — a null target resolves to the default home in the host.
Text(
text = context.getString(R.string.widget_agenda_title),
style = TextStyle(
@@ -200,7 +202,11 @@ private fun AgendaHeader() {
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
),
modifier = GlanceModifier.defaultWeight(),
modifier = GlanceModifier
.defaultWeight()
.clickable(
actionStartActivity(MainActivity.openViewIntent(context, view = null)),
),
)
IconButton(
resId = R.drawable.ic_widget_refresh,

View File

@@ -194,6 +194,8 @@ private fun MonthHeader(label: String) {
),
),
)
// Tap the month/year title to open the app on the month view (issue #18),
// rooted over the default home so backing out returns there.
Text(
text = label,
style = TextStyle(
@@ -204,16 +206,17 @@ private fun MonthHeader(label: String) {
),
modifier = GlanceModifier
.defaultWeight()
.clickable(actionRunCallback<ResetMonthAction>()),
.clickable(
actionStartActivity(
MainActivity.openViewIntent(context, CalendarView.Month),
),
),
)
// The "today" button snaps the grid back to the current month in place.
HeaderIcon(
resId = R.drawable.ic_widget_today,
contentDescription = context.getString(R.string.widget_today),
onClick = GlanceModifier.clickable(
actionStartActivity(
MainActivity.openDateIntent(context, today(systemZone()), CalendarView.Month),
),
),
onClick = GlanceModifier.clickable(actionRunCallback<ResetMonthAction>()),
)
HeaderIcon(
resId = R.drawable.ic_widget_chevron_right,