Add search filters

This commit is contained in:
MM20
2024-04-21 01:30:33 +02:00
parent 3c489fc648
commit 58ebd5646b
14 changed files with 574 additions and 98 deletions
@@ -199,7 +199,7 @@ fun AssistantScaffold(
}, },
actions = actions, actions = actions,
highlightedAction = searchVM.bestMatch.value as? SearchAction, highlightedAction = searchVM.bestMatch.value as? SearchAction,
showHiddenItemsButton = true, isSearchOpen = true,
value = { value }, value = { value },
onValueChange = { searchVM.search(it) }, onValueChange = { searchVM.search(it) },
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
@@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.search.SearchService import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.toList import de.mm20.launcher2.search.toList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -40,7 +41,7 @@ class SearchablePickerVM : ViewModel(), KoinComponent {
searchJob = viewModelScope.launch { searchJob = viewModelScope.launch {
searchService.search( searchService.search(
query = query, query = query,
allowNetwork = true, filters = SearchFilters(allowNetwork = true)
).collectLatest { ).collectLatest {
if (searchQuery != query) return@collectLatest if (searchQuery != query) return@collectLatest
items = withContext(Dispatchers.Default) { items = withContext(Dispatchers.Default) {
@@ -2,9 +2,7 @@ package de.mm20.launcher2.ui.launcher
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.slideIn import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut import androidx.compose.animation.slideOut
import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.LocalOverscrollConfiguration
@@ -627,7 +625,7 @@ fun PagerScaffold(
}, },
actions = actions, actions = actions,
highlightedAction = searchVM.bestMatch.value as? SearchAction, highlightedAction = searchVM.bestMatch.value as? SearchAction,
showHiddenItemsButton = isSearchOpen, isSearchOpen = isSearchOpen,
value = { value }, value = { value },
onValueChange = { searchVM.search(it) }, onValueChange = { searchVM.search(it) },
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
@@ -603,7 +603,7 @@ fun PullDownScaffold(
}, },
actions = actions, actions = actions,
highlightedAction = searchVM.bestMatch.value as? SearchAction, highlightedAction = searchVM.bestMatch.value as? SearchAction,
showHiddenItemsButton = isSearchOpen, isSearchOpen = isSearchOpen,
value = { value }, value = { value },
onValueChange = { searchVM.search(it) }, onValueChange = { searchVM.search(it) },
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
@@ -27,6 +27,7 @@ import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchService import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.Location import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.search.Website import de.mm20.launcher2.search.Website
import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.Calculator
import de.mm20.launcher2.search.data.UnitConverter import de.mm20.launcher2.search.data.UnitConverter
@@ -90,6 +91,8 @@ class SearchVM : ViewModel(), KoinComponent {
val favoritesEnabled = searchUiSettings.favorites val favoritesEnabled = searchUiSettings.favorites
val hideFavorites = mutableStateOf(false) val hideFavorites = mutableStateOf(false)
val filters = mutableStateOf(SearchFilters())
val separateWorkProfile = searchUiSettings.separateWorkProfile val separateWorkProfile = searchUiSettings.separateWorkProfile
private val hiddenItemKeys = searchableRepository private val hiddenItemKeys = searchableRepository
@@ -117,6 +120,11 @@ class SearchVM : ViewModel(), KoinComponent {
} }
} }
fun setFilters(filters: SearchFilters) {
this.filters.value = filters
search(searchQuery.value, forceRestart = true)
}
private var searchJob: Job? = null private var searchJob: Job? = null
fun search(query: String, forceRestart: Boolean = false) { fun search(query: String, forceRestart: Boolean = false) {
if (searchQuery.value == query && !forceRestart) return if (searchQuery.value == query && !forceRestart) return
@@ -124,9 +132,10 @@ class SearchVM : ViewModel(), KoinComponent {
isSearchEmpty.value = query.isEmpty() isSearchEmpty.value = query.isEmpty()
hiddenResults.value = emptyList() hiddenResults.value = emptyList()
val filters = filters.value
if (isSearchEmpty.value) if (isSearchEmpty.value)
bestMatch.value = null bestMatch.value = null
try { try {
searchJob?.cancel() searchJob?.cancel()
} catch (_: CancellationException) { } catch (_: CancellationException) {
@@ -136,7 +145,7 @@ class SearchVM : ViewModel(), KoinComponent {
searchUiSettings.resultOrder.collectLatest { resultOrder -> searchUiSettings.resultOrder.collectLatest { resultOrder ->
searchService.search( searchService.search(
query, query,
allowNetwork = true, filters = filters,
).collectLatest { results -> ).collectLatest { results ->
var resultsList = withContext(Dispatchers.Default) { var resultsList = withContext(Dispatchers.Default) {
listOfNotNull( listOfNotNull(
@@ -219,10 +228,9 @@ class SearchVM : ViewModel(), KoinComponent {
val actions = mutableListOf<SearchAction>() val actions = mutableListOf<SearchAction>()
for (r in resultsList) { for (r in resultsList) {
when { when {
r is SavableSearchable && hiddenKeys.contains(r.key) -> { r is SavableSearchable && hiddenKeys.contains(r.key) && !filters.hiddenItems -> {
hidden.add(r) hidden.add(r)
} }
r is Application && r.profile == AppProfile.Work -> workApps.add(r) r is Application && r.profile == AppProfile.Work -> workApps.add(r)
r is Application -> apps.add(r) r is Application -> apps.add(r)
r is AppShortcut -> shortcuts.add(r) r is AppShortcut -> shortcuts.add(r)
@@ -0,0 +1,239 @@
package de.mm20.launcher2.ui.launcher.searchbar
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AppShortcut
import androidx.compose.material.icons.rounded.Apps
import androidx.compose.material.icons.rounded.Description
import androidx.compose.material.icons.rounded.Handyman
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Public
import androidx.compose.material.icons.rounded.Today
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.icons.Wikipedia
import de.mm20.launcher2.ui.overlays.Overlay
@Composable
fun KeyboardFilterBar(filters: SearchFilters, onFiltersChange: (SearchFilters) -> Unit) {
Overlay {
val allCategoriesEnabled = filters.allCategoriesEnabled
Box(modifier = Modifier
.fillMaxSize()
.imePadding(), contentAlignment = Alignment.BottomCenter) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainerLow)
) {
HorizontalDivider()
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
FilterChip(
selected = filters.allowNetwork,
onClick = {
onFiltersChange(filters.copy(allowNetwork = !filters.allowNetwork))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Language,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.search_filter_online)) }
)
VerticalDivider(
modifier = Modifier
.height(36.dp)
.padding(horizontal = 8.dp)
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.apps && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleApps())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Apps,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text("Apps") }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.files && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleFiles())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Description,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_files)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.contacts && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleContacts())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Person,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_contacts)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.events && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleEvents())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Today,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_calendar)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.shortcuts && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleShortcuts())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.AppShortcut,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_appshortcuts)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.articles && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleArticles())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Wikipedia,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_wikipedia)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.websites && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleWebsites())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Public,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_websites)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.places && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.togglePlaces())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Place,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_locations)) }
)
FilterChip(
selected = filters.tools && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleTools())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Handyman,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.search_filter_tools)) }
)
VerticalDivider(
modifier = Modifier
.height(36.dp)
.padding(horizontal = 8.dp)
)
FilterChip(
selected = filters.hiddenItems,
onClick = {
onFiltersChange(filters.copy(hiddenItems = !filters.hiddenItems))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.VisibilityOff,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_hidden_items)) }
)
}
HorizontalDivider()
}
}
}
}
@@ -4,20 +4,29 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.isImeVisible
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.text.KeyboardActionScope import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material.icons.rounded.FilterAlt
import androidx.compose.material3.Badge
import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect 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.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.SearchBarStyle import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
@@ -37,7 +46,7 @@ fun LauncherSearchBar(
onFocusChange: (Boolean) -> Unit, onFocusChange: (Boolean) -> Unit,
actions: List<SearchAction>, actions: List<SearchAction>,
highlightedAction: SearchAction?, highlightedAction: SearchAction?,
showHiddenItemsButton: Boolean = false, isSearchOpen: Boolean = false,
reverse: Boolean = false, reverse: Boolean = false,
darkColors: Boolean = false, darkColors: Boolean = false,
onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null
@@ -57,6 +66,15 @@ fun LauncherSearchBar(
else focusManager.clearFocus() else focusManager.clearFocus()
} }
if (isSearchOpen && WindowInsets.isImeVisible) {
KeyboardFilterBar(
filters = searchVM.filters.value,
onFiltersChange = {
searchVM.setFilters(it)
}
)
}
val _value = value() val _value = value()
SearchBar( SearchBar(
@@ -66,21 +84,37 @@ fun LauncherSearchBar(
darkColors = darkColors, darkColors = darkColors,
menu = { menu = {
AnimatedVisibility( AnimatedVisibility(
hiddenItemsButtonEnabled && showHiddenItemsButton && hiddenItems.isNotEmpty(), isSearchOpen,
enter = scaleIn(tween(100)), enter = scaleIn(tween(100)),
exit = scaleOut(tween(100)) exit = scaleOut(tween(100))
) { ) {
FilledIconButton( FilledIconButton(
onClick = { sheetManager.showHiddenItemsSheet() }, onClick = { },
colors = if (sheetManager.hiddenItemsSheetShown.value) IconButtonDefaults.filledTonalIconButtonColors() else IconButtonDefaults.iconButtonColors() colors = IconButtonDefaults.iconButtonColors()
) { ) {
Icon(imageVector = Icons.Rounded.VisibilityOff, contentDescription = null) Box {
Icon(imageVector = Icons.Rounded.FilterAlt, contentDescription = null)
androidx.compose.animation.AnimatedVisibility(
!searchVM.filters.value.allCategoriesEnabled,
enter = scaleIn(tween(100)),
exit = scaleOut(tween(100)),
modifier = Modifier.align(Alignment.BottomEnd).offset(-3.dp, -3.dp)
) {
Badge(
containerColor = MaterialTheme.colorScheme.tertiary,
)
}
}
} }
} }
SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange) SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange)
}, },
actions = { actions = {
SearchBarActions(actions = actions, reverse = reverse, highlightedAction = highlightedAction) SearchBarActions(
actions = actions,
reverse = reverse,
highlightedAction = highlightedAction
)
}, },
focusRequester = focusRequester, focusRequester = focusRequester,
onFocus = { onFocusChange(true) }, onFocus = { onFocusChange(true) },
@@ -0,0 +1,2 @@
package de.mm20.launcher2.ui.launcher.searchbar
@@ -0,0 +1,146 @@
package de.mm20.launcher2.ui.launcher.searchbar
import de.mm20.launcher2.search.SearchFilters
fun SearchFilters.withAllCategories(): SearchFilters {
return copy(
apps = true,
websites = true,
articles = true,
places = true,
files = true,
shortcuts = true,
contacts = true,
events = true,
tools = true
)
}
fun SearchFilters.withOnlyCategory(
apps: Boolean = false,
websites: Boolean = false,
articles: Boolean = false,
places: Boolean = false,
files: Boolean = false,
shortcuts: Boolean = false,
contacts: Boolean = false,
events: Boolean = false,
utilities: Boolean = false
): SearchFilters {
return copy(
apps = apps,
websites = websites,
articles = articles,
places = places,
files = files,
shortcuts = shortcuts,
contacts = contacts,
events = events,
tools = utilities
)
}
/**
* Create a new [SearchFilters] object with the [apps] property update, according to the following rules:
* - If all categories are enabled, disable all categories except for apps.
* - If apps is the only enabled category, enable all categories.
* - Otherwise, toggle the apps category.
*/
fun SearchFilters.toggleApps(): SearchFilters {
if (allCategoriesEnabled) {
return withOnlyCategory(apps = true)
}
if (apps && enabledCategories == 1) {
return withAllCategories()
}
return copy(apps = !apps)
}
fun SearchFilters.toggleWebsites(): SearchFilters {
if (allCategoriesEnabled) {
return withOnlyCategory(websites = true)
}
if (websites && enabledCategories == 1) {
return withAllCategories()
}
return copy(websites = !websites)
}
fun SearchFilters.toggleArticles(): SearchFilters {
if (allCategoriesEnabled) {
return withOnlyCategory(articles = true)
}
if (articles && enabledCategories == 1) {
return withAllCategories()
}
return copy(articles = !articles)
}
fun SearchFilters.togglePlaces(): SearchFilters {
if (allCategoriesEnabled) {
return withOnlyCategory(places = true)
}
if (places && enabledCategories == 1) {
return withAllCategories()
}
return copy(places = !places)
}
fun SearchFilters.toggleFiles(): SearchFilters {
if (allCategoriesEnabled) {
return withOnlyCategory(files = true)
}
if (files && enabledCategories == 1) {
return withAllCategories()
}
return copy(files = !files)
}
fun SearchFilters.toggleShortcuts(): SearchFilters {
if (allCategoriesEnabled) {
return withOnlyCategory(shortcuts = true)
}
if (shortcuts && enabledCategories == 1) {
return withAllCategories()
}
return copy(shortcuts = !shortcuts)
}
fun SearchFilters.toggleContacts(): SearchFilters {
if (allCategoriesEnabled) {
return withOnlyCategory(contacts = true)
}
if (contacts && enabledCategories == 1) {
return withAllCategories()
}
return copy(contacts = !contacts)
}
fun SearchFilters.toggleEvents(): SearchFilters {
if (allCategoriesEnabled) {
return withOnlyCategory(events = true)
}
if (events && enabledCategories == 1) {
return withAllCategories()
}
return copy(events = !events)
}
fun SearchFilters.toggleTools(): SearchFilters {
if (allCategoriesEnabled) {
return withOnlyCategory(utilities = true)
}
if (tools && enabledCategories == 1) {
return withAllCategories()
}
return copy(tools = !tools)
}
@@ -195,7 +195,7 @@ fun SearchSettingsScreen() {
SwitchPreference( SwitchPreference(
title = stringResource(R.string.preference_search_websites), title = stringResource(R.string.preference_search_websites),
summary = stringResource(R.string.preference_search_websites_summary), summary = stringResource(R.string.preference_search_websites_summary),
icon = Icons.Rounded.Language, icon = Icons.Rounded.Public,
value = websites == true, value = websites == true,
onValueChanged = { onValueChanged = {
viewModel.setWebsites(it) viewModel.setWebsites(it)
@@ -915,4 +915,6 @@
<string name="clock_style_custom">Custom widget</string> <string name="clock_style_custom">Custom widget</string>
<string name="clock_variant_standard">Standard</string> <string name="clock_variant_standard">Standard</string>
<string name="clock_variant_outlined">Outlined</string> <string name="clock_variant_outlined">Outlined</string>
<string name="search_filter_tools">Tools</string>
<string name="search_filter_online">Online results</string>
</resources> </resources>
@@ -0,0 +1,5 @@
package de.mm20.launcher2.ktx
inline fun Boolean.toInt(): Int {
return if (this) 1 else 0
}
@@ -0,0 +1,23 @@
package de.mm20.launcher2.search
import de.mm20.launcher2.ktx.toInt
data class SearchFilters(
val allowNetwork: Boolean = false,
val hiddenItems: Boolean = false,
val apps: Boolean = true,
val websites: Boolean = true,
val articles: Boolean = true,
val places: Boolean = true,
val files: Boolean = true,
val shortcuts: Boolean = true,
val contacts: Boolean = true,
val events: Boolean = true,
val tools: Boolean = true,
) {
val allCategoriesEnabled
get() = apps && websites && articles && places && files && shortcuts && contacts && events && tools
val enabledCategories: Int
get() = apps.toInt() + websites.toInt() + articles.toInt() + places.toInt() + files.toInt() + shortcuts.toInt() + contacts.toInt() + events.toInt() + tools.toInt()
}
@@ -23,7 +23,7 @@ import kotlinx.coroutines.supervisorScope
interface SearchService { interface SearchService {
fun search( fun search(
query: String, query: String,
allowNetwork: Boolean = false, filters: SearchFilters,
): Flow<SearchResults> ): Flow<SearchResults>
} }
@@ -44,7 +44,7 @@ internal class SearchServiceImpl(
override fun search( override fun search(
query: String, query: String,
allowNetwork: Boolean, filters: SearchFilters,
): Flow<SearchResults> = channelFlow { ): Flow<SearchResults> = channelFlow {
val results = MutableStateFlow(SearchResults()) val results = MutableStateFlow(SearchResults())
supervisorScope { supervisorScope {
@@ -56,98 +56,116 @@ internal class SearchServiceImpl(
} }
} }
} }
launch { if (filters.apps) {
appRepository.search(query, allowNetwork) launch {
.withCustomLabels(customAttributesRepository) appRepository.search(query, filters.allowNetwork)
.collectLatest { r -> .withCustomLabels(customAttributesRepository)
results.update { .collectLatest { r ->
it.copy(apps = r.toImmutableList()) results.update {
it.copy(apps = r.toImmutableList())
}
} }
}
}
launch {
appShortcutRepository.search(query, allowNetwork)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(shortcuts = r.toImmutableList())
}
}
}
launch {
contactRepository.search(query, allowNetwork)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(contacts = r.toImmutableList())
}
}
}
launch {
calendarRepository.search(query, allowNetwork)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(calendars = r.toImmutableList())
}
}
}
launch {
calculatorRepository.search(query).collectLatest { r ->
results.update {
it.copy(calculators = r?.let { persistentListOf(it) }
?: persistentListOf())
}
} }
} }
launch { if (filters.shortcuts) {
unitConverterRepository.search(query) launch {
.collectLatest { r -> appShortcutRepository.search(query, filters.allowNetwork)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(shortcuts = r.toImmutableList())
}
}
}
}
if (filters.contacts) {
launch {
contactRepository.search(query, filters.allowNetwork)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(contacts = r.toImmutableList())
}
}
}
}
if (filters.events) {
launch {
calendarRepository.search(query, filters.allowNetwork)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(calendars = r.toImmutableList())
}
}
}
}
if (filters.tools) {
launch {
calculatorRepository.search(query).collectLatest { r ->
results.update { results.update {
it.copy(unitConverters = r?.let { persistentListOf(it) } it.copy(calculators = r?.let { persistentListOf(it) }
?: persistentListOf()) ?: persistentListOf())
} }
} }
} }
launch { launch {
websiteRepository.search(query, allowNetwork) unitConverterRepository.search(query)
.withCustomLabels(customAttributesRepository) .collectLatest { r ->
.collectLatest { r -> results.update {
results.update { it.copy(unitConverters = r?.let { persistentListOf(it) }
it.copy(websites = r.toImmutableList()) ?: persistentListOf())
}
} }
} }
} }
launch { if (filters.websites) {
delay(750) launch {
articleRepository.search(query, allowNetwork) websiteRepository.search(query, filters.allowNetwork)
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(wikipedia = r.toImmutableList()) it.copy(websites = r.toImmutableList())
}
} }
} }
} }
launch { if (filters.articles) {
locationRepository.search(query, allowNetwork) launch {
.withCustomLabels(customAttributesRepository) delay(750)
.collectLatest { r -> articleRepository.search(query, filters.allowNetwork)
results.update { .withCustomLabels(customAttributesRepository)
it.copy(locations = r.toImmutableList()) .collectLatest { r ->
results.update {
it.copy(wikipedia = r.toImmutableList())
}
} }
} }
} }
launch { if (filters.places) {
fileRepository.search( launch {
query, locationRepository.search(query, filters.allowNetwork)
allowNetwork .withCustomLabels(customAttributesRepository)
) .collectLatest { r ->
.withCustomLabels(customAttributesRepository) results.update {
.collectLatest { r -> it.copy(locations = r.toImmutableList())
results.update { }
it.copy(files = r.toImmutableList())
} }
} }
}
if (filters.files) {
launch {
fileRepository.search(
query,
filters.allowNetwork
)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(files = r.toImmutableList())
}
}
}
} }
launch { launch {
customAttributesRepository.search(query) customAttributesRepository.search(query)