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.
This commit is contained in:
19
CLAUDE.md
19
CLAUDE.md
@@ -59,12 +59,21 @@ changes on top of that:
|
|||||||
* **A new part shows today's agenda.** `CalendarPartProvider`
|
* **A new part shows today's agenda.** `CalendarPartProvider`
|
||||||
(`app/ui/.../widgets/clock/parts/`) is toggled with the *Events* switch
|
(`app/ui/.../widgets/clock/parts/`) is toggled with the *Events* switch
|
||||||
(`clockWidgetCalendarPart`). It lists the events that are still running or upcoming today, plus
|
(`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"
|
today's all-day events, capped at `clockWidgetCalendarPartMaxRows` rows (1..8, default 4) — if
|
||||||
hint that opens the calendar app. Tapping a row opens the event
|
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/<id>`,
|
(`CalendarEvent.launch`, i.e. `ACTION_VIEW` on `content://com.android.calendar/events/<id>`,
|
||||||
which KashCal handles). Rows reuse `CalendarEvent.getSummary()` from
|
which KashCal handles). Rows reuse `CalendarEvent.getSummary()` from
|
||||||
`ui/launcher/search/calendar/CalendarItem.kt` (made `internal` for this), so an all-day event
|
`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".
|
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
|
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
|
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.
|
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
|
The clock settings themselves are in `core/preferences`: `LauncherSettingsData` (new fields
|
||||||
`clockWidgetCalendarPart`, `clockWidgetDynamicZoneSlots`), `ClockWidgetParts` and the
|
`clockWidgetCalendarPart`, `clockWidgetCalendarPartExcludedCalendars`,
|
||||||
`ClockWidgetSettings` accessors in `core/preferences/.../ui/ClockWidgetSettings.kt`, and the UI
|
`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
|
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.
|
`core/i18n/src/main/res/values/strings.xml`; translations come from Crowdin upstream.
|
||||||
|
|
||||||
|
|||||||
@@ -709,6 +709,14 @@ fun ConfigureClockWidgetSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (smartspacer == false) {
|
||||||
|
AnimatedVisibility(visible = parts?.calendar == true) {
|
||||||
|
ConfigureCalendarPart(
|
||||||
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ import de.mm20.launcher2.permissions.PermissionGroup
|
|||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.TimeFormat
|
import de.mm20.launcher2.preferences.TimeFormat
|
||||||
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
|
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
|
||||||
|
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
|
||||||
import de.mm20.launcher2.search.CalendarEvent
|
import de.mm20.launcher2.search.CalendarEvent
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.locals.LocalTimeFormat
|
import de.mm20.launcher2.ui.locals.LocalTimeFormat
|
||||||
@@ -65,10 +66,10 @@ import java.time.OffsetDateTime
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highest number of rows the agenda shows. If there are more events than this, the last
|
* Number of rows the agenda shows before it collapses into a "+N more events" row, until the
|
||||||
* row is a "+N more events" hint.
|
* 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. */
|
/** Re-query interval, so the agenda survives calendar changes that don't notify. */
|
||||||
private const val RefreshInterval = 15 * 60 * 1000L
|
private const val RefreshInterval = 15 * 60 * 1000L
|
||||||
@@ -96,13 +97,17 @@ class CalendarPartProvider : PartProvider, KoinComponent {
|
|||||||
private val calendarRepository: CalendarRepository by inject()
|
private val calendarRepository: CalendarRepository by inject()
|
||||||
private val permissionsManager: PermissionsManager by inject()
|
private val permissionsManager: PermissionsManager by inject()
|
||||||
private val searchSettings: CalendarSearchSettings by inject()
|
private val searchSettings: CalendarSearchSettings by inject()
|
||||||
|
private val clockWidgetSettings: ClockWidgetSettings by inject()
|
||||||
|
|
||||||
private val agenda = MutableStateFlow<List<CalendarEvent>>(emptyList())
|
private val agenda = MutableStateFlow<List<CalendarEvent>>(emptyList())
|
||||||
|
|
||||||
|
private val maxRows = MutableStateFlow(DefaultMaxRows)
|
||||||
|
|
||||||
override fun getRanking(context: Context): Flow<Int> = channelFlow {
|
override fun getRanking(context: Context): Flow<Int> = channelFlow {
|
||||||
// Load the agenda in the background; the ranking itself only depends on the
|
// Load the agenda in the background; the ranking itself only depends on the
|
||||||
// already known agenda, so it can be emitted right away.
|
// already known agenda, so it can be emitted right away.
|
||||||
launch { observeAgenda(context) }
|
launch { observeAgenda(context) }
|
||||||
|
launch { clockWidgetSettings.calendarPartMaxRows.collect { maxRows.value = it } }
|
||||||
agenda.map { ranking(it) }
|
agenda.map { ranking(it) }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect { send(it) }
|
.collect { send(it) }
|
||||||
@@ -112,8 +117,17 @@ class CalendarPartProvider : PartProvider, KoinComponent {
|
|||||||
combine(
|
combine(
|
||||||
dayChanges(context),
|
dayChanges(context),
|
||||||
permissionsManager.hasPermission(PermissionGroup.Calendar),
|
permissionsManager.hasPermission(PermissionGroup.Calendar),
|
||||||
|
clockWidgetSettings.calendarPartExcludedCalendars,
|
||||||
searchSettings.excludedCalendars,
|
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) ->
|
.collectLatest { (now, hasPermission, excludedCalendars) ->
|
||||||
agenda.value = if (!hasPermission) {
|
agenda.value = if (!hasPermission) {
|
||||||
emptyList()
|
emptyList()
|
||||||
@@ -176,13 +190,15 @@ class CalendarPartProvider : PartProvider, KoinComponent {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Component(compactLayout: Boolean) {
|
override fun Component(compactLayout: Boolean) {
|
||||||
val events by agenda.collectAsState()
|
val events by agenda.collectAsState()
|
||||||
|
val maxRows by this.maxRows.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
if (events.isEmpty()) return
|
if (events.isEmpty()) return
|
||||||
|
|
||||||
val timeFormat = LocalTimeFormat.current
|
val timeFormat = LocalTimeFormat.current
|
||||||
val contentColor = LocalContentColor.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
|
val hiddenCount = events.size - visibleCount
|
||||||
|
|
||||||
Column(
|
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<String>,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time of an event, or "all-day" for all-day events. Never a date.
|
* The time of an event, or "all-day" for all-day events. Never a date.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
ROUTE_MEDIA_INTEGRATION -> MediaIntegrationSettingsRoute
|
ROUTE_MEDIA_INTEGRATION -> MediaIntegrationSettingsRoute
|
||||||
ROUTE_SEARCH_ACTIONS -> SearchActionsSettingsRoute
|
ROUTE_SEARCH_ACTIONS -> SearchActionsSettingsRoute
|
||||||
ROUTE_HIDDEN_ITEMS -> HiddenItemsSettingsRoute
|
ROUTE_HIDDEN_ITEMS -> HiddenItemsSettingsRoute
|
||||||
|
ROUTE_SEARCH_CALENDARS -> CalendarSearchSettingsRoute
|
||||||
ROUTE_CRASH_REPORT if (intent.hasExtra(EXTRA_CRASH_REPORT_PATH)) -> {
|
ROUTE_CRASH_REPORT if (intent.hasExtra(EXTRA_CRASH_REPORT_PATH)) -> {
|
||||||
CrashReportRoute(intent.getStringExtra(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_MEDIA_INTEGRATION = "settings/integrations/media"
|
||||||
const val ROUTE_SEARCH_ACTIONS = "settings/search/searchactions"
|
const val ROUTE_SEARCH_ACTIONS = "settings/search/searchactions"
|
||||||
const val ROUTE_HIDDEN_ITEMS = "settings/search/hiddenitems"
|
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 ROUTE_CRASH_REPORT = "settings/debug/crashreport"
|
||||||
const val EXTRA_CRASH_REPORT_PATH = "crash_report_path"
|
const val EXTRA_CRASH_REPORT_PATH = "crash_report_path"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ package de.mm20.launcher2.ui.settings.clockwidget
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.BatteryStatusVisibility
|
||||||
import de.mm20.launcher2.preferences.ClockWidgetAlignment
|
import de.mm20.launcher2.preferences.ClockWidgetAlignment
|
||||||
import de.mm20.launcher2.preferences.ClockWidgetColors
|
import de.mm20.launcher2.preferences.ClockWidgetColors
|
||||||
import de.mm20.launcher2.preferences.ClockWidgetStyle
|
import de.mm20.launcher2.preferences.ClockWidgetStyle
|
||||||
import de.mm20.launcher2.preferences.TimeFormat
|
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.ClockWidgetSettings
|
||||||
import de.mm20.launcher2.preferences.ui.UiSettings
|
import de.mm20.launcher2.preferences.ui.UiSettings
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -19,6 +24,9 @@ import org.koin.core.component.inject
|
|||||||
class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
|
class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
|
||||||
private val settings: ClockWidgetSettings by inject()
|
private val settings: ClockWidgetSettings by inject()
|
||||||
private val uiSettings: UiSettings 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
|
val compact = settings.compact
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
@@ -106,6 +114,40 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
settings.setCalendarPart(calendarPart)
|
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<String>())
|
||||||
|
|
||||||
|
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<CalendarList>())
|
||||||
|
|
||||||
|
/** Whether the agenda can show anything at all. */
|
||||||
|
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), true)
|
||||||
|
|
||||||
val slots = settings.slots
|
val slots = settings.slots
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
|
||||||
|
|
||||||
|
|||||||
@@ -615,6 +615,12 @@
|
|||||||
<string name="preference_clockwidget_alarm_part_summary">Show alarms that will ring within the next 24 hours</string>
|
<string name="preference_clockwidget_alarm_part_summary">Show alarms that will ring within the next 24 hours</string>
|
||||||
<string name="preference_clockwidget_calendar_part">Events</string>
|
<string name="preference_clockwidget_calendar_part">Events</string>
|
||||||
<string name="preference_clockwidget_calendar_part_summary">Show today\'s agenda</string>
|
<string name="preference_clockwidget_calendar_part_summary">Show today\'s agenda</string>
|
||||||
|
<string name="preference_clockwidget_calendar_part_events">Visible events</string>
|
||||||
|
<string name="preference_clockwidget_calendar_part_calendars">Calendars</string>
|
||||||
|
<string name="preference_clockwidget_calendar_part_all_calendars">All calendars</string>
|
||||||
|
<string name="preference_clockwidget_calendar_part_calendars_summary">%1$d of %2$d calendars</string>
|
||||||
|
<string name="preference_clockwidget_calendar_part_no_calendars">No calendars found</string>
|
||||||
|
<string name="preference_clockwidget_calendar_part_manage_calendars">Manage calendars</string>
|
||||||
<string name="clockwidget_calendar_part_all_day">all-day</string>
|
<string name="clockwidget_calendar_part_all_day">all-day</string>
|
||||||
<plurals name="clockwidget_calendar_part_more_events">
|
<plurals name="clockwidget_calendar_part_more_events">
|
||||||
<item quantity="one">+%d more event</item>
|
<item quantity="one">+%d more event</item>
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ data class LauncherSettingsData internal constructor(
|
|||||||
val clockWidgetMusicPart: Boolean = true,
|
val clockWidgetMusicPart: Boolean = true,
|
||||||
val clockWidgetDatePart: Boolean = true,
|
val clockWidgetDatePart: Boolean = true,
|
||||||
val clockWidgetCalendarPart: Boolean = false,
|
val clockWidgetCalendarPart: Boolean = false,
|
||||||
|
val clockWidgetCalendarPartExcludedCalendars: Set<String> = emptySet(),
|
||||||
|
val clockWidgetCalendarPartMaxRows: Int = 4,
|
||||||
val clockWidgetDynamicZoneSlots: Int = 1,
|
val clockWidgetDynamicZoneSlots: Int = 1,
|
||||||
val clockWidgetFillHeight: Boolean = false,
|
val clockWidgetFillHeight: Boolean = false,
|
||||||
val clockWidgetAlignment: ClockWidgetAlignment = ClockWidgetAlignment.Bottom,
|
val clockWidgetAlignment: ClockWidgetAlignment = ClockWidgetAlignment.Bottom,
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
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(
|
data class ClockWidgetParts(
|
||||||
val date: Boolean,
|
val date: Boolean,
|
||||||
val music: Boolean = false,
|
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) {
|
fun setDatePart(datePart: Boolean) {
|
||||||
launcherDataStore.update {
|
launcherDataStore.update {
|
||||||
it.copy(clockWidgetDatePart = datePart)
|
it.copy(clockWidgetDatePart = datePart)
|
||||||
|
|||||||
Reference in New Issue
Block a user