From c824b100b682516319df1d0ac8e495f90aee9c2f Mon Sep 17 00:00:00 2001 From: Jonas Haugesen Date: Thu, 17 Sep 2026 08:56:55 +0200 Subject: [PATCH] Make the clock agenda's calendars and row count configurable The agenda part filtered calendars through the search settings and always collapsed after four rows. It now has its own selection: an exclusion set (`clockWidgetCalendarPartExcludedCalendars`, empty = all) that is added to the search settings' exclusions, because the picker only offers calendars those settings leave enabled. The row cap is `clockWidgetCalendarPartMaxRows` (1..8, default 4); the agenda shows that many events and then a "+N more events" row if more are left, so with the default five events now render as four rows plus "+1 more event" instead of three plus "+2". Both are configured in `ConfigureCalendarPart`, shown by the clock sheet while the Events part is enabled: a slider, a Calendars row that expands into a colour-tinted checkbox per calendar, and a button to Settings -> Search -> Calendar (new `ROUTE_SEARCH_CALENDARS`) for calendars the search settings disable. A missing calendar permission shows a banner with a Grant button. New setting fields default to the previous behaviour, so no datastore migration is needed. --- CLAUDE.md | 19 +- .../ui/launcher/widgets/clock/ClockWidget.kt | 8 + .../widgets/clock/ConfigureCalendarPart.kt | 181 ++++++++++++++++++ .../clock/parts/CalendarPartProvider.kt | 33 +++- .../launcher2/ui/settings/SettingsActivity.kt | 2 + .../ClockWidgetSettingsScreenVM.kt | 42 ++++ core/i18n/src/main/res/values/strings.xml | 6 + .../preferences/LauncherSettingsData.kt | 2 + .../preferences/ui/ClockWidgetSettings.kt | 37 ++++ 9 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ConfigureCalendarPart.kt diff --git a/CLAUDE.md b/CLAUDE.md index 4da4aef54..355990acf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,12 +59,21 @@ changes on top of that: * **A new part shows today's agenda.** `CalendarPartProvider` (`app/ui/.../widgets/clock/parts/`) is toggled with the *Events* switch (`clockWidgetCalendarPart`). It lists the events that are still running or upcoming today, plus - today's all-day events, capped at 4 rows — if there are more, the last row is a "+N more events" - hint that opens the calendar app. Tapping a row opens the event + today's all-day events, capped at `clockWidgetCalendarPartMaxRows` rows (1..8, default 4) — if + there are more, the rows are followed by a "+N more events" hint that opens the calendar app. + Tapping a row opens the event (`CalendarEvent.launch`, i.e. `ACTION_VIEW` on `content://com.android.calendar/events/`, which KashCal handles). Rows reuse `CalendarEvent.getSummary()` from `ui/launcher/search/calendar/CalendarItem.kt` (made `internal` for this), so an all-day event reads "All day" and a timed one reads e.g. "09:00 – 09:30". +* **The agenda picks its calendars.** `clockWidgetCalendarPartExcludedCalendars` (empty = all) + hides individual calendars; the query's exclusion set is that set plus the search settings' + exclusions, because the picker only ever offers calendars the search settings leave enabled + (`ClockWidgetSettingsScreenVM.calendars` filters them out of `CalendarRepository.getCalendars()`, + and a "Manage calendars" button opens `Settings → Search → Calendar` via the + `ROUTE_SEARCH_CALENDARS` route in `SettingsActivity`). The whole calendar configuration lives in + `ConfigureCalendarPart.kt`, shown by `ConfigureClockWidgetSheet` while the Events part is on — + the agenda deliberately ignores the calendar *widget*'s own `excludedCalendarIds`. Rankings, in the order the zone prefers them: `CalendarPartProvider` 70 when an event is running or starts within 30 minutes, else 30 while there are events left today, else 0 (so the date part keeps @@ -78,8 +87,10 @@ launcher only re-query when they become visible). `PartProvider.setTime()` is ca by the clock widget; the provider folds that into its ranking instead of re-querying. The clock settings themselves are in `core/preferences`: `LauncherSettingsData` (new fields -`clockWidgetCalendarPart`, `clockWidgetDynamicZoneSlots`), `ClockWidgetParts` and the -`ClockWidgetSettings` accessors in `core/preferences/.../ui/ClockWidgetSettings.kt`, and the UI +`clockWidgetCalendarPart`, `clockWidgetCalendarPartExcludedCalendars`, +`clockWidgetCalendarPartMaxRows`, `clockWidgetDynamicZoneSlots`), `ClockWidgetParts` and the +`ClockWidgetSettings` accessors in `core/preferences/.../ui/ClockWidgetSettings.kt` (including the +`MinCalendarPartRows`/`MaxCalendarPartRows` bounds), and the UI state in `ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt`. New strings are English-only in `core/i18n/src/main/res/values/strings.xml`; translations come from Crowdin upstream. diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt index 1d4ddc356..0ed53cc7c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt @@ -709,6 +709,14 @@ fun ConfigureClockWidgetSheet( } } } + if (smartspacer == false) { + AnimatedVisibility(visible = parts?.calendar == true) { + ConfigureCalendarPart( + modifier = Modifier.padding(top = 16.dp), + viewModel = viewModel, + ) + } + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ConfigureCalendarPart.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ConfigureCalendarPart.kt new file mode 100644 index 000000000..6aa3db87c --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ConfigureCalendarPart.kt @@ -0,0 +1,181 @@ +package de.mm20.launcher2.ui.launcher.widgets.clock + +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.mm20.launcher2.permissions.PermissionGroup +import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.preferences.ui.MaxCalendarPartRows +import de.mm20.launcher2.preferences.ui.MinCalendarPartRows +import de.mm20.launcher2.themes.colors.atTone +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.MissingPermissionBanner +import de.mm20.launcher2.ui.component.preferences.CheckboxPreference +import de.mm20.launcher2.ui.component.preferences.Preference +import de.mm20.launcher2.ui.component.preferences.SliderPreference +import de.mm20.launcher2.ui.locals.LocalDarkTheme +import de.mm20.launcher2.ui.settings.SettingsActivity +import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreenVM +import org.koin.compose.koinInject + +/** + * Settings for the agenda in the clock widget's dynamic zone: how many events are shown + * before they collapse into a "+N more events" row, and which calendars they come from. + */ +@Composable +internal fun ConfigureCalendarPart( + viewModel: ClockWidgetSettingsScreenVM, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val activity = LocalLifecycleOwner.current as? AppCompatActivity + val permissionsManager: PermissionsManager = koinInject() + + val calendars by viewModel.calendars.collectAsStateWithLifecycle() + val excludedCalendars by viewModel.calendarPartExcludedCalendars.collectAsStateWithLifecycle() + val maxRows by viewModel.calendarPartMaxRows.collectAsStateWithLifecycle() + val hasPermission by viewModel.hasCalendarPermission.collectAsStateWithLifecycle() + + var showCalendars by remember { mutableStateOf(false) } + + val shownCalendars = calendars.count { it.id !in excludedCalendars } + + Column(modifier = modifier) { + if (!hasPermission) { + MissingPermissionBanner( + modifier = Modifier.padding(bottom = 8.dp), + text = stringResource(R.string.missing_permission_calendar_widget_settings), + onClick = { + activity?.let { + permissionsManager.requestPermission(it, PermissionGroup.Calendar) + } + }, + ) + } + OutlinedCard( + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + SliderPreference( + title = stringResource(R.string.preference_clockwidget_calendar_part_events), + icon = R.drawable.event_24px, + value = maxRows, + min = MinCalendarPartRows, + max = MaxCalendarPartRows, + step = 1, + onValueChanged = { + viewModel.setCalendarPartMaxRows(it) + } + ) + HorizontalDivider() + Preference( + title = stringResource(R.string.preference_clockwidget_calendar_part_calendars), + summary = when { + calendars.isEmpty() -> stringResource(R.string.preference_clockwidget_calendar_part_no_calendars) + shownCalendars == calendars.size -> stringResource(R.string.preference_clockwidget_calendar_part_all_calendars) + else -> stringResource( + R.string.preference_clockwidget_calendar_part_calendars_summary, + shownCalendars, + calendars.size, + ) + }, + icon = R.drawable.calendar_today_24px, + onClick = { showCalendars = !showCalendars }, + enabled = calendars.isNotEmpty(), + controls = { + Icon( + painter = painterResource( + if (showCalendars) R.drawable.keyboard_arrow_up_24px + else R.drawable.keyboard_arrow_down_24px + ), + contentDescription = null, + ) + }, + ) + AnimatedVisibility( + visible = showCalendars && calendars.isNotEmpty(), + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + HorizontalDivider() + for (calendar in calendars) { + CheckboxPreference( + title = calendar.name, + summary = calendar.owner, + iconPadding = false, + value = calendar.id !in excludedCalendars, + onValueChanged = { + viewModel.setCalendarExcluded(calendar.id, !it) + }, + checkboxColors = CheckboxDefaults.colors( + checkedColor = if (calendar.color == 0) MaterialTheme.colorScheme.primary + else Color( + calendar.color.atTone(if (LocalDarkTheme.current) 80 else 40) + ), + checkmarkColor = if (calendar.color == 0) MaterialTheme.colorScheme.onPrimary + else Color( + calendar.color.atTone(if (LocalDarkTheme.current) 20 else 100) + ) + ) + ) + } + } + } + } + } + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally), + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + onClick = { + context.startActivity( + Intent(context, SettingsActivity::class.java).apply { + putExtra( + SettingsActivity.EXTRA_ROUTE, + SettingsActivity.ROUTE_SEARCH_CALENDARS, + ) + } + ) + }, + ) { + Text(stringResource(R.string.preference_clockwidget_calendar_part_manage_calendars)) + Icon( + modifier = Modifier + .padding(start = ButtonDefaults.IconSpacing) + .requiredSize(ButtonDefaults.IconSize), + painter = painterResource(R.drawable.open_in_new_20px), + contentDescription = null, + ) + } + } +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/CalendarPartProvider.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/CalendarPartProvider.kt index 672344f7b..3e4821e11 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/CalendarPartProvider.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/CalendarPartProvider.kt @@ -39,6 +39,7 @@ import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.TimeFormat import de.mm20.launcher2.preferences.search.CalendarSearchSettings +import de.mm20.launcher2.preferences.ui.ClockWidgetSettings import de.mm20.launcher2.search.CalendarEvent import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.locals.LocalTimeFormat @@ -65,10 +66,10 @@ import java.time.OffsetDateTime import java.util.Date /** - * Highest number of rows the agenda shows. If there are more events than this, the last - * row is a "+N more events" hint. + * Number of rows the agenda shows before it collapses into a "+N more events" row, until the + * setting has been read. */ -private const val MaxRows = 4 +private const val DefaultMaxRows = 4 /** Re-query interval, so the agenda survives calendar changes that don't notify. */ private const val RefreshInterval = 15 * 60 * 1000L @@ -96,13 +97,17 @@ class CalendarPartProvider : PartProvider, KoinComponent { private val calendarRepository: CalendarRepository by inject() private val permissionsManager: PermissionsManager by inject() private val searchSettings: CalendarSearchSettings by inject() + private val clockWidgetSettings: ClockWidgetSettings by inject() private val agenda = MutableStateFlow>(emptyList()) + private val maxRows = MutableStateFlow(DefaultMaxRows) + override fun getRanking(context: Context): Flow = channelFlow { // Load the agenda in the background; the ranking itself only depends on the // already known agenda, so it can be emitted right away. launch { observeAgenda(context) } + launch { clockWidgetSettings.calendarPartMaxRows.collect { maxRows.value = it } } agenda.map { ranking(it) } .distinctUntilChanged() .collect { send(it) } @@ -112,8 +117,17 @@ class CalendarPartProvider : PartProvider, KoinComponent { combine( dayChanges(context), permissionsManager.hasPermission(PermissionGroup.Calendar), + clockWidgetSettings.calendarPartExcludedCalendars, searchSettings.excludedCalendars, - ) { now, hasPermission, excludedCalendars -> Triple(now, hasPermission, excludedCalendars) } + ) { now, hasPermission, hiddenInWidget, excludedInSearch -> + AgendaInputs( + now = now, + hasCalendarPermission = hasPermission, + // Calendars the user disabled in the search settings cannot be picked in the + // widget either, so the two sets just add up. + excludedCalendars = if (hasPermission) hiddenInWidget + excludedInSearch else emptySet(), + ) + } .collectLatest { (now, hasPermission, excludedCalendars) -> agenda.value = if (!hasPermission) { emptyList() @@ -176,13 +190,15 @@ class CalendarPartProvider : PartProvider, KoinComponent { @Composable override fun Component(compactLayout: Boolean) { val events by agenda.collectAsState() + val maxRows by this.maxRows.collectAsState() val context = LocalContext.current if (events.isEmpty()) return val timeFormat = LocalTimeFormat.current val contentColor = LocalContentColor.current - val visibleCount = if (events.size > MaxRows) MaxRows - 1 else events.size + // The configured number of events, then a "+N more events" row if there are more. + val visibleCount = minOf(events.size, maxRows) val hiddenCount = events.size - visibleCount Column( @@ -216,6 +232,13 @@ class CalendarPartProvider : PartProvider, KoinComponent { } } +/** The three inputs the agenda query depends on. */ +private data class AgendaInputs( + val now: Long, + val hasCalendarPermission: Boolean, + val excludedCalendars: Set, +) + /** * The time of an event, or "all-day" for all-day events. Never a date. */ diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index 9cd0e904d..cfeaa01f8 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -422,6 +422,7 @@ class SettingsActivity : BaseActivity() { ROUTE_MEDIA_INTEGRATION -> MediaIntegrationSettingsRoute ROUTE_SEARCH_ACTIONS -> SearchActionsSettingsRoute ROUTE_HIDDEN_ITEMS -> HiddenItemsSettingsRoute + ROUTE_SEARCH_CALENDARS -> CalendarSearchSettingsRoute ROUTE_CRASH_REPORT if (intent.hasExtra(EXTRA_CRASH_REPORT_PATH)) -> { CrashReportRoute(intent.getStringExtra(EXTRA_CRASH_REPORT_PATH)!!) } @@ -435,6 +436,7 @@ class SettingsActivity : BaseActivity() { const val ROUTE_MEDIA_INTEGRATION = "settings/integrations/media" const val ROUTE_SEARCH_ACTIONS = "settings/search/searchactions" const val ROUTE_HIDDEN_ITEMS = "settings/search/hiddenitems" + const val ROUTE_SEARCH_CALENDARS = "settings/search/calendar" const val ROUTE_CRASH_REPORT = "settings/debug/crashreport" const val EXTRA_CRASH_REPORT_PATH = "crash_report_path" } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt index 57d938d77..89200a74a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt @@ -2,11 +2,16 @@ package de.mm20.launcher2.ui.settings.clockwidget import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import de.mm20.launcher2.calendar.CalendarRepository +import de.mm20.launcher2.calendar.providers.CalendarList +import de.mm20.launcher2.permissions.PermissionGroup +import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.BatteryStatusVisibility import de.mm20.launcher2.preferences.ClockWidgetAlignment import de.mm20.launcher2.preferences.ClockWidgetColors import de.mm20.launcher2.preferences.ClockWidgetStyle import de.mm20.launcher2.preferences.TimeFormat +import de.mm20.launcher2.preferences.search.CalendarSearchSettings import de.mm20.launcher2.preferences.ui.ClockWidgetSettings import de.mm20.launcher2.preferences.ui.UiSettings import kotlinx.coroutines.flow.SharingStarted @@ -19,6 +24,9 @@ import org.koin.core.component.inject class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent { private val settings: ClockWidgetSettings by inject() private val uiSettings: UiSettings by inject() + private val calendarRepository: CalendarRepository by inject() + private val calendarSearchSettings: CalendarSearchSettings by inject() + private val permissionsManager: PermissionsManager by inject() val compact = settings.compact .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) @@ -106,6 +114,40 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent { settings.setCalendarPart(calendarPart) } + /** + * Number of event rows in the agenda before it collapses into a "+N more events" row. + */ + val calendarPartMaxRows = settings.calendarPartMaxRows + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 4) + + fun setCalendarPartMaxRows(rows: Int) { + settings.setCalendarPartMaxRows(rows) + } + + val calendarPartExcludedCalendars = settings.calendarPartExcludedCalendars + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptySet()) + + fun setCalendarExcluded(calendarId: String, excluded: Boolean) { + settings.setCalendarExcluded(calendarId, excluded) + } + + /** + * Calendars the agenda can show, i.e. every calendar the search settings make available. + * Calendars disabled there are missing here on purpose. + */ + val calendars = combine( + calendarRepository.getCalendars(), + calendarSearchSettings.excludedCalendars, + ) { calendars, excluded -> + calendars + .filter { it.id !in excluded } + .sortedWith(compareBy({ it.owner ?: "" }, { it.name })) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + /** Whether the agenda can show anything at all. */ + val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), true) + val slots = settings.slots .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1) diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 30a9b415f..e46f8fc83 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -615,6 +615,12 @@ Show alarms that will ring within the next 24 hours Events Show today\'s agenda + Visible events + Calendars + All calendars + %1$d of %2$d calendars + No calendars found + Manage calendars all-day +%d more event diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index 64b116aec..6ee430f8b 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -59,6 +59,8 @@ data class LauncherSettingsData internal constructor( val clockWidgetMusicPart: Boolean = true, val clockWidgetDatePart: Boolean = true, val clockWidgetCalendarPart: Boolean = false, + val clockWidgetCalendarPartExcludedCalendars: Set = emptySet(), + val clockWidgetCalendarPartMaxRows: Int = 4, val clockWidgetDynamicZoneSlots: Int = 1, val clockWidgetFillHeight: Boolean = false, val clockWidgetAlignment: ClockWidgetAlignment = ClockWidgetAlignment.Bottom, diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/ClockWidgetSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/ClockWidgetSettings.kt index 7ae46a3a3..d19e73c54 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/ClockWidgetSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/ClockWidgetSettings.kt @@ -11,6 +11,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +/** Bounds for the number of event rows the agenda shows before collapsing. */ +const val MinCalendarPartRows = 1 +const val MaxCalendarPartRows = 8 + data class ClockWidgetParts( val date: Boolean, val music: Boolean = false, @@ -61,6 +65,39 @@ class ClockWidgetSettings internal constructor( } } + /** + * Calendars the agenda does not show. Empty means "all calendars". + */ + val calendarPartExcludedCalendars + get() = launcherDataStore.data.map { it.clockWidgetCalendarPartExcludedCalendars } + .distinctUntilChanged() + + fun setCalendarExcluded(calendarId: String, excluded: Boolean) { + launcherDataStore.update { + it.copy( + clockWidgetCalendarPartExcludedCalendars = + if (excluded) it.clockWidgetCalendarPartExcludedCalendars + calendarId + else it.clockWidgetCalendarPartExcludedCalendars - calendarId + ) + } + } + + /** + * Number of event rows in the agenda before it collapses into a "+N more events" row. + */ + val calendarPartMaxRows + get() = launcherDataStore.data + .map { it.clockWidgetCalendarPartMaxRows.coerceIn(MinCalendarPartRows, MaxCalendarPartRows) } + .distinctUntilChanged() + + fun setCalendarPartMaxRows(rows: Int) { + launcherDataStore.update { + it.copy( + clockWidgetCalendarPartMaxRows = rows.coerceIn(MinCalendarPartRows, MaxCalendarPartRows) + ) + } + } + fun setDatePart(datePart: Boolean) { launcherDataStore.update { it.copy(clockWidgetDatePart = datePart)