From e258a4a7f2a3a07e38b71937ae28fce3186496d1 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 28 Jun 2026 22:29:19 +0200 Subject: [PATCH] feat(widget): apply the past-event display setting to the agenda widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #12 asks for hiding finished events in the agenda view *and* its home-screen widget. The in-app screen already honours the Past events setting (Show / Dim / Hide); extend the same setting to the "Upcoming" agenda widget. The widget loads its events unfiltered and applies the mode reactively in the composition against a captured "now" — mirroring how the range is read from per-instance Glance state — so toggling the setting reflects on a live widget without relying on provideGlance's data preamble re-running. Hiding drops finished events and any day they empty; dimming fades the colour stripe and lowers the title's emphasis (Glance has no generic alpha modifier). setPastEventDisplay now pushes the mode into each widget's Glance state and recomposes, like setAgendaWidgetRange. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 13 ++--- .../calendula/data/prefs/SettingsPrefs.kt | 6 +++ .../ui/settings/SettingsViewModel.kt | 15 +++++- .../calendula/widget/WidgetData.kt | 11 +++++ .../calendula/widget/agenda/AgendaWidget.kt | 49 +++++++++++++++++-- 5 files changed, 82 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b75b8..d6ee6d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - See past events your way. Two new settings change how events that have already ended are shown. **Past events** (Settings → Appearance → Agenda) lets the - agenda keep them as usual, dim them, or hide them from the list entirely. - **Dim completed events** (Settings → Appearance) separately fades finished - events in the month and week views. Both are off by default, an event only - counts as finished once it has actually ended (events still in progress are - never dimmed), and the list updates on its own as the day goes on. Thanks to - @ptab for the suggestion ([#12]). + agenda — both in the app and the home-screen "Upcoming" widget — keep them as + usual, dim them, or hide them from the list entirely. **Dim completed events** + (Settings → Appearance) separately fades finished events in the month and week + views. Both are off by default, an event only counts as finished once it has + actually ended (events still in progress are never dimmed), and the lists + update on their own as the day goes on. Thanks to @ptab for the suggestion + ([#12]). ### Changed - Readable titles on overlapping events. In the week and day views, events that diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index 7e574ca..1a1f1d5 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -45,6 +45,12 @@ enum class TimeFormatPref { AUTO, TWELVE_HOUR, TWENTY_FOUR_HOUR } */ enum class PastEventDisplay { SHOW, DIM, HIDE } +/** Parse a stored [PastEventDisplay.name]; unknown/null falls back to [default]. */ +fun parsePastEventDisplay( + stored: String?, + default: PastEventDisplay = PastEventDisplay.SHOW, +): PastEventDisplay = PastEventDisplay.entries.firstOrNull { it.name == stored } ?: default + /** * Resolve to a concrete 24-hour flag. AUTO defers to [systemIs24Hour] (the * device's `DateFormat.is24HourFormat` value). diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index b0bea35..557ab21 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -21,6 +21,7 @@ import de.jeanlucmakiola.calendula.domain.EventFormField 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.widget.agenda.AGENDA_PAST_DISPLAY_KEY import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_RANGE_KEY import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget import de.jeanlucmakiola.calendula.widget.month.MonthWidget @@ -219,7 +220,19 @@ class SettingsViewModel @Inject constructor( } fun setPastEventDisplay(mode: PastEventDisplay) { - viewModelScope.launch { prefs.setPastEventDisplay(mode) } + viewModelScope.launch { + prefs.setPastEventDisplay(mode) + // The agenda widget honours this setting too, so push it into each + // instance's Glance state and recompose — same reliable-update path as + // setAgendaWidgetRange (updateAll alone won't re-run the data preamble). + widgetRefreshMutex.withLock { + val manager = GlanceAppWidgetManager(appContext) + manager.getGlanceIds(AgendaWidget::class.java).forEach { id -> + updateAppWidgetState(appContext, id) { it[AGENDA_PAST_DISPLAY_KEY] = mode.name } + } + AgendaWidget().updateAll(appContext) + } + } } fun setDimCompletedEvents(enabled: Boolean) { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt index 468939b..cbce878 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt @@ -4,6 +4,7 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import androidx.core.content.ContextCompat +import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay import de.jeanlucmakiola.calendula.data.prefs.is24Hour import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay import de.jeanlucmakiola.calendula.domain.EventInstance @@ -24,6 +25,7 @@ import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import java.util.Locale import kotlin.time.Clock +import kotlin.time.Instant /** * How far either side of today the month widget pre-loads. The displayed month @@ -52,6 +54,8 @@ sealed interface AgendaWidgetData { * range is sliced from this in the composition (read reactively from * Glance state), so a range change is pure recomposition rather than a * flaky widget session restart — the same approach the month widget uses. + * Kept unfiltered: hiding/dimming finished events also happens reactively + * in the composition, against [now], for the same reliable-update reason. */ val days: List, /** Resolved clock convention for event time labels (the time-format pref). */ @@ -60,6 +64,10 @@ sealed interface AgendaWidgetData { val weekStart: DayOfWeek, /** Saved range pref — the fallback when an instance has no Glance state yet. */ val savedRange: AgendaRange, + /** Saved past-event display pref — the fallback before Glance state is set. */ + val savedPastDisplay: PastEventDisplay, + /** Snapshot instant the data was read at, for "has this event ended?" tests. */ + val now: Instant, ) : AgendaWidgetData } @@ -108,6 +116,7 @@ internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData { val ep = widgetEntryPoint() val prefs = ep.settingsPrefs() val savedRange = prefs.agendaWidgetRange.first() + val savedPastDisplay = prefs.pastEventDisplay.first() val weekStart = prefs.weekStart.first().resolveFirstDay(Locale.getDefault()) // Load the widest selectable window once; the displayed range is sliced in // the composition from Glance state, so changing the range is a plain @@ -122,6 +131,8 @@ internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData { is24Hour = is24Hour, weekStart = weekStart, savedRange = savedRange, + savedPastDisplay = savedPastDisplay, + now = Clock.System.now(), ) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt index 4d5c77d..19c06c5 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt @@ -43,11 +43,15 @@ import androidx.glance.text.Text import androidx.glance.text.TextStyle import de.jeanlucmakiola.calendula.MainActivity import de.jeanlucmakiola.calendula.R +import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay +import de.jeanlucmakiola.calendula.data.prefs.parsePastEventDisplay import de.jeanlucmakiola.calendula.domain.EventInstance +import de.jeanlucmakiola.calendula.domain.hasEnded import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.agenda.dayCount import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay import de.jeanlucmakiola.calendula.ui.common.pastelize import de.jeanlucmakiola.calendula.widget.AgendaWidgetData @@ -79,6 +83,14 @@ import java.util.Locale */ internal val AGENDA_RANGE_KEY = stringPreferencesKey("agenda_range") +/** + * Per-instance Glance state key holding the past-event display mode (as + * [PastEventDisplay.name]). Read reactively in the composition for the same + * reason as [AGENDA_RANGE_KEY] — so toggling the setting reflects on the live + * widget without depending on the `provideGlance` preamble re-running. + */ +internal val AGENDA_PAST_DISPLAY_KEY = stringPreferencesKey("agenda_past_display") + class AgendaWidget : GlanceAppWidget() { override val stateDefinition = PreferencesGlanceStateDefinition @@ -125,11 +137,26 @@ private fun AgendaWidgetBody(data: AgendaWidgetData, dark: Boolean) { // to the saved pref for a freshly placed widget), then the wide // pre-loaded window is sliced to it — pure recomposition. val range = parseAgendaRange(currentState(AGENDA_RANGE_KEY), data.savedRange) + val pastDisplay = + parsePastEventDisplay(currentState(AGENDA_PAST_DISPLAY_KEY), data.savedPastDisplay) val rangeEnd = data.today.plus( range.dayCount(data.today, data.weekStart) - 1, DateTimeUnit.DAY, ) - val visibleDays = data.days.filter { it.date <= rangeEnd } + // Slice to the chosen range, then drop finished events (and any day + // they leave empty) when the mode is Hide — so "Upcoming" really is. + val visibleDays = data.days + .filter { it.date <= rangeEnd } + .let { days -> + if (pastDisplay == PastEventDisplay.HIDE) { + days.mapNotNull { day -> + val remaining = day.events.filterNot { it.hasEnded(data.now) } + if (remaining.isEmpty()) null else day.copy(events = remaining) + } + } else { + days + } + } if (visibleDays.isEmpty()) { WidgetMessage(R.string.agenda_empty_title) } else { @@ -143,7 +170,13 @@ private fun AgendaWidgetBody(data: AgendaWidgetData, dark: Boolean) { items(rows.size) { index -> when (val row = rows[index]) { is AgendaRow.Header -> DayHeaderRow(row.date, row.today) - is AgendaRow.Event -> EventRow(row.event, dark, data.is24Hour) + is AgendaRow.Event -> EventRow( + event = row.event, + dark = dark, + is24Hour = data.is24Hour, + dimmed = pastDisplay == PastEventDisplay.DIM && + row.event.hasEnded(data.now), + ) } } } @@ -224,9 +257,15 @@ private fun DayHeaderRow(date: LocalDate, today: LocalDate) { } @Composable -private fun EventRow(event: EventInstance, dark: Boolean, is24Hour: Boolean) { +private fun EventRow(event: EventInstance, dark: Boolean, is24Hour: Boolean, dimmed: Boolean) { val context = androidx.glance.LocalContext.current val title = event.title.ifBlank { context.getString(R.string.event_untitled) } + // Glance has no generic alpha modifier, so dim by fading the colour stripe and + // dropping both text lines to the lower-emphasis on-surface-variant tone. + val stripeColor = pastelize(event.color, dark).let { + if (dimmed) it.copy(alpha = EventDimAlpha) else it + } + val titleColor = if (dimmed) GlanceTheme.colors.onSurfaceVariant else GlanceTheme.colors.onSurface Row( modifier = GlanceModifier .fillMaxWidth() @@ -249,14 +288,14 @@ private fun EventRow(event: EventInstance, dark: Boolean, is24Hour: Boolean) { .width(5.dp) .height(36.dp) .cornerRadius(3.dp) - .background(pastelize(event.color, dark)), + .background(stripeColor), ) {} Spacer(GlanceModifier.width(10.dp)) Column(modifier = GlanceModifier.defaultWeight()) { Text( text = title, maxLines = 1, - style = TextStyle(color = GlanceTheme.colors.onSurface, fontSize = 14.sp), + style = TextStyle(color = titleColor, fontSize = 14.sp), ) Text( text = eventTimeSummary(context, event, is24Hour),