Add calendar plugin preferences

This commit is contained in:
MM20 2024-08-06 21:10:54 +02:00
parent 1a97e01c6a
commit 1dad8b48d5
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
10 changed files with 318 additions and 25 deletions

View File

@ -1,6 +1,8 @@
package de.mm20.launcher2.ui.component.preferences
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxColors
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
@ -12,7 +14,8 @@ fun CheckboxPreference(
summary: String? = null,
value: Boolean,
onValueChanged: (Boolean) -> Unit,
enabled: Boolean = true
enabled: Boolean = true,
checkboxColors: CheckboxColors = CheckboxDefaults.colors(),
) {
Preference(
title = title,
@ -25,7 +28,7 @@ fun CheckboxPreference(
},
controls = {
Checkbox(
enabled = enabled, checked = value, onCheckedChange = onValueChanged,
enabled = enabled, checked = value, onCheckedChange = onValueChanged, colors = checkboxColors
)
}
)

View File

@ -19,7 +19,8 @@ fun PreferenceWithSwitch(
enabled: Boolean = true,
onClick: () -> Unit = {},
switchValue: Boolean,
onSwitchChanged: (Boolean) -> Unit
onSwitchChanged: (Boolean) -> Unit,
iconPadding: Boolean = true
) {
Row(
verticalAlignment = (Alignment.CenterVertically)
@ -32,7 +33,8 @@ fun PreferenceWithSwitch(
summary = summary,
icon = icon,
enabled = enabled,
onClick = onClick
onClick = onClick,
iconPadding = iconPadding
)
}
Box(

View File

@ -4,26 +4,46 @@ import android.app.PendingIntent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
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.items
import androidx.compose.material.icons.Icons
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.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.themes.atTone
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalDarkTheme
@Composable
fun CalendarSearchSettingsScreen() {
@ -34,6 +54,14 @@ fun CalendarSearchSettingsScreen() {
val plugins by viewModel.availablePlugins.collectAsState(emptyList())
val enabledProviders by viewModel.enabledProviders.collectAsState(emptySet())
val calendarLists by viewModel.calendarLists.collectAsStateWithLifecycle(
null,
minActiveState = Lifecycle.State.RESUMED
)
val excludedCalendars by viewModel.excludedCalendars.collectAsState(emptyList())
var showDialogForProvider by remember { mutableStateOf<String?>(null) }
PreferenceScreen(title = stringResource(R.string.preference_search_calendar)) {
item {
AnimatedVisibility(hasCalendarPermission == false) {
@ -45,21 +73,32 @@ fun CalendarSearchSettingsScreen() {
modifier = Modifier.padding(16.dp)
)
}
SwitchPreference(
val selectedCalendars = remember(excludedCalendars, calendarLists) {
calendarLists?.count { it.providerId == "local" }
?.minus(excludedCalendars.count {
it.startsWith("local")
})
}
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary),
value = enabledProviders.contains("local") && hasCalendarPermission == true,
onValueChanged = {
summary = if (selectedCalendars != null && calendarLists != null) "$selectedCalendars lists selected"
else stringResource(R.string.preference_search_calendar_summary),
switchValue = enabledProviders.contains("local") && hasCalendarPermission == true,
onSwitchChanged = {
viewModel.setProviderEnabled("local", it)
},
enabled = hasCalendarPermission == true
enabled = hasCalendarPermission == true,
onClick = {
showDialogForProvider = "local"
}
)
for (plugin in plugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message ?: stringResource(id = R.string.plugin_state_setup_required),
text = state.message
?: stringResource(id = R.string.plugin_state_setup_required),
icon = Icons.Rounded.ErrorOutline,
primaryAction = {
TextButton(onClick = {
@ -74,18 +113,76 @@ fun CalendarSearchSettingsScreen() {
}
)
}
SwitchPreference(
val selectedCalendars = remember(excludedCalendars, calendarLists) {
calendarLists?.count { it.providerId == plugin.plugin.authority }
?.minus(excludedCalendars.count {
it.startsWith(
plugin.plugin.authority
)
})
}
PreferenceWithSwitch(
title = plugin.plugin.label,
enabled = state is PluginState.Ready,
summary = (state as? PluginState.Ready)?.text
?: (state as? PluginState.SetupRequired)?.message
?: plugin.plugin.description,
value = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready,
onValueChanged = {
summary = (state as? PluginState.SetupRequired)?.message
?: if (selectedCalendars != null && calendarLists != null) "$selectedCalendars lists selected"
else (state as? PluginState.Ready)?.text ?: plugin.plugin.description,
switchValue = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready,
onSwitchChanged = {
viewModel.setProviderEnabled(plugin.plugin.authority, it)
},
onClick = {
showDialogForProvider = plugin.plugin.authority
}
)
}
}
}
val dialogCalendarLists by remember {
derivedStateOf {
if (showDialogForProvider == null) null
else calendarLists?.filter { it.providerId == showDialogForProvider }
}
}
if (showDialogForProvider != null && dialogCalendarLists != null) {
BasicAlertDialog(
onDismissRequest = {
showDialogForProvider = null
},
) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
LazyColumn {
items(dialogCalendarLists ?: emptyList()) {
CheckboxPreference(
title = it.name,
summary = it.owner,
iconPadding = false,
value = it.id !in excludedCalendars,
onValueChanged = { value ->
viewModel.setCalendarExcluded(it.id, !value)
},
checkboxColors = CheckboxDefaults.colors(
checkedColor = if (it.color == 0) MaterialTheme.colorScheme.primary
else Color(
it.color.atTone(if (LocalDarkTheme.current) 80 else 40)
),
checkmarkColor = if (it.color == 0) MaterialTheme.colorScheme.onPrimary
else Color(
it.color.atTone(if (LocalDarkTheme.current) 20 else 100)
)
)
)
}
}
}
}
}
}

View File

@ -2,16 +2,21 @@ package de.mm20.launcher2.ui.settings.calendarsearch
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
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.plugin.Plugin
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class CalendarSearchSettingsScreenVM : ViewModel(), KoinComponent {
private val settings: CalendarSearchSettings by inject()
private val calendarRepository: CalendarRepository by inject()
private val pluginService: PluginService by inject()
private val permissionsManager: PermissionsManager by inject()
@ -31,4 +36,11 @@ class CalendarSearchSettingsScreenVM : ViewModel(), KoinComponent {
fun requestCalendarPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
}
val calendarLists = calendarRepository.getCalendars()
val excludedCalendars = settings.excludedCalendars
fun setCalendarExcluded(calendarId: String, excluded: Boolean) {
settings.setCalendarExcluded(calendarId, excluded)
}
}

View File

@ -16,6 +16,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
@ -29,6 +33,9 @@ import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Today
import androidx.compose.material.icons.rounded.Verified
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -39,9 +46,14 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.stringResource
import androidx.compose.ui.unit.dp
@ -53,11 +65,15 @@ import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.themes.atTone
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
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.LocalNavController
@Composable
@ -88,6 +104,11 @@ fun PluginSettingsScreen(pluginId: String) {
minActiveState = Lifecycle.State.RESUMED
)
val calendarPlugins by viewModel.calendarPlugins.collectAsStateWithLifecycle(
emptyList(),
minActiveState = Lifecycle.State.RESUMED
)
val weatherPlugins by viewModel.weatherPlugins.collectAsStateWithLifecycle(
emptyList(),
minActiveState = Lifecycle.State.RESUMED
@ -108,6 +129,10 @@ fun PluginSettingsScreen(pluginId: String) {
null
)
val enabledCalendarSearchPlugins by viewModel.enabledCalendarSearchPlugins.collectAsStateWithLifecycle(
null
)
val weatherProviderId by viewModel.weatherProvider.collectAsStateWithLifecycle(
null
)
@ -316,7 +341,9 @@ fun PluginSettingsScreen(pluginId: String) {
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(context)
state.setupActivity.sendWithBackgroundPermission(
context
)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
@ -367,7 +394,9 @@ fun PluginSettingsScreen(pluginId: String) {
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(context)
state.setupActivity.sendWithBackgroundPermission(
context
)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
@ -402,6 +431,111 @@ fun PluginSettingsScreen(pluginId: String) {
}
}
}
if (calendarPlugins.isNotEmpty()) {
PreferenceCategory(
stringResource(R.string.plugin_type_calendar),
iconPadding = false,
) {
val excludedCalendars by viewModel.excludedCalendars.collectAsState(
emptySet()
)
for (plugin in calendarPlugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message
?: stringResource(R.string.plugin_state_setup_required),
icon = Icons.Rounded.Info,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(
context
)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(R.string.plugin_action_setup))
}
}
)
} else if (state is PluginState.Error) {
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.plugin_state_error),
icon = Icons.Rounded.Error,
color = MaterialTheme.colorScheme.errorContainer,
)
}
val calendarLists by remember(plugin.state, plugin.plugin) {
viewModel.getCalendarLists(plugin.plugin)
}.collectAsStateWithLifecycle(null, minActiveState = Lifecycle.State.RESUMED)
var showDialog by remember { mutableStateOf(false) }
val selectedCalendars = remember(excludedCalendars, calendarLists) {
calendarLists?.size?.minus(excludedCalendars.count { it.startsWith(plugin.plugin.authority) })
}
PreferenceWithSwitch(
title = plugin.plugin.label,
enabled = enabledCalendarSearchPlugins != null && state is PluginState.Ready,
summary = (state as? PluginState.SetupRequired)?.message
?: if (selectedCalendars != null && calendarLists != null) "$selectedCalendars lists selected"
else (state as? PluginState.Ready)?.text ?: plugin.plugin.description,
switchValue = enabledCalendarSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready,
onSwitchChanged = {
viewModel.setCalendarSearchPluginEnabled(
plugin.plugin.authority,
it
)
},
iconPadding = false,
onClick = {
showDialog = true
}
)
if (showDialog && calendarLists?.isNotEmpty() == true) {
BasicAlertDialog(
onDismissRequest = {
showDialog = false
},
) {
Surface(
modifier = Modifier.wrapContentWidth()
.wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
LazyColumn {
items(calendarLists!!) {
CheckboxPreference(
title = it.name,
summary = it.owner,
iconPadding = false,
value = it.id !in excludedCalendars,
onValueChanged = { value ->
viewModel.setCalendarExcluded(it.id, !value)
},
checkboxColors = CheckboxDefaults.colors(
checkedColor = if (it.color == 0) MaterialTheme.colorScheme.primary
else Color(
it.color.atTone(if (LocalDarkTheme.current) 80 else 40)
),
checkmarkColor = if (it.color == 0) MaterialTheme.colorScheme.onPrimary
else Color(
it.color.atTone(if (LocalDarkTheme.current) 20 else 100)
)
)
)
}
}
}
}
}
}
}
}
if (weatherPlugins.isNotEmpty()) {
PreferenceCategory(
stringResource(R.string.plugin_type_weather),
@ -418,7 +552,9 @@ fun PluginSettingsScreen(pluginId: String) {
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(context)
state.setupActivity.sendWithBackgroundPermission(
context
)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}

View File

@ -7,12 +7,16 @@ import android.net.Uri
import android.provider.Settings
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.ktx.tryStartActivity
import de.mm20.launcher2.plugin.Plugin
import de.mm20.launcher2.plugin.PluginPackage
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.plugins.PluginWithState
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import de.mm20.launcher2.preferences.search.FileSearchSettings
import de.mm20.launcher2.preferences.search.LocationSearchSettings
import de.mm20.launcher2.preferences.weather.WeatherSettings
@ -31,8 +35,10 @@ import org.koin.core.component.inject
class PluginSettingsScreenVM : ViewModel(), KoinComponent {
private val pluginService by inject<PluginService>()
private val calendarRepository by inject<CalendarRepository>()
private val fileSearchSettings: FileSearchSettings by inject()
private val locationSearchSettings: LocationSearchSettings by inject()
private val calendarSearchSettings: CalendarSearchSettings by inject()
private val weatherSettings: WeatherSettings by inject()
private var pluginPackageName = MutableStateFlow<String?>(null)
@ -79,6 +85,11 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
it.filter { it.plugin.type == PluginType.LocationSearch }
}
val calendarPlugins = states
.map {
it.filter { it.plugin.type == PluginType.Calendar }
}
val weatherPlugins = states
.map {
it.filter { it.plugin.type == PluginType.Weather }
@ -121,8 +132,22 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
locationSearchSettings.setPluginEnabled(authority, enabled)
}
val enabledCalendarSearchPlugins = calendarSearchSettings.providers
fun setCalendarSearchPluginEnabled(authority: String, enabled: Boolean) {
calendarSearchSettings.setProviderEnabled(authority, enabled)
}
val weatherProvider = weatherSettings.providerId
fun setWeatherProvider(providerId: String) {
weatherSettings.setProvider(providerId)
}
fun getCalendarLists(plugin: Plugin): Flow<List<CalendarList>> {
return calendarRepository.getCalendars(plugin.authority)
}
val excludedCalendars = calendarSearchSettings.excludedCalendars
fun setCalendarExcluded(calendarId: String, excluded: Boolean) {
calendarSearchSettings.setCalendarExcluded(calendarId, excluded)
}
}

View File

@ -56,6 +56,7 @@ data class LauncherSettingsData internal constructor(
@Deprecated("Use calendarSearchProviders `local` instead")
val calendarSearchEnabled: Boolean = true,
val calendarSearchProviders: Set<String> = setOf("local"),
val calendarSearchExcludedCalendars: Set<String> = setOf(),
val shortcutSearchEnabled: Boolean = true,

View File

@ -21,4 +21,16 @@ class CalendarSearchSettings internal constructor(
}
}
}
val excludedCalendars
get() = dataStore.data.map { it.calendarSearchExcludedCalendars }
fun setCalendarExcluded(calendarId: String, excluded: Boolean) {
dataStore.update {
if (excluded) {
it.copy(calendarSearchExcludedCalendars = it.calendarSearchExcludedCalendars + calendarId)
} else {
it.copy(calendarSearchExcludedCalendars = it.calendarSearchExcludedCalendars - calendarId)
}
}
}
}

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.calendar
import android.content.Context
import android.util.Log
import de.mm20.launcher2.calendar.providers.AndroidCalendarProvider
import de.mm20.launcher2.calendar.providers.CalendarList
import de.mm20.launcher2.calendar.providers.CalendarProvider
@ -58,8 +59,9 @@ internal class CalendarRepositoryImpl(
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
val providerIds = settings.providers
val excludedCalendars = settings.excludedCalendars
return combineTransform(hasPermission, providerIds) { perm, providerIds ->
return combineTransform(hasPermission, providerIds, excludedCalendars) { perm, providerIds, excludedCalendars ->
val providers = providerIds.mapNotNull {
when (it) {
"local" -> if (perm) AndroidCalendarProvider(context) else null
@ -72,6 +74,7 @@ internal class CalendarRepositoryImpl(
queryCalendarEvents(
query = query,
excludeAllDayEvents = false,
excludeCalendars = excludedCalendars.toList(),
providers = providers,
intervalStart = now,
intervalEnd = now + 730.days.inWholeMilliseconds,

View File

@ -47,10 +47,12 @@ class PluginCalendarProvider(
}
override fun Uri.Builder.appendQueryParameters(query: CalendarQuery): Uri.Builder {
if (query.query != null) {
appendQueryParameter(
CalendarPluginContract.Params.Query,
query.query
)
}
val start = query.start
if (start != null) {
appendQueryParameter(