Add extended filter view

This commit is contained in:
MM20 2024-04-21 17:29:28 +02:00
parent 0ebf0f872d
commit 9b29696757
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
12 changed files with 534 additions and 257 deletions

View File

@ -250,7 +250,7 @@ fun PagerScaffold(
} else { } else {
pagerState.animateScrollToPage(0) pagerState.animateScrollToPage(0)
} }
searchVM.search("") searchVM.reset()
} }
} }
@ -262,7 +262,7 @@ fun PagerScaffold(
when { when {
isSearchOpen -> { isSearchOpen -> {
viewModel.closeSearch() viewModel.closeSearch()
searchVM.search("") searchVM.reset()
true true
} }

View File

@ -252,7 +252,7 @@ fun PullDownScaffold(
animationSpec = spring(stiffness = Spring.StiffnessMediumLow) animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
) )
} else { } else {
searchVM.search("") searchVM.reset()
if (viewModel.skipNextSearchAnimation) { if (viewModel.skipNextSearchAnimation) {
pagerState.scrollToPage(0) pagerState.scrollToPage(0)
viewModel.skipNextSearchAnimation = false viewModel.skipNextSearchAnimation = false
@ -273,7 +273,7 @@ fun PullDownScaffold(
when { when {
isSearchOpen -> { isSearchOpen -> {
viewModel.closeSearch() viewModel.closeSearch()
searchVM.search("") searchVM.reset()
true true
} }

View File

@ -279,7 +279,7 @@ abstract class SharedLauncherActivity(
super.onResume() super.onResume()
if (System.currentTimeMillis() - pauseTime > 20000) { if (System.currentTimeMillis() - pauseTime > 20000) {
viewModel.closeSearchWithoutAnimation() viewModel.closeSearchWithoutAnimation()
searchVM.search("") searchVM.reset()
} }
} }

View File

@ -1,10 +1,13 @@
package de.mm20.launcher2.ui.launcher.search package de.mm20.launcher2.ui.launcher.search
import androidx.activity.compose.BackHandler
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -28,10 +31,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -39,7 +42,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.FavoritesTagSelector import de.mm20.launcher2.ui.common.FavoritesTagSelector
@ -51,6 +53,7 @@ import de.mm20.launcher2.ui.launcher.search.calculator.CalculatorItem
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
import de.mm20.launcher2.ui.launcher.search.favorites.SearchFavoritesVM import de.mm20.launcher2.ui.launcher.search.favorites.SearchFavoritesVM
import de.mm20.launcher2.ui.launcher.search.filters.SearchFilters
import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterItem import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterItem
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItem import de.mm20.launcher2.ui.launcher.search.website.WebsiteItem
import de.mm20.launcher2.ui.launcher.search.wikipedia.ArticleItem import de.mm20.launcher2.ui.launcher.search.wikipedia.ArticleItem
@ -116,253 +119,290 @@ fun SearchColumn(
val favoritesEditButton by favoritesVM.showEditButton.collectAsState(false) val favoritesEditButton by favoritesVM.showEditButton.collectAsState(false)
val favoritesTagsExpanded by favoritesVM.tagsExpanded.collectAsState(false) val favoritesTagsExpanded by favoritesVM.tagsExpanded.collectAsState(false)
LazyColumn( val showFilters by viewModel.showFilters
state = state,
modifier = modifier,
userScrollEnabled = userScrollEnabled,
contentPadding = paddingValues,
reverseLayout = reverse,
) {
if (!hideFavs && favoritesEnabled) {
GridResults(
items = favorites.toImmutableList(),
columns = columns,
key = "favorites",
reverse = reverse,
before = if (favorites.isEmpty()) {
{
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(
if (selectedTag == null) R.string.favorites_empty else R.string.favorites_empty_tag
),
icon = if (selectedTag == null) Icons.Rounded.Star else Icons.Rounded.Tag,
)
}
} else null,
after = if (pinnedTags.isEmpty() && !favoritesEditButton) {
null
} else {
{
FavoritesTagSelector(
tags = pinnedTags,
selectedTag = selectedTag,
editButton = favoritesEditButton,
reverse = reverse,
onSelectTag = { favoritesVM.selectTag(it) },
scrollState = tagsScrollState,
expanded = favoritesTagsExpanded,
onExpand = { favoritesVM.setTagsExpanded(it) }
)
}
},
highlightedItem = bestMatch as? SavableSearchable
)
}
GridResults( AnimatedContent(showFilters) {
items = if (separateWorkProfile) if ((showWorkProfileApps || apps.isEmpty()) && workApps.isNotEmpty()) workApps.toImmutableList() else apps.toImmutableList() else listOf(apps, workApps).flatten().sorted().toImmutableList(), if (it) {
columns = columns, BackHandler {
reverse = reverse, viewModel.showFilters.value = false
key = "apps", }
before = if (separateWorkProfile && workApps.isNotEmpty() && apps.isNotEmpty()) { Box(
{ modifier = modifier
Row( .fillMaxSize()
.padding(paddingValues),
contentAlignment = if (reverse) Alignment.BottomCenter else Alignment.TopCenter,
) {
LauncherCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
SearchFilters(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp) .padding(12.dp),
.padding( filters = viewModel.filters.value,
top = if (reverse) 4.dp else 8.dp, onFiltersChange = {
bottom = if (reverse) 8.dp else 4.dp viewModel.setFilters(it)
), }
) { )
FilterChip( }
modifier = Modifier.padding(horizontal = 8.dp), }
selected = !showWorkProfileApps, } else {
onClick = { showWorkProfileApps = false }, LazyColumn(
leadingIcon = { state = state,
Icon( modifier = modifier,
imageVector = Icons.Rounded.Person, userScrollEnabled = userScrollEnabled,
contentDescription = null, contentPadding = paddingValues,
modifier = Modifier.size(FilterChipDefaults.IconSize) reverseLayout = reverse,
) ) {
}, if (!hideFavs && favoritesEnabled) {
label = { GridResults(
Text( items = favorites.toImmutableList(),
stringResource(R.string.apps_profile_main), columns = columns,
maxLines = 1, key = "favorites",
overflow = TextOverflow.Ellipsis reverse = reverse,
before = if (favorites.isEmpty()) {
{
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(
if (selectedTag == null) R.string.favorites_empty else R.string.favorites_empty_tag
),
icon = if (selectedTag == null) Icons.Rounded.Star else Icons.Rounded.Tag,
) )
} }
) } else null,
FilterChip( after = if (pinnedTags.isEmpty() && !favoritesEditButton) {
selected = showWorkProfileApps, null
onClick = { showWorkProfileApps = true }, } else {
leadingIcon = { {
Icon( FavoritesTagSelector(
imageVector = Icons.Rounded.Work, tags = pinnedTags,
contentDescription = null, selectedTag = selectedTag,
modifier = Modifier.size(FilterChipDefaults.IconSize) editButton = favoritesEditButton,
) reverse = reverse,
}, onSelectTag = { favoritesVM.selectTag(it) },
label = { scrollState = tagsScrollState,
Text( expanded = favoritesTagsExpanded,
stringResource(R.string.apps_profile_work), onExpand = { favoritesVM.setTagsExpanded(it) }
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
} }
) },
highlightedItem = bestMatch as? SavableSearchable
)
}
GridResults(
items = if (separateWorkProfile) if ((showWorkProfileApps || apps.isEmpty()) && workApps.isNotEmpty()) workApps.toImmutableList() else apps.toImmutableList() else listOf(
apps,
workApps
).flatten().sorted().toImmutableList(),
columns = columns,
reverse = reverse,
key = "apps",
before = if (separateWorkProfile && workApps.isNotEmpty() && apps.isNotEmpty()) {
{
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.padding(
top = if (reverse) 4.dp else 8.dp,
bottom = if (reverse) 8.dp else 4.dp
),
) {
FilterChip(
modifier = Modifier.padding(horizontal = 8.dp),
selected = !showWorkProfileApps,
onClick = { showWorkProfileApps = false },
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Person,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = {
Text(
stringResource(R.string.apps_profile_main),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
FilterChip(
selected = showWorkProfileApps,
onClick = { showWorkProfileApps = true },
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Work,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = {
Text(
stringResource(R.string.apps_profile_work),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
} else null,
highlightedItem = bestMatch as? SavableSearchable
)
ListResults(
before = if (missingShortcutsPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(
R.string.missing_permission_appshortcuts_search,
stringResource(R.string.app_name)
),
onClick = { viewModel.requestAppShortcutPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableAppShortcutSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = appShortcuts.toImmutableList(),
reverse = reverse,
key = "shortcuts",
highlightedItem = bestMatch as? SavableSearchable
)
for (conv in unitConverter) {
SingleResult {
UnitConverterItem(unitConverter = conv)
} }
} }
} else null, for (calc in calculator) {
highlightedItem = bestMatch as? SavableSearchable SingleResult {
) CalculatorItem(calculator = calc)
ListResults( }
before = if (missingShortcutsPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(
R.string.missing_permission_appshortcuts_search,
stringResource(R.string.app_name)
),
onClick = { viewModel.requestAppShortcutPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableAppShortcutSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
} }
} else null, ListResults(
items = appShortcuts.toImmutableList(), before = if (missingCalendarPermission && !isSearchEmpty) {
reverse = reverse, {
key = "shortcuts", MissingPermissionBanner(
highlightedItem = bestMatch as? SavableSearchable modifier = Modifier.padding(8.dp),
) text = stringResource(R.string.missing_permission_calendar_search),
for (conv in unitConverter) { onClick = { viewModel.requestCalendarPermission(context as AppCompatActivity) },
SingleResult { secondaryAction = {
UnitConverterItem(unitConverter = conv) OutlinedButton(onClick = {
viewModel.disableCalendarSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = events.toImmutableList(),
reverse = reverse,
key = "events",
highlightedItem = bestMatch as? SavableSearchable
)
ListResults(
before = if (missingContactsPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_contact_search),
onClick = { viewModel.requestContactsPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableContactsSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = contacts.toImmutableList(),
reverse = reverse,
key = "contacts",
highlightedItem = bestMatch as? SavableSearchable
)
ListResults(
before = if (missingLocationPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_location_search),
onClick = { viewModel.requestLocationPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableLocationSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = locations.toImmutableList(),
reverse = reverse,
key = "locations",
highlightedItem = bestMatch as? SavableSearchable
)
for (wiki in wikipedia) {
SingleResult(highlight = bestMatch == wiki) {
ArticleItem(article = wiki)
}
}
for (ws in website) {
SingleResult(highlight = bestMatch == ws) {
WebsiteItem(website = ws)
}
}
ListResults(
before = if (missingFilesPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_files_search),
onClick = { viewModel.requestFilesPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableFilesSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = files.toImmutableList(),
reverse = reverse,
key = "files",
highlightedItem = bestMatch as? SavableSearchable
)
} }
} }
for (calc in calculator) {
SingleResult {
CalculatorItem(calculator = calc)
}
}
ListResults(
before = if (missingCalendarPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_calendar_search),
onClick = { viewModel.requestCalendarPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableCalendarSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = events.toImmutableList(),
reverse = reverse,
key = "events",
highlightedItem = bestMatch as? SavableSearchable
)
ListResults(
before = if (missingContactsPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_contact_search),
onClick = { viewModel.requestContactsPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableContactsSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = contacts.toImmutableList(),
reverse = reverse,
key = "contacts",
highlightedItem = bestMatch as? SavableSearchable
)
ListResults(
before = if (missingLocationPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_location_search),
onClick = { viewModel.requestLocationPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableLocationSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = locations.toImmutableList(),
reverse = reverse,
key = "locations",
highlightedItem = bestMatch as? SavableSearchable
)
for (wiki in wikipedia) {
SingleResult(highlight = bestMatch == wiki) {
ArticleItem(article = wiki)
}
}
for (ws in website) {
SingleResult(highlight = bestMatch == ws) {
WebsiteItem(website = ws)
}
}
ListResults(
before = if (missingFilesPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_files_search),
onClick = { viewModel.requestFilesPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableFilesSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = files.toImmutableList(),
reverse = reverse,
key = "files",
highlightedItem = bestMatch as? SavableSearchable
)
} }
val sheetManager = LocalBottomSheetManager.current val sheetManager = LocalBottomSheetManager.current
if (sheetManager.hiddenItemsSheetShown.value) { if (sheetManager.hiddenItemsSheetShown.value) {
HiddenItemsSheet( HiddenItemsSheet(

View File

@ -91,6 +91,8 @@ class SearchVM : ViewModel(), KoinComponent {
val favoritesEnabled = searchUiSettings.favorites val favoritesEnabled = searchUiSettings.favorites
val hideFavorites = mutableStateOf(false) val hideFavorites = mutableStateOf(false)
val showFilters = mutableStateOf(false)
val filters = mutableStateOf(SearchFilters()) val filters = mutableStateOf(SearchFilters())
val separateWorkProfile = searchUiSettings.separateWorkProfile val separateWorkProfile = searchUiSettings.separateWorkProfile
@ -125,9 +127,21 @@ class SearchVM : ViewModel(), KoinComponent {
search(searchQuery.value, forceRestart = true) search(searchQuery.value, forceRestart = true)
} }
fun closeFilters() {
showFilters.value = false
}
fun reset() {
closeFilters()
search("")
}
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
if (searchQuery.value != query) {
showFilters.value = false
}
searchQuery.value = query searchQuery.value = query
isSearchEmpty.value = query.isEmpty() isSearchEmpty.value = query.isEmpty()
hiddenResults.value = emptyList() hiddenResults.value = emptyList()
@ -145,7 +159,7 @@ class SearchVM : ViewModel(), KoinComponent {
searchUiSettings.resultOrder.collectLatest { resultOrder -> searchUiSettings.resultOrder.collectLatest { resultOrder ->
searchService.search( searchService.search(
query, query,
filters = filters, filters = if (query.isEmpty()) filters.copy(apps = true) else filters,
).collectLatest { results -> ).collectLatest { results ->
var resultsList = withContext(Dispatchers.Default) { var resultsList = withContext(Dispatchers.Default) {
listOfNotNull( listOfNotNull(

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.ui.launcher.searchbar package de.mm20.launcher2.ui.launcher.search.filters
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
@ -91,7 +91,7 @@ fun KeyboardFilterBar(filters: SearchFilters, onFiltersChange: (SearchFilters) -
modifier = Modifier.size(FilterChipDefaults.IconSize) modifier = Modifier.size(FilterChipDefaults.IconSize)
) )
}, },
label = { Text("Apps") } label = { Text(stringResource(R.string.search_filter_apps)) }
) )
FilterChip( FilterChip(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),

View File

@ -0,0 +1,212 @@
package de.mm20.launcher2.ui.launcher.search.filters
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.Text
import androidx.compose.runtime.Composable
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
@Composable
fun SearchFilters(
filters: SearchFilters,
onFiltersChange: (SearchFilters) -> Unit,
modifier: Modifier = Modifier
) {
val allCategoriesEnabled = filters.allCategoriesEnabled
Column(
modifier = modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 4.dp),
) {
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)) }
)
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
FlowRow {
FilterChip(
modifier = Modifier.padding(end = 16.dp),
selected = filters.apps && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleApps())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Apps,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.search_filter_apps)) }
)
FilterChip(
modifier = Modifier.padding(end = 16.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 = 16.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 = 16.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 = 16.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 = 16.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 = 16.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 = 16.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)) }
)
}
HorizontalDivider(modifier = Modifier.padding(vertical = 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)) }
)
}
}

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.ui.launcher.searchbar package de.mm20.launcher2.ui.launcher.search.filters
import de.mm20.launcher2.search.SearchFilters import de.mm20.launcher2.search.SearchFilters

View File

@ -16,7 +16,6 @@ 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.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
@ -33,6 +32,7 @@ import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.component.SearchBar import de.mm20.launcher2.ui.component.SearchBar
import de.mm20.launcher2.ui.component.SearchBarLevel import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.search.filters.KeyboardFilterBar
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
@Composable @Composable
@ -54,19 +54,14 @@ fun LauncherSearchBar(
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val sheetManager = LocalBottomSheetManager.current
val searchVM: SearchVM = viewModel() val searchVM: SearchVM = viewModel()
val hiddenItemsButtonEnabled by searchVM.hiddenResultsButton.collectAsState(false)
val hiddenItems by searchVM.hiddenResults
LaunchedEffect(focused) { LaunchedEffect(focused) {
if (focused) focusRequester.requestFocus() if (focused) focusRequester.requestFocus()
else focusManager.clearFocus() else focusManager.clearFocus()
} }
if (isSearchOpen && WindowInsets.isImeVisible) { if (isSearchOpen && !searchVM.showFilters.value && WindowInsets.isImeVisible) {
KeyboardFilterBar( KeyboardFilterBar(
filters = searchVM.filters.value, filters = searchVM.filters.value,
onFiltersChange = { onFiltersChange = {
@ -89,8 +84,11 @@ fun LauncherSearchBar(
exit = scaleOut(tween(100)) exit = scaleOut(tween(100))
) { ) {
FilledIconButton( FilledIconButton(
onClick = { }, onClick = {
colors = IconButtonDefaults.iconButtonColors() searchVM.showFilters.value = !searchVM.showFilters.value
},
colors = if (searchVM.showFilters.value) IconButtonDefaults.filledTonalIconButtonColors()
else IconButtonDefaults.iconButtonColors()
) { ) {
Box { Box {
Icon(imageVector = Icons.Rounded.FilterAlt, contentDescription = null) Icon(imageVector = Icons.Rounded.FilterAlt, contentDescription = null)
@ -98,7 +96,9 @@ fun LauncherSearchBar(
!searchVM.filters.value.allCategoriesEnabled, !searchVM.filters.value.allCategoriesEnabled,
enter = scaleIn(tween(100)), enter = scaleIn(tween(100)),
exit = scaleOut(tween(100)), exit = scaleOut(tween(100)),
modifier = Modifier.align(Alignment.BottomEnd).offset(-3.dp, -3.dp) modifier = Modifier
.align(Alignment.BottomEnd)
.offset(-3.dp, -3.dp)
) { ) {
Badge( Badge(
containerColor = MaterialTheme.colorScheme.tertiary, containerColor = MaterialTheme.colorScheme.tertiary,

View File

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

View File

@ -917,4 +917,5 @@
<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_tools">Tools</string>
<string name="search_filter_online">Online results</string> <string name="search_filter_online">Online results</string>
<string name="search_filter_apps">Apps</string>
</resources> </resources>

View File

@ -172,7 +172,19 @@ internal class SearchServiceImpl(
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(other = r.toImmutableList()) it.copy(other = r
.filter {
filters.apps && it is Application ||
filters.shortcuts && it is AppShortcut ||
filters.contacts && it is Contact ||
filters.events && it is CalendarEvent ||
filters.files && it is File ||
filters.websites && it is Website ||
filters.articles && it is Article ||
filters.places && it is Location
}
.toImmutableList()
)
} }
} }
} }