Add search filters

This commit is contained in:
MM20 2024-04-21 01:26:22 +02:00
parent 3c489fc648
commit 58ebd5646b
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
14 changed files with 574 additions and 98 deletions

View File

@ -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,

View File

@ -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) {

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

@ -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()
}
}
}
}

View File

@ -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) },

View File

@ -0,0 +1,2 @@
package de.mm20.launcher2.ui.launcher.searchbar

View File

@ -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)
}

View File

@ -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)

View File

@ -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>

View File

@ -0,0 +1,5 @@
package de.mm20.launcher2.ktx
inline fun Boolean.toInt(): Int {
return if (this) 1 else 0
}

View File

@ -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()
}

View File

@ -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)