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 19cef02a..3d1d5eeb 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 @@ -59,8 +59,10 @@ fun AssistantScaffold( val searchState = rememberLazyListState() + val filterBar by searchVM.filterBar.collectAsState(false) + val keyboardFilterBarPadding by animateDpAsState( - if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value) 50.dp else 0.dp + if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value && filterBar) 50.dp else 0.dp ) val isSearchAtStart by remember { 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 da937c03..cd04dcd6 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 @@ -129,8 +129,10 @@ fun PagerScaffold( val pagerState = rememberPagerState { 2 } + val filterBar by searchVM.filterBar.collectAsState(false) + val keyboardFilterBarPadding by animateDpAsState( - if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value) 50.dp else 0.dp + if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value && filterBar) 50.dp else 0.dp ) val isSearchAtBottom by remember { 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 01cb6155..74cdf35f 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 @@ -1,5 +1,6 @@ package de.mm20.launcher2.ui.launcher +import android.util.Log import android.view.HapticFeedbackConstants import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility @@ -114,8 +115,9 @@ fun PullDownScaffold( val maxOffset = with(density) { 64.dp.toPx() } val toggleSearchThreshold = with(density) { 48.dp.toPx() } + val filterBar by searchVM.filterBar.collectAsState(false) val keyboardFilterBarPadding by animateDpAsState( - if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value) 50.dp else 0.dp + if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value && filterBar) 50.dp else 0.dp ) val isSearchAtTop by remember { 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 11d4cd33..d5c645cd 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 @@ -93,8 +93,9 @@ class SearchVM : ViewModel(), KoinComponent { val showFilters = mutableStateOf(false) - val defaultFilters = searchFilterSettings.defaultFilter.stateIn(viewModelScope, SharingStarted.Eagerly, SearchFilters()) + private val defaultFilters = searchFilterSettings.defaultFilter.stateIn(viewModelScope, SharingStarted.Eagerly, SearchFilters()) val filters = mutableStateOf(defaultFilters.value) + val filterBar = searchFilterSettings.filterBar val separateWorkProfile = searchUiSettings.separateWorkProfile diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/filters/SearchFilters.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/filters/SearchFilters.kt index 12df4394..c6b7142b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/filters/SearchFilters.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/filters/SearchFilters.kt @@ -34,7 +34,8 @@ import de.mm20.launcher2.ui.icons.Wikipedia fun SearchFilters( filters: SearchFilters, onFiltersChange: (SearchFilters) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + settings: Boolean = false ) { val allCategoriesEnabled = filters.allCategoriesEnabled Column( @@ -60,9 +61,13 @@ fun SearchFilters( FlowRow { FilterChip( modifier = Modifier.padding(end = 16.dp), - selected = filters.apps && !allCategoriesEnabled, + selected = filters.apps && (!allCategoriesEnabled || settings), onClick = { - onFiltersChange(filters.toggleApps()) + if (settings) { + onFiltersChange(filters.copy(apps = !filters.apps)) + } else { + onFiltersChange(filters.toggleApps()) + } }, leadingIcon = { Icon( @@ -75,9 +80,13 @@ fun SearchFilters( ) FilterChip( modifier = Modifier.padding(end = 16.dp), - selected = filters.files && !allCategoriesEnabled, + selected = filters.files && (!allCategoriesEnabled || settings), onClick = { - onFiltersChange(filters.toggleFiles()) + if (settings) { + onFiltersChange(filters.copy(files = !filters.files)) + } else { + onFiltersChange(filters.toggleFiles()) + } }, leadingIcon = { Icon( @@ -90,9 +99,13 @@ fun SearchFilters( ) FilterChip( modifier = Modifier.padding(end = 16.dp), - selected = filters.contacts && !allCategoriesEnabled, + selected = filters.contacts && (!allCategoriesEnabled || settings), onClick = { - onFiltersChange(filters.toggleContacts()) + if (settings) { + onFiltersChange(filters.copy(contacts = !filters.contacts)) + } else { + onFiltersChange(filters.toggleContacts()) + } }, leadingIcon = { Icon( @@ -105,9 +118,13 @@ fun SearchFilters( ) FilterChip( modifier = Modifier.padding(end = 16.dp), - selected = filters.events && !allCategoriesEnabled, + selected = filters.events && (!allCategoriesEnabled || settings), onClick = { - onFiltersChange(filters.toggleEvents()) + if (settings) { + onFiltersChange(filters.copy(events = !filters.events)) + } else { + onFiltersChange(filters.toggleEvents()) + } }, leadingIcon = { Icon( @@ -120,9 +137,13 @@ fun SearchFilters( ) FilterChip( modifier = Modifier.padding(end = 16.dp), - selected = filters.shortcuts && !allCategoriesEnabled, + selected = filters.shortcuts && (!allCategoriesEnabled || settings), onClick = { - onFiltersChange(filters.toggleShortcuts()) + if (settings) { + onFiltersChange(filters.copy(shortcuts = !filters.shortcuts)) + } else { + onFiltersChange(filters.toggleShortcuts()) + } }, leadingIcon = { Icon( @@ -135,9 +156,13 @@ fun SearchFilters( ) FilterChip( modifier = Modifier.padding(end = 16.dp), - selected = filters.articles && !allCategoriesEnabled, + selected = filters.articles && (!allCategoriesEnabled || settings), onClick = { - onFiltersChange(filters.toggleArticles()) + if (settings) { + onFiltersChange(filters.copy(articles = !filters.articles)) + } else { + onFiltersChange(filters.toggleArticles()) + } }, leadingIcon = { Icon( @@ -150,9 +175,13 @@ fun SearchFilters( ) FilterChip( modifier = Modifier.padding(end = 16.dp), - selected = filters.websites && !allCategoriesEnabled, + selected = filters.websites && (!allCategoriesEnabled || settings), onClick = { - onFiltersChange(filters.toggleWebsites()) + if (settings) { + onFiltersChange(filters.copy(websites = !filters.websites)) + } else { + onFiltersChange(filters.toggleWebsites()) + } }, leadingIcon = { Icon( @@ -165,9 +194,13 @@ fun SearchFilters( ) FilterChip( modifier = Modifier.padding(end = 16.dp), - selected = filters.places && !allCategoriesEnabled, + selected = filters.places && (!allCategoriesEnabled || settings), onClick = { - onFiltersChange(filters.togglePlaces()) + if (settings) { + onFiltersChange(filters.copy(places = !filters.places)) + } else { + onFiltersChange(filters.togglePlaces()) + } }, leadingIcon = { Icon( @@ -179,9 +212,13 @@ fun SearchFilters( label = { Text(stringResource(R.string.preference_search_locations)) } ) FilterChip( - selected = filters.tools && !allCategoriesEnabled, + selected = filters.tools && (!allCategoriesEnabled || settings), onClick = { - onFiltersChange(filters.toggleTools()) + if (settings) { + onFiltersChange(filters.copy(tools = !filters.tools)) + } else { + onFiltersChange(filters.toggleTools()) + } }, leadingIcon = { Icon( 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 841169f4..e1b661e3 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 @@ -23,6 +23,8 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme 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 @@ -65,6 +67,7 @@ fun LauncherSearchBar( else focusManager.clearFocus() } + val filterBar by searchVM.filterBar.collectAsState(false) val _value = value() @@ -127,7 +130,7 @@ fun LauncherSearchBar( onKeyboardActionGo = onKeyboardActionGo ) - AnimatedVisibility (isSearchOpen && !searchVM.showFilters.value + AnimatedVisibility (filterBar && isSearchOpen && !searchVM.showFilters.value // Use imeAnimationTarget instead of isImeVisible to animate the filter bar at the same time as the keyboard && WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0, enter = fadeIn(), 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 e00c93ea..e962039a 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 @@ -1,37 +1,58 @@ package de.mm20.launcher2.ui.settings.search -import android.content.Context -import android.content.pm.LauncherApps import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.* +import androidx.compose.material.icons.rounded.AppShortcut +import androidx.compose.material.icons.rounded.ArrowOutward +import androidx.compose.material.icons.rounded.Calculate +import androidx.compose.material.icons.rounded.Description +import androidx.compose.material.icons.rounded.FilterAlt +import androidx.compose.material.icons.rounded.Keyboard +import androidx.compose.material.icons.rounded.Loop +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.Sort +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.Tag +import androidx.compose.material.icons.rounded.Today +import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material.icons.rounded.Warning +import androidx.compose.material.icons.rounded.Work +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.core.content.getSystemService import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import de.mm20.launcher2.preferences.LegacySettings import de.mm20.launcher2.preferences.SearchResultOrder import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.MissingPermissionBanner -import de.mm20.launcher2.ui.component.preferences.* +import de.mm20.launcher2.ui.component.SmallMessage +import de.mm20.launcher2.ui.component.preferences.ListPreference +import de.mm20.launcher2.ui.component.preferences.Preference +import de.mm20.launcher2.ui.component.preferences.PreferenceCategory +import de.mm20.launcher2.ui.component.preferences.PreferenceScreen +import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch +import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.icons.Wikipedia +import de.mm20.launcher2.ui.launcher.search.filters.SearchFilters import de.mm20.launcher2.ui.locals.LocalNavController -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map @Composable fun SearchSettingsScreen() { @@ -50,6 +71,10 @@ fun SearchSettingsScreen() { } } + var showFilterEditor by remember { + mutableStateOf(false) + } + PreferenceScreen(title = stringResource(R.string.preference_screen_search)) { item { @@ -246,6 +271,27 @@ fun SearchSettingsScreen() { ) } } + item { + val filterBar by viewModel.filterBar.collectAsStateWithLifecycle(null) + PreferenceCategory { + Preference( + title = stringResource(R.string.preference_default_filter), + summary = stringResource(R.string.preference_default_filter_summary), + icon = Icons.Rounded.FilterAlt, + onClick = { + showFilterEditor = true + }, + ) + SwitchPreference( + title = stringResource(R.string.preference_filter_bar), + summary = stringResource(R.string.preference_filter_bar_summary), + value = filterBar == true, + onValueChanged = { + viewModel.setFilterBar(it) + } + ) + } + } if (hasWorkProfile) { item { PreferenceCategory { @@ -322,4 +368,28 @@ fun SearchSettingsScreen() { } } } + + if (showFilterEditor) { + val filters by viewModel.searchFilters.collectAsStateWithLifecycle() + BottomSheetDialog(onDismissRequest = { showFilterEditor = false }) { + Column( + modifier = Modifier.padding(it) + ) { + AnimatedVisibility(filters.allowNetwork) { + SmallMessage( + modifier = Modifier.padding(bottom = 16.dp), + icon = Icons.Rounded.Warning, + text = stringResource(R.string.filter_settings_network_warning) + ) + } + SearchFilters( + filters = filters, + onFiltersChange = { + viewModel.setSearchFilters(it) + }, + settings = true, + ) + } + } + } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt index 7f2fcd9e..fbdd95d2 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt @@ -14,11 +14,13 @@ import de.mm20.launcher2.preferences.search.CalculatorSearchSettings import de.mm20.launcher2.preferences.search.CalendarSearchSettings import de.mm20.launcher2.preferences.search.ContactSearchSettings import de.mm20.launcher2.preferences.search.LocationSearchSettings +import de.mm20.launcher2.preferences.search.SearchFilterSettings import de.mm20.launcher2.preferences.search.ShortcutSearchSettings import de.mm20.launcher2.preferences.search.UnitConverterSettings import de.mm20.launcher2.preferences.search.WebsiteSearchSettings import de.mm20.launcher2.preferences.search.WikipediaSearchSettings import de.mm20.launcher2.preferences.ui.SearchUiSettings +import de.mm20.launcher2.search.SearchFilters import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn @@ -34,6 +36,7 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent { private val websiteSearchSettings: WebsiteSearchSettings by inject() private val unitConverterSettings: UnitConverterSettings by inject() private val calculatorSearchSettings: CalculatorSearchSettings by inject() + private val searchFilterSettings: SearchFilterSettings by inject() private val permissionsManager: PermissionsManager by inject() private val locationSearchSettings: LocationSearchSettings by inject() @@ -160,4 +163,18 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent { fun requestAppShortcutsPermission(activity: AppCompatActivity) { permissionsManager.requestPermission(activity, PermissionGroup.AppShortcuts) } + + val filterBar = searchFilterSettings.filterBar + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + fun setFilterBar(filterBar: Boolean) { + searchFilterSettings.setFilterBar(filterBar) + } + + val searchFilters = searchFilterSettings.defaultFilter + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SearchFilters()) + + fun setSearchFilters(searchFilters: SearchFilters) { + searchFilterSettings.setDefaultFilter(searchFilters) + } } \ No newline at end of file diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 16a3fadf..bbcaf834 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -918,4 +918,9 @@ Tools Online results Apps + Default filter + Customize the default filter for searches + Show filter bar + Show quick filters above the keyboard + The current filter enables online results by default. Your search queries might unintentionally be sent to external web services. For privacy reasons, this is not recommended. \ No newline at end of file diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/SearchFilterSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/SearchFilterSettings.kt index fc5fc76d..01e76180 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/SearchFilterSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/SearchFilterSettings.kt @@ -1,6 +1,7 @@ package de.mm20.launcher2.preferences.search import de.mm20.launcher2.preferences.LauncherDataStore +import de.mm20.launcher2.search.SearchFilters import kotlinx.coroutines.flow.map class SearchFilterSettings internal constructor( @@ -8,4 +9,20 @@ class SearchFilterSettings internal constructor( ) { val defaultFilter get() = launcherDataStore.data.map { it.searchFilter } + + fun setDefaultFilter(filter: SearchFilters) { + launcherDataStore.update { + it.copy(searchFilter = filter) + } + } + + val filterBar + get() = launcherDataStore.data.map { it.searchFilterBar } + + fun setFilterBar(filterBar: Boolean) { + launcherDataStore.update { + it.copy(searchFilterBar = filterBar) + } + } + } \ No newline at end of file