Simplify calendar search settings when no plugins are installed

This commit is contained in:
MM20 2025-03-24 23:31:16 +01:00
parent dfb598b24b
commit b4056df5c2
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
9 changed files with 263 additions and 150 deletions

View File

@ -38,6 +38,7 @@ import de.mm20.launcher2.ui.settings.about.AboutSettingsScreen
import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarProviderSettingsScreen
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarSearchSettingsScreen
import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen
@ -198,6 +199,11 @@ class SettingsActivity : BaseActivity() {
composable("settings/search/calendar") {
CalendarSearchSettingsScreen()
}
composable("settings/search/calendar/{providerId}") {
CalendarProviderSettingsScreen(
it.arguments?.getString("providerId") ?: return@composable
)
}
composable("settings/search/searchactions") {
SearchActionsSettingsScreen()
}

View File

@ -0,0 +1,97 @@
package de.mm20.launcher2.ui.settings.calendarsearch
import android.app.PendingIntent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.calendar.providers.CalendarList
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.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.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalDarkTheme
@Composable
fun CalendarProviderSettingsScreen(providerId: String) {
val viewModel = viewModel<CalendarProviderSettingsScreenVM>()
LaunchedEffect(providerId) {
viewModel.init(providerId)
}
val enabled by viewModel.isProviderEnabled.collectAsStateWithLifecycle(false)
val calendarLists by viewModel.calendarLists.collectAsStateWithLifecycle(sortedMapOf<String, List<CalendarList>>())
val excludedCalendars by viewModel.excludedCalendars.collectAsStateWithLifecycle(emptySet())
val pluginState by viewModel.pluginState.collectAsStateWithLifecycle(null)
val providerAvailable = providerId == "local" || pluginState != null
PreferenceScreen(
title = pluginState?.plugin?.label ?: stringResource(R.string.preference_search_calendar)
) {
if (!providerAvailable) {
return@PreferenceScreen
}
item {
PreferenceCategory {
SwitchPreference(
title =
if (providerId == "local") stringResource(R.string.preference_search_calendar)
else pluginState?.plugin?.label ?: "",
summary =
if (providerId == "local") stringResource(R.string.preference_search_local_calendar_summary)
else (pluginState?.state as? PluginState.Ready)?.text
?: pluginState?.plugin?.description,
value = enabled && (pluginState == null || pluginState?.state is PluginState.Ready),
onValueChanged = { viewModel.setProviderEnabled(providerId, it) }
)
}
}
items(calendarLists.toList()) { (k, v) ->
PreferenceCategory(
title = k,
) {
for (list in v) {
CheckboxPreference(
title = list.name,
value = !excludedCalendars.contains(list.id),
onValueChanged = { viewModel.setCalendarExcluded(list.id, !it) },
checkboxColors = CheckboxDefaults.colors(
checkedColor = if (list.color == 0) MaterialTheme.colorScheme.primary
else Color(
list.color.atTone(if (LocalDarkTheme.current) 80 else 40)
),
checkmarkColor = if (list.color == 0) MaterialTheme.colorScheme.onPrimary
else Color(
list.color.atTone(if (LocalDarkTheme.current) 20 else 100)
)
),
enabled = enabled,
)
}
}
}
}
}

View File

@ -0,0 +1,42 @@
package de.mm20.launcher2.ui.settings.calendarsearch
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class CalendarProviderSettingsScreenVM: ViewModel(), KoinComponent {
private val providerId = MutableStateFlow<String>("")
fun init(providerId: String) {
this.providerId.value = providerId
}
private val calendarSearchSettings: CalendarSearchSettings by inject()
private val calendarRepository: CalendarRepository by inject()
private val pluginService: PluginService by inject()
val pluginState = providerId.flatMapLatest { pluginService.getPluginWithState(it) }
val isProviderEnabled = providerId.flatMapLatest { calendarSearchSettings.isProviderEnabled(it) }
fun setProviderEnabled(providerId: String, enabled: Boolean) {
calendarSearchSettings.setProviderEnabled(providerId, enabled)
}
val calendarLists = providerId
.flatMapLatest { calendarRepository.getCalendars(it) }
.map { it.groupBy { it.owner }.toSortedMap(compareBy { it }) }
val excludedCalendars = calendarSearchSettings.excludedCalendars
fun setCalendarExcluded(calendarId: String, excluded: Boolean) {
calendarSearchSettings.setCalendarExcluded(calendarId, excluded)
}
}

View File

@ -1,32 +1,21 @@
package de.mm20.launcher2.ui.settings.calendarsearch
import android.app.PendingIntent
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
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.material3.CheckboxDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
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.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
@ -35,163 +24,83 @@ 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.search.calendar.CalendarListType
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.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalNavController
@Composable
fun CalendarSearchSettingsScreen() {
val viewModel: CalendarSearchSettingsScreenVM = viewModel()
val context = LocalContext.current
val navController = LocalNavController.current
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsState(null)
val plugins by viewModel.availablePlugins.collectAsState(emptyList())
val plugins by viewModel.availablePlugins.collectAsStateWithLifecycle(emptyList(), minActiveState = Lifecycle.State.RESUMED)
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) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_calendar_search_settings),
onClick = {
viewModel.requestCalendarPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
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 = 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,
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),
icon = Icons.Rounded.ErrorOutline,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(id = R.string.plugin_action_setup))
}
}
PreferenceCategory {
AnimatedVisibility(hasCalendarPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_calendar_search_settings),
onClick = {
viewModel.requestCalendarPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
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.SetupRequired)?.message
?: if (selectedCalendars != null && calendarLists != null) {
pluralStringResource(
R.plurals.calendar_search_enabled_lists,
selectedCalendars,
selectedCalendars
)
}
else (state as? PluginState.Ready)?.text ?: plugin.plugin.description,
switchValue = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready,
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_local_calendar_summary),
switchValue = enabledProviders.contains("local") && hasCalendarPermission == true,
onSwitchChanged = {
viewModel.setProviderEnabled(plugin.plugin.authority, it)
viewModel.setProviderEnabled("local", it)
},
enabled = hasCalendarPermission == true,
onClick = {
showDialogForProvider = plugin.plugin.authority
navController?.navigate("settings/search/calendar/local")
}
)
}
}
}
Log.d("MM20", "${calendarLists.toString()}")
val dialogCalendarLists by remember {
derivedStateOf {
if (showDialogForProvider == null) null
else calendarLists?.filter { it.providerId == showDialogForProvider }
}
}
if (showDialogForProvider != null && dialogCalendarLists != null) {
ModalBottomSheet(
onDismissRequest = {
showDialogForProvider = null
},
) {
val groups = remember(dialogCalendarLists) {
dialogCalendarLists!!.groupBy { it.owner }.entries.sortedBy { it.key }
}
LazyColumn {
items(groups) {
PreferenceCategory(
title = it.key,
iconPadding = false,
) {
for (list in it.value) {
CheckboxPreference(
title = list.name,
iconPadding = false,
value = list.id !in excludedCalendars,
onValueChanged = { value ->
viewModel.setCalendarExcluded(list.id, !value)
},
checkboxColors = CheckboxDefaults.colors(
checkedColor = if (list.color == 0) MaterialTheme.colorScheme.primary
else Color(
list.color.atTone(if (LocalDarkTheme.current) 80 else 40)
),
checkmarkColor = if (list.color == 0) MaterialTheme.colorScheme.onPrimary
else Color(
list.color.atTone(if (LocalDarkTheme.current) 20 else 100)
)
)
)
}
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),
icon = Icons.Rounded.ErrorOutline,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(id = R.string.plugin_action_setup))
}
}
)
}
PreferenceWithSwitch(
title = plugin.plugin.label,
enabled = state is PluginState.Ready,
summary = (state as? PluginState.SetupRequired)?.message
?: (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 = {
navController?.navigate("settings/search/calendar/${plugin.plugin.authority}")
}
)
}
}
}

