From 58ebd5646b83ee6342e6d382ccab6da5dc70aed3 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sun, 21 Apr 2024 01:26:22 +0200 Subject: [PATCH] Add search filters --- .../ui/assistant/AssistantScaffold.kt | 2 +- .../launcher2/ui/common/SearchablePickerVM.kt | 3 +- .../launcher2/ui/launcher/PagerScaffold.kt | 4 +- .../launcher2/ui/launcher/PullDownScaffold.kt | 2 +- .../launcher2/ui/launcher/search/SearchVM.kt | 16 +- .../launcher/searchbar/KeyboardFilterBar.kt | 239 ++++++++++++++++++ .../launcher/searchbar/LauncherSearchBar.kt | 48 +++- .../ui/launcher/searchbar/SearchBarFilters.kt | 2 + .../ui/launcher/searchbar/SearchFilters.kt | 146 +++++++++++ .../settings/search/SearchSettingsScreen.kt | 2 +- core/i18n/src/main/res/values/strings.xml | 2 + .../java/de/mm20/launcher2/ktx/Boolean.kt | 5 + .../de/mm20/launcher2/search/SearchFilters.kt | 23 ++ .../de/mm20/launcher2/search/SearchService.kt | 178 +++++++------ 14 files changed, 574 insertions(+), 98 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/KeyboardFilterBar.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarFilters.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchFilters.kt create mode 100644 core/ktx/src/main/java/de/mm20/launcher2/ktx/Boolean.kt create mode 100644 services/search/src/main/java/de/mm20/launcher2/search/SearchFilters.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt index 2ee1002f..4d6bd4af 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt @@ -199,7 +199,7 @@ fun AssistantScaffold( }, actions = actions, highlightedAction = searchVM.bestMatch.value as? SearchAction, - showHiddenItemsButton = true, + isSearchOpen = true, value = { value }, onValueChange = { searchVM.search(it) }, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt index 4f6f5991..c49f9159 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.search.SavableSearchable +import de.mm20.launcher2.search.SearchFilters import de.mm20.launcher2.search.SearchService import de.mm20.launcher2.search.toList import kotlinx.coroutines.Dispatchers @@ -40,7 +41,7 @@ class SearchablePickerVM : ViewModel(), KoinComponent { searchJob = viewModelScope.launch { searchService.search( query = query, - allowNetwork = true, + filters = SearchFilters(allowNetwork = true) ).collectLatest { if (searchQuery != query) return@collectLatest items = withContext(Dispatchers.Default) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt index 65b6f692..ea1965b8 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt @@ -2,9 +2,7 @@ package de.mm20.launcher2.ui.launcher import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.spring import androidx.compose.animation.slideIn import androidx.compose.animation.slideOut import androidx.compose.foundation.LocalOverscrollConfiguration @@ -627,7 +625,7 @@ fun PagerScaffold( }, actions = actions, highlightedAction = searchVM.bestMatch.value as? SearchAction, - showHiddenItemsButton = isSearchOpen, + isSearchOpen = isSearchOpen, value = { value }, onValueChange = { searchVM.search(it) }, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt index ff5381ea..20f12f81 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt @@ -603,7 +603,7 @@ fun PullDownScaffold( }, actions = actions, highlightedAction = searchVM.bestMatch.value as? SearchAction, - showHiddenItemsButton = isSearchOpen, + isSearchOpen = isSearchOpen, value = { value }, onValueChange = { searchVM.search(it) }, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index ba24a14a..063ba62b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -27,6 +27,7 @@ import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchService import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Location +import de.mm20.launcher2.search.SearchFilters import de.mm20.launcher2.search.Website import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.UnitConverter @@ -90,6 +91,8 @@ class SearchVM : ViewModel(), KoinComponent { val favoritesEnabled = searchUiSettings.favorites val hideFavorites = mutableStateOf(false) + val filters = mutableStateOf(SearchFilters()) + val separateWorkProfile = searchUiSettings.separateWorkProfile 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 fun search(query: String, forceRestart: Boolean = false) { if (searchQuery.value == query && !forceRestart) return @@ -124,9 +132,10 @@ class SearchVM : ViewModel(), KoinComponent { isSearchEmpty.value = query.isEmpty() hiddenResults.value = emptyList() + val filters = filters.value + if (isSearchEmpty.value) bestMatch.value = null - try { searchJob?.cancel() } catch (_: CancellationException) { @@ -136,7 +145,7 @@ class SearchVM : ViewModel(), KoinComponent { searchUiSettings.resultOrder.collectLatest { resultOrder -> searchService.search( query, - allowNetwork = true, + filters = filters, ).collectLatest { results -> var resultsList = withContext(Dispatchers.Default) { listOfNotNull( @@ -219,10 +228,9 @@ class SearchVM : ViewModel(), KoinComponent { val actions = mutableListOf() for (r in resultsList) { when { - r is SavableSearchable && hiddenKeys.contains(r.key) -> { + r is SavableSearchable && hiddenKeys.contains(r.key) && !filters.hiddenItems -> { hidden.add(r) } - r is Application && r.profile == AppProfile.Work -> workApps.add(r) r is Application -> apps.add(r) r is AppShortcut -> shortcuts.add(r) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/KeyboardFilterBar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/KeyboardFilterBar.kt new file mode 100644 index 00000000..2dd86537 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/KeyboardFilterBar.kt @@ -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() + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt index 532ae5f3..a71a8473 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt @@ -4,20 +4,29 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.scaleIn 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.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.Icon import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.preferences.SearchBarStyle import de.mm20.launcher2.searchactions.actions.SearchAction @@ -37,7 +46,7 @@ fun LauncherSearchBar( onFocusChange: (Boolean) -> Unit, actions: List, highlightedAction: SearchAction?, - showHiddenItemsButton: Boolean = false, + isSearchOpen: Boolean = false, reverse: Boolean = false, darkColors: Boolean = false, onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null @@ -57,6 +66,15 @@ fun LauncherSearchBar( else focusManager.clearFocus() } + if (isSearchOpen && WindowInsets.isImeVisible) { + KeyboardFilterBar( + filters = searchVM.filters.value, + onFiltersChange = { + searchVM.setFilters(it) + } + ) + } + val _value = value() SearchBar( @@ -66,21 +84,37 @@ fun LauncherSearchBar( darkColors = darkColors, menu = { AnimatedVisibility( - hiddenItemsButtonEnabled && showHiddenItemsButton && hiddenItems.isNotEmpty(), + isSearchOpen, enter = scaleIn(tween(100)), exit = scaleOut(tween(100)) ) { FilledIconButton( - onClick = { sheetManager.showHiddenItemsSheet() }, - colors = if (sheetManager.hiddenItemsSheetShown.value) IconButtonDefaults.filledTonalIconButtonColors() else IconButtonDefaults.iconButtonColors() + onClick = { }, + 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) }, actions = { - SearchBarActions(actions = actions, reverse = reverse, highlightedAction = highlightedAction) + SearchBarActions( + actions = actions, + reverse = reverse, + highlightedAction = highlightedAction + ) }, focusRequester = focusRequester, onFocus = { onFocusChange(true) }, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarFilters.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarFilters.kt new file mode 100644 index 00000000..bd35cb31 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarFilters.kt @@ -0,0 +1,2 @@ +package de.mm20.launcher2.ui.launcher.searchbar + diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchFilters.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchFilters.kt new file mode 100644 index 00000000..b0eb3c79 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchFilters.kt @@ -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) +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt index 1b39357b..e00c93ea 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt @@ -195,7 +195,7 @@ fun SearchSettingsScreen() { SwitchPreference( title = stringResource(R.string.preference_search_websites), summary = stringResource(R.string.preference_search_websites_summary), - icon = Icons.Rounded.Language, + icon = Icons.Rounded.Public, value = websites == true, onValueChanged = { viewModel.setWebsites(it) diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index f06964d9..c928419a 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -915,4 +915,6 @@ Custom widget Standard Outlined + Tools + Online results \ No newline at end of file diff --git a/core/ktx/src/main/java/de/mm20/launcher2/ktx/Boolean.kt b/core/ktx/src/main/java/de/mm20/launcher2/ktx/Boolean.kt new file mode 100644 index 00000000..5c8196f6 --- /dev/null +++ b/core/ktx/src/main/java/de/mm20/launcher2/ktx/Boolean.kt @@ -0,0 +1,5 @@ +package de.mm20.launcher2.ktx + +inline fun Boolean.toInt(): Int { + return if (this) 1 else 0 +} \ No newline at end of file diff --git a/services/search/src/main/java/de/mm20/launcher2/search/SearchFilters.kt b/services/search/src/main/java/de/mm20/launcher2/search/SearchFilters.kt new file mode 100644 index 00000000..273e35f7 --- /dev/null +++ b/services/search/src/main/java/de/mm20/launcher2/search/SearchFilters.kt @@ -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() +} diff --git a/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt b/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt index 6c92b2d8..3e199a27 100644 --- a/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt +++ b/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.supervisorScope interface SearchService { fun search( query: String, - allowNetwork: Boolean = false, + filters: SearchFilters, ): Flow } @@ -44,7 +44,7 @@ internal class SearchServiceImpl( override fun search( query: String, - allowNetwork: Boolean, + filters: SearchFilters, ): Flow = channelFlow { val results = MutableStateFlow(SearchResults()) supervisorScope { @@ -56,98 +56,116 @@ internal class SearchServiceImpl( } } } - launch { - appRepository.search(query, allowNetwork) - .withCustomLabels(customAttributesRepository) - .collectLatest { r -> - results.update { - it.copy(apps = r.toImmutableList()) + if (filters.apps) { + launch { + appRepository.search(query, filters.allowNetwork) + .withCustomLabels(customAttributesRepository) + .collectLatest { r -> + 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 { - unitConverterRepository.search(query) - .collectLatest { r -> + if (filters.shortcuts) { + launch { + 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 { - it.copy(unitConverters = r?.let { persistentListOf(it) } + it.copy(calculators = r?.let { persistentListOf(it) } ?: persistentListOf()) } } - } - launch { - websiteRepository.search(query, allowNetwork) - .withCustomLabels(customAttributesRepository) - .collectLatest { r -> - results.update { - it.copy(websites = r.toImmutableList()) + } + launch { + unitConverterRepository.search(query) + .collectLatest { r -> + results.update { + it.copy(unitConverters = r?.let { persistentListOf(it) } + ?: persistentListOf()) + } } - } + } } - launch { - delay(750) - articleRepository.search(query, allowNetwork) - .withCustomLabels(customAttributesRepository) - .collectLatest { r -> - results.update { - it.copy(wikipedia = r.toImmutableList()) + if (filters.websites) { + launch { + websiteRepository.search(query, filters.allowNetwork) + .withCustomLabels(customAttributesRepository) + .collectLatest { r -> + results.update { + it.copy(websites = r.toImmutableList()) + } } - } + } } - launch { - locationRepository.search(query, allowNetwork) - .withCustomLabels(customAttributesRepository) - .collectLatest { r -> - results.update { - it.copy(locations = r.toImmutableList()) + if (filters.articles) { + launch { + delay(750) + articleRepository.search(query, filters.allowNetwork) + .withCustomLabels(customAttributesRepository) + .collectLatest { r -> + results.update { + it.copy(wikipedia = r.toImmutableList()) + } } - } + } } - launch { - fileRepository.search( - query, - allowNetwork - ) - .withCustomLabels(customAttributesRepository) - .collectLatest { r -> - results.update { - it.copy(files = r.toImmutableList()) + if (filters.places) { + launch { + locationRepository.search(query, filters.allowNetwork) + .withCustomLabels(customAttributesRepository) + .collectLatest { r -> + results.update { + it.copy(locations = r.toImmutableList()) + } } - } + } + } + if (filters.files) { + launch { + fileRepository.search( + query, + filters.allowNetwork + ) + .withCustomLabels(customAttributesRepository) + .collectLatest { r -> + results.update { + it.copy(files = r.toImmutableList()) + } + } + } } launch { customAttributesRepository.search(query)