Restructure calendar widget settings

This commit is contained in:
MM20 2024-08-16 23:26:26 +02:00
parent bf29ce6f08
commit cc9bff436a
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
8 changed files with 157 additions and 87 deletions

View File

@ -44,6 +44,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.roundToIntRect
@ -121,7 +122,13 @@ fun CalendarItem(
this@AnimatedContent this@AnimatedContent
), ),
text = calendar.labelOverride ?: calendar.label, text = calendar.labelOverride ?: calendar.label,
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
maxLines = 1,
textDecoration = if (calendar.isCompleted == true) {
TextDecoration.LineThrough
} else {
TextDecoration.None
}
) )
if (calendar.calendarName != null) { if (calendar.calendarName != null) {
Text( Text(
@ -300,7 +307,13 @@ fun CalendarItem(
this@AnimatedContent this@AnimatedContent
), ),
text = calendar.labelOverride ?: calendar.label, text = calendar.labelOverride ?: calendar.label,
style = MaterialTheme.typography.titleSmall style = MaterialTheme.typography.titleSmall,
maxLines = 1,
textDecoration = if (calendar.isCompleted == true) {
TextDecoration.LineThrough
} else {
TextDecoration.None
}
) )
Text( Text(
modifier = Modifier modifier = Modifier

View File

@ -12,6 +12,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.awaitFirstDown
@ -34,6 +35,7 @@ import androidx.compose.material.icons.rounded.Link
import androidx.compose.material.icons.rounded.LinkOff import androidx.compose.material.icons.rounded.LinkOff
import androidx.compose.material.icons.rounded.OpenInNew import androidx.compose.material.icons.rounded.OpenInNew
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -42,7 +44,6 @@ import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -52,22 +53,26 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isUnspecified import androidx.compose.ui.unit.isUnspecified
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.compose.LocalLifecycleOwner
import de.mm20.launcher2.calendar.CalendarRepository import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.calendar.providers.CalendarList
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.plugin.PluginRepository
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.search.calendar.CalendarListType
import de.mm20.launcher2.themes.atTone
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.LocalAppWidgetHost import de.mm20.launcher2.ui.base.LocalAppWidgetHost
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
@ -383,7 +388,7 @@ fun ColumnScope.ConfigureAppWidget(
val minHeight = if (widgetInfo.minResizeHeight in 1..widgetInfo.minHeight) { val minHeight = if (widgetInfo.minResizeHeight in 1..widgetInfo.minHeight) {
widgetInfo.minResizeHeight.toDp() widgetInfo.minResizeHeight.toDp()
} else { } else {
widgetInfo.minHeight.toDp() widgetInfo.minHeight.toDp()
} }
DragResizeHandle( DragResizeHandle(
@ -503,66 +508,103 @@ fun ColumnScope.ConfigureCalendarWidget(
) { ) {
val calendarRepository: CalendarRepository = get() val calendarRepository: CalendarRepository = get()
val permissionsManager: PermissionsManager = get() val permissionsManager: PermissionsManager = get()
val pluginRepository: PluginRepository = get()
val calendars by remember { val calendars by remember {
calendarRepository.getCalendars().map { calendarRepository.getCalendars().map {
it.sortedBy { it.name } it.sortedBy { it.name }
} }
}.collectAsState(null) }.collectAsState(null)
val plugins by remember {
pluginRepository.findMany(
type = PluginType.Calendar,
enabled = true,
)
}.collectAsState(emptyList())
val hasPermission by remember { val hasPermission by remember {
permissionsManager.hasPermission(PermissionGroup.Calendar) permissionsManager.hasPermission(PermissionGroup.Calendar)
}.collectAsState(true) }.collectAsState(true)
OutlinedCard { val hasTasks = remember(calendars) {
Column( calendars?.any { it.types.contains(CalendarListType.Tasks) } == true
modifier = Modifier.fillMaxWidth()
) {
SwitchPreference(
title = stringResource(R.string.preference_calendar_hide_allday),
iconPadding = false,
value = !widget.config.allDayEvents,
onValueChanged = {
onWidgetUpdated(widget.copy(config = widget.config.copy(allDayEvents = !it)))
}
)
}
} }
Text(
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp), AnimatedVisibility(hasTasks) {
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.secondary,
text = stringResource(R.string.preference_calendar_calendars)
)
val context = LocalLifecycleOwner.current as AppCompatActivity
val excludedCalendars = remember(widget.config) {
widget.config.excludedCalendarIds ?: widget.config.legacyExcludedCalendarIds?.map { "local:$it" } ?: emptyList()
}
if (calendars?.isNotEmpty() == true) {
OutlinedCard { OutlinedCard {
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
for ((i, calendar) in calendars!!.withIndex()) { SwitchPreference(
if (i > 0) HorizontalDivider() title = stringResource(R.string.preference_calendar_hide_completed),
CheckboxPreference( iconPadding = false,
title = calendar.name, value = !widget.config.completedTasks,
summary = calendar.owner, onValueChanged = {
iconPadding = false, onWidgetUpdated(widget.copy(config = widget.config.copy(completedTasks = !it)))
value = excludedCalendars.contains(calendar.id) != true, }
onValueChanged = { )
onWidgetUpdated( }
widget.copy( }
config = widget.config.copy( }
excludedCalendarIds = if (it) { val context = LocalLifecycleOwner.current as AppCompatActivity
excludedCalendars - calendar.id val excludedCalendars = remember(widget.config) {
} else { widget.config.excludedCalendarIds
excludedCalendars + calendar.id ?: widget.config.legacyExcludedCalendarIds?.map { "local:$it" } ?: emptyList()
} }
val groups = remember(calendars) {
calendars?.groupBy { it.providerId }?.entries
}
if (groups?.isNotEmpty() == true) {
for (group in groups) {
val pluginName = remember(plugins, group.key) {
if (group.key == "local") context.getString(R.string.preference_calendar_calendars)
else plugins.find { it.authority == group.key }?.label
}
if (pluginName != null) {
Text(
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.secondary,
text = pluginName
)
}
OutlinedCard {
Column(
modifier = Modifier.fillMaxWidth()
) {
for ((i, calendar) in group.value.withIndex()) {
if (i > 0) HorizontalDivider()
CheckboxPreference(
title = calendar.name,
summary = calendar.owner,
iconPadding = false,
value = !excludedCalendars.contains(calendar.id),
onValueChanged = {
onWidgetUpdated(
widget.copy(
config = widget.config.copy(
excludedCalendarIds = if (it) {
excludedCalendars - calendar.id
} else {
excludedCalendars + calendar.id
}
)
) )
) )
},
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)
)
) )
} )
) }
} }
} }
} }

View File

@ -208,7 +208,8 @@ fun CalendarWidget(
), ),
onClick = { onClick = {
viewModel.showAllEvents() viewModel.showAllEvents()
} },
modifier = Modifier.padding(top = 8.dp)
) )
} }
if (nextEvents.isNotEmpty()) { if (nextEvents.isNotEmpty()) {
@ -263,10 +264,11 @@ fun CalendarWidget(
@Composable @Composable
private fun Info( private fun Info(
text: String, text: String,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
) { ) {
Box( Box(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clip(MaterialTheme.shapes.small) .clip(MaterialTheme.shapes.small)
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.small) .border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.small)

View File

@ -176,7 +176,10 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
maxVisibility = VisibilityLevel.SearchOnly, maxVisibility = VisibilityLevel.SearchOnly,
limit = 9999, limit = 9999,
).collectLatest { hidden -> ).collectLatest { hidden ->
upcomingEvents = events.filter { !hidden.contains(it.key) }.sortedBy { it.startTime ?: it.endTime } upcomingEvents = events
.filter {
!hidden.contains(it.key) && !(!config.completedTasks && it.isCompleted == true)
}.sortedBy { it.startTime ?: it.endTime }
} }
} }

View File

@ -1,20 +1,19 @@
package de.mm20.launcher2.ui.settings.calendarsearch package de.mm20.launcher2.ui.settings.calendarsearch
import android.app.PendingIntent import android.app.PendingIntent
import android.util.Log
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CalendarToday
import androidx.compose.material.icons.rounded.Checklist
import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -35,14 +34,15 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.sendWithBackgroundPermission import de.mm20.launcher2.ktx.sendWithBackgroundPermission
import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.search.calendar.CalendarListType
import de.mm20.launcher2.themes.atTone import de.mm20.launcher2.themes.atTone
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalDarkTheme import de.mm20.launcher2.ui.locals.LocalDarkTheme
@Composable @Composable
@ -76,7 +76,7 @@ fun CalendarSearchSettingsScreen() {
val selectedCalendars = remember(excludedCalendars, calendarLists) { val selectedCalendars = remember(excludedCalendars, calendarLists) {
calendarLists?.count { it.providerId == "local" } calendarLists?.count { it.providerId == "local" }
?.minus(excludedCalendars.count { ?.minus(excludedCalendars.count {
it.startsWith("local") it.startsWith("local:")
}) })
} }
PreferenceWithSwitch( PreferenceWithSwitch(
@ -117,7 +117,7 @@ fun CalendarSearchSettingsScreen() {
calendarLists?.count { it.providerId == plugin.plugin.authority } calendarLists?.count { it.providerId == plugin.plugin.authority }
?.minus(excludedCalendars.count { ?.minus(excludedCalendars.count {
it.startsWith( it.startsWith(
plugin.plugin.authority "${plugin.plugin.authority}:"
) )
}) })
} }
@ -126,7 +126,7 @@ fun CalendarSearchSettingsScreen() {
enabled = state is PluginState.Ready, enabled = state is PluginState.Ready,
summary = (state as? PluginState.SetupRequired)?.message summary = (state as? PluginState.SetupRequired)?.message
?: if (selectedCalendars != null && calendarLists != null) "$selectedCalendars lists selected" ?: if (selectedCalendars != null && calendarLists != null) "$selectedCalendars lists selected"
else (state as? PluginState.Ready)?.text ?: plugin.plugin.description, else (state as? PluginState.Ready)?.text ?: plugin.plugin.description,
switchValue = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready, switchValue = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready,
onSwitchChanged = { onSwitchChanged = {
viewModel.setProviderEnabled(plugin.plugin.authority, it) viewModel.setProviderEnabled(plugin.plugin.authority, it)
@ -139,6 +139,8 @@ fun CalendarSearchSettingsScreen() {
} }
} }
Log.d("MM20", "${calendarLists.toString()}")
val dialogCalendarLists by remember { val dialogCalendarLists by remember {
derivedStateOf { derivedStateOf {
if (showDialogForProvider == null) null if (showDialogForProvider == null) null
@ -147,39 +149,41 @@ fun CalendarSearchSettingsScreen() {
} }
if (showDialogForProvider != null && dialogCalendarLists != null) { if (showDialogForProvider != null && dialogCalendarLists != null) {
BasicAlertDialog( ModalBottomSheet(
onDismissRequest = { onDismissRequest = {
showDialogForProvider = null showDialogForProvider = null
}, },
) { ) {
Surface( val groups = remember(dialogCalendarLists) {
modifier = Modifier dialogCalendarLists!!.groupBy { it.owner }.entries.sortedBy { it.key }
.wrapContentWidth() }
.wrapContentHeight(),
shape = MaterialTheme.shapes.large, LazyColumn {
tonalElevation = AlertDialogDefaults.TonalElevation items(groups) {
) { PreferenceCategory(
LazyColumn { title = it.key,
items(dialogCalendarLists ?: emptyList()) { iconPadding = false,
CheckboxPreference( ) {
title = it.name, for (list in it.value) {
summary = it.owner, CheckboxPreference(
iconPadding = false, title = list.name,
value = it.id !in excludedCalendars, iconPadding = false,
onValueChanged = { value -> value = list.id !in excludedCalendars,
viewModel.setCalendarExcluded(it.id, !value) onValueChanged = { value ->
}, viewModel.setCalendarExcluded(list.id, !value)
checkboxColors = CheckboxDefaults.colors( },
checkedColor = if (it.color == 0) MaterialTheme.colorScheme.primary checkboxColors = CheckboxDefaults.colors(
else Color( checkedColor = if (list.color == 0) MaterialTheme.colorScheme.primary
it.color.atTone(if (LocalDarkTheme.current) 80 else 40) else Color(
), list.color.atTone(if (LocalDarkTheme.current) 80 else 40)
checkmarkColor = if (it.color == 0) MaterialTheme.colorScheme.onPrimary ),
else Color( checkmarkColor = if (list.color == 0) MaterialTheme.colorScheme.onPrimary
it.color.atTone(if (LocalDarkTheme.current) 20 else 100) else Color(
list.color.atTone(if (LocalDarkTheme.current) 20 else 100)
)
) )
) )
) }
} }
} }
} }

View File

@ -627,6 +627,7 @@
<string name="preference_google_signin_summary">Sign in to search Google Drive</string> <string name="preference_google_signin_summary">Sign in to search Google Drive</string>
<string name="preference_calendar_calendars">Calendars</string> <string name="preference_calendar_calendars">Calendars</string>
<string name="preference_calendar_hide_allday">Hide all-day events</string> <string name="preference_calendar_hide_allday">Hide all-day events</string>
<string name="preference_calendar_hide_completed">Hide completed tasks</string>
<string name="preference_screen_buildinfo">Build information</string> <string name="preference_screen_buildinfo">Build information</string>
<string name="preference_screen_buildinfo_summary">More information about this build of this app</string> <string name="preference_screen_buildinfo_summary">More information about this build of this app</string>
<string name="preference_search_bar_style">Style</string> <string name="preference_search_bar_style">Style</string>

View File

@ -6,6 +6,7 @@ import android.provider.CalendarContract
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.search.CalendarEvent import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.calendar.CalendarListType
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.Calendar import java.util.Calendar
@ -201,6 +202,7 @@ class AndroidCalendarProvider(
name = cursor.getStringOrNull(5) ?: cursor.getStringOrNull(1) ?: "", name = cursor.getStringOrNull(5) ?: cursor.getStringOrNull(1) ?: "",
owner = cursor.getStringOrNull(2), owner = cursor.getStringOrNull(2),
color = cursor.getInt(3), color = cursor.getInt(3),
types = listOf(CalendarListType.Calendar),
providerId = "local", providerId = "local",
) )
) )

View File

@ -15,6 +15,9 @@ data class CalendarWidgetConfig(
val legacyExcludedCalendarIds: List<Long>? = null, val legacyExcludedCalendarIds: List<Long>? = null,
@SerialName("excludedCalendars") @SerialName("excludedCalendars")
val excludedCalendarIds: List<String>? = null, val excludedCalendarIds: List<String>? = null,
val completedTasks: Boolean = true,
val upcomingEventsCount: Int = 3,
val upcomingTaskCount: Int = 3,
) )
data class CalendarWidget( data class CalendarWidget(
override val id: UUID, override val id: UUID,