View File

@ -25,6 +25,7 @@ import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material.icons.rounded.Work
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -49,6 +50,7 @@ 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.icons.Wikipedia
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.ui.launcher.search.filters.SearchFilters
import de.mm20.launcher2.ui.locals.LocalNavController
@ -65,6 +67,11 @@ fun SearchSettingsScreen() {
mutableStateOf(false)
}
val plugins by viewModel.plugins.collectAsStateWithLifecycle(emptyList())
val hasCalendarPlugins by remember { derivedStateOf { plugins.any { it.plugin.type == PluginType.Calendar } } }
val hasLocationPlugins by remember { derivedStateOf { plugins.any { it.plugin.type == PluginType.LocationSearch } } }
PreferenceScreen(title = stringResource(R.string.preference_screen_search)) {
item {
@ -116,14 +123,43 @@ fun SearchSettingsScreen() {
enabled = hasContactsPermission == true
)
Preference(
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary),
icon = Icons.Rounded.Today,
onClick = {
navController?.navigate("settings/search/calendar")
},
)
if (hasCalendarPlugins) {
Preference(
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary),
icon = Icons.Rounded.Today,
onClick = {
navController?.navigate("settings/search/calendar")
},
)
} else {
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsStateWithLifecycle(
null
)
val calendar by viewModel.calendarSearch.collectAsStateWithLifecycle(null)
AnimatedVisibility(hasCalendarPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_calendar_search_settings),
onClick = {
viewModel.requestCalendarPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary),
switchValue = calendar == true,
onSwitchChanged = {
viewModel.setCalendarSearch(it)
},
icon = Icons.Rounded.Today,
enabled = hasCalendarPermission == true,
onClick = {
navController?.navigate("settings/search/calendar/local")
}
)
}
val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(
null

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.search.CalculatorSearchSettings
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import de.mm20.launcher2.preferences.search.ContactSearchSettings
@ -32,6 +33,7 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
private val calculatorSearchSettings: CalculatorSearchSettings by inject()
private val searchFilterSettings: SearchFilterSettings by inject()
private val pluginService: PluginService by inject()
private val permissionsManager: PermissionsManager by inject()
private val locationSearchSettings: LocationSearchSettings by inject()
@ -42,6 +44,14 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
searchUiSettings.setFavorites(favorites)
}
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val calendarSearch = calendarSearchSettings.isProviderEnabled("local")
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setCalendarSearch(enabled: Boolean) {
calendarSearchSettings.setProviderEnabled("local", enabled)
}
val hasContactsPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
@ -52,6 +62,10 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
contactSearchSettings.setEnabled(contacts)
}
fun requestCalendarPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
}
fun requestContactsPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Contacts)
}
@ -130,4 +144,7 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
fun setSearchFilters(searchFilters: SearchFilters) {
searchFilterSettings.setDefaultFilter(searchFilters)
}
val plugins = pluginService.getPluginsWithState(enabled = true)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
}

View File

@ -608,6 +608,7 @@
<string name="preference_search_contacts_summary">Search contacts on this device</string>
<string name="preference_search_calendar">Calendar</string>
<string name="preference_search_calendar_summary">Search upcoming appointments and events</string>
<string name="preference_search_local_calendar_summary">Search calendars on this device</string>
<string name="preference_search_appshortcuts">App shortcuts</string>
<string name="preference_search_appshortcuts_summary">Search app shortcuts</string>
<string name="preference_search_calculator">Calculator</string>

View File

@ -12,6 +12,8 @@ class CalendarSearchSettings internal constructor(
val enabledProviders
get() = dataStore.data.map { it.calendarSearchProviders }
fun isProviderEnabled(provider: String) = dataStore.data.map { it.calendarSearchProviders.contains(provider) }
fun setProviderEnabled(provider: String, enabled: Boolean) {
dataStore.update {
if (enabled) {

View File

@ -47,6 +47,9 @@ interface PluginService {
enabled: Boolean? = null,
): Flow<List<PluginWithState>>
/**
* Get a plugin with its current state or null if the plugin is not found.
*/
fun getPluginWithState(
authority: String,
): Flow<PluginWithState?>