Add search filters
This commit is contained in:
parent
3c489fc648
commit
58ebd5646b
@ -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>
|
||||||
5
core/ktx/src/main/java/de/mm20/launcher2/ktx/Boolean.kt
Normal file
5
core/ktx/src/main/java/de/mm20/launcher2/ktx/Boolean.kt
Normal file
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user