Loading app/src/main/java/foundation/e/apps/ui/compose/components/SearchPlaceholder.kt 0 → 100644 +75 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.ui.compose.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.tooling.preview.Preview import androidx.compose.ui.unit.dp import foundation.e.apps.R import foundation.e.apps.ui.compose.theme.AppTheme @Composable fun SearchPlaceholder(modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxSize(), contentAlignment = Alignment.Center, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), ) { Icon( imageVector = Icons.Outlined.Search, contentDescription = null, tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.45f), modifier = Modifier .padding(bottom = 4.dp) .size(72.dp), ) Text( text = stringResource(id = R.string.search_hint), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.72f), ) } } } @Preview(showBackground = false) @Composable private fun SearchPlaceholderPreview() { AppTheme(darkTheme = true) { SearchPlaceholder() } } app/src/main/java/foundation/e/apps/ui/compose/screens/SearchScreen.kt 0 → 100644 +158 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.ui.compose.screens import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold 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.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.repeatOnLifecycle import foundation.e.apps.ui.compose.components.SearchResultsContent import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.search.v2.SearchTabType import foundation.e.apps.ui.search.v2.SearchUiState @Composable fun SearchScreen( uiState: SearchUiState, onQueryChange: (String) -> Unit, onBackClick: () -> Unit, onClearQuery: () -> Unit, onSubmitSearch: (String) -> Unit, onSuggestionSelect: (String) -> Unit, onTabSelect: (SearchTabType) -> Unit, modifier: Modifier = Modifier, ) { val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current val lifecycleOwner = LocalLifecycleOwner.current val focusRequester = remember { FocusRequester() } val shouldAutoFocus = !uiState.hasSubmittedSearch var isSearchExpanded by rememberSaveable { mutableStateOf(shouldAutoFocus) } var hasRequestedInitialFocus by rememberSaveable { mutableStateOf(false) } val selectedTab = uiState.selectedTab val showSuggestions = isSearchExpanded && uiState.isSuggestionVisible val showResults = uiState.hasSubmittedSearch && selectedTab != null && uiState.availableTabs.isNotEmpty() LaunchedEffect(lifecycleOwner, shouldAutoFocus) { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { if (shouldAutoFocus && !hasRequestedInitialFocus) { hasRequestedInitialFocus = true withFrameNanos { focusRequester.requestFocus() keyboardController?.show() } } else { // Intentionally no-op; the initial focus request has already happened. } } } LaunchedEffect(uiState.hasSubmittedSearch) { if (uiState.hasSubmittedSearch) { isSearchExpanded = false focusManager.clearFocus() } else { // Intentionally no-op; results are not active so focus remains unchanged. } } Scaffold( modifier = modifier, topBar = { SearchTopBar( uiState = uiState, expanded = isSearchExpanded, showSuggestions = showSuggestions, focusRequester = focusRequester, focusManager = focusManager, onQueryChange = onQueryChange, onClearQuery = onClearQuery, onSearchSubmit = onSubmitSearch, onSuggestionSelect = onSuggestionSelect, onExpandedChange = { expanded -> isSearchExpanded = expanded if (expanded) { focusRequester.requestFocus() } else { focusManager.clearFocus() } }, onBack = onBackClick, ) }, ) { innerPadding -> Column( modifier = Modifier .fillMaxSize() .padding(innerPadding) ) { when { showResults && selectedTab != null -> { SearchResultsContent( tabs = uiState.availableTabs, selectedTab = selectedTab, resultsByTab = uiState.resultsByTab, onTabSelect = onTabSelect, modifier = Modifier.fillMaxSize(), ) } else -> { // Suggestions render in the top bar dropdown; leave body empty. } } } } } @Preview(showBackground = false) @Composable private fun SearchScreenPreview() { AppTheme(darkTheme = true) { SearchScreen( uiState = SearchUiState( query = "telegram", suggestions = listOf("telegram", "telegram messenger") ), onQueryChange = {}, onBackClick = {}, onClearQuery = {}, onSubmitSearch = {}, onSuggestionSelect = {}, onTabSelect = {}, ) } } app/src/main/java/foundation/e/apps/ui/compose/screens/SearchTopBar.kt 0 → 100644 +216 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.ui.compose.screens import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable 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.focus.FocusManager import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import foundation.e.apps.R import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.search.v2.SearchTabType import foundation.e.apps.ui.search.v2.SearchUiState @OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchTopBar( uiState: SearchUiState, expanded: Boolean, showSuggestions: Boolean, focusRequester: FocusRequester, focusManager: FocusManager, onQueryChange: (String) -> Unit, onClearQuery: () -> Unit, onSearchSubmit: (String) -> Unit, onSuggestionSelect: (String) -> Unit, onExpandedChange: (Boolean) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, ) { SearchBar( modifier = modifier.fillMaxWidth(), colors = SearchBarDefaults.colors( containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), dividerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), ), expanded = expanded, onExpandedChange = { isExpanded -> onExpandedChange(isExpanded) }, inputField = { SearchBarDefaults.InputField( modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester), query = uiState.query, onQueryChange = { query -> onExpandedChange(true) onQueryChange(query) }, onSearch = { query -> onExpandedChange(false) focusManager.clearFocus() onSearchSubmit(query) }, expanded = expanded, onExpandedChange = { isExpanded -> onExpandedChange(isExpanded) }, placeholder = { Text(text = stringResource(id = R.string.search_hint)) }, leadingIcon = { IconButton(onClick = { focusManager.clearFocus() onBack() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, ) } }, trailingIcon = { if (uiState.query.isNotEmpty()) { IconButton(onClick = { onClearQuery() onExpandedChange(true) focusRequester.requestFocus() }) { Icon( imageVector = Icons.Filled.Close, contentDescription = null, ) } } }, ) }, ) { if (showSuggestions) { SuggestionList( suggestions = uiState.suggestions, onSuggestionSelect = { suggestion -> onExpandedChange(false) focusManager.clearFocus() onSuggestionSelect(suggestion) }, modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)), ) } } } @Composable private fun SuggestionList( suggestions: List<String>, onSuggestionSelect: (String) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( modifier = modifier, ) { items( items = suggestions, key = { suggestion -> suggestion }, ) { suggestion -> ListItem( headlineContent = { Text(text = suggestion) }, leadingContent = { Icon( imageVector = Icons.Filled.Search, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, ) }, modifier = Modifier .fillMaxWidth() .clickable { onSuggestionSelect(suggestion) }, ) } } } @Preview(showBackground = true) @Composable private fun SearchTopBarPreview() { AppTheme(darkTheme = true) { val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current var expanded by remember { mutableStateOf(true) } val sampleState = SearchUiState( query = "browser", suggestions = listOf( "browser", "browser apps", "browser downloader", "browser for android", ), isSuggestionVisible = true, availableTabs = listOf( SearchTabType.COMMON_APPS, SearchTabType.OPEN_SOURCE, SearchTabType.WEB_APPS, ), selectedTab = SearchTabType.OPEN_SOURCE, hasSubmittedSearch = false, ) SearchTopBar( uiState = sampleState, expanded = expanded, showSuggestions = expanded && sampleState.suggestions.isNotEmpty(), focusRequester = focusRequester, focusManager = focusManager, onQueryChange = { expanded = true }, onClearQuery = { expanded = true }, onSearchSubmit = { expanded = false }, onSuggestionSelect = { expanded = false }, onExpandedChange = { expanded = it }, onBack = {}, ) } } app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt +20 −1 Original line number Diff line number Diff line Loading @@ -20,19 +20,38 @@ package foundation.e.apps.ui.search.v2 import android.os.Bundle import android.view.View import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.ui.compose.screens.SearchScreen import foundation.e.apps.ui.compose.theme.AppTheme @AndroidEntryPoint class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) { private val searchViewModel: SearchViewModelV2 by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val composeView = view.findViewById<ComposeView>(R.id.composeView) composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) composeView.setContent { AppTheme { } AppTheme { val uiState by searchViewModel.uiState.collectAsStateWithLifecycle() SearchScreen( uiState = uiState, onQueryChange = searchViewModel::onQueryChanged, onBackClick = { requireActivity().onBackPressedDispatcher.onBackPressed() }, onClearQuery = searchViewModel::onQueryCleared, onSubmitSearch = searchViewModel::onSearchSubmitted, onSuggestionSelect = searchViewModel::onSuggestionSelected, onTabSelect = searchViewModel::onTabSelected, ) } } } } app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +1 −1 Original line number Diff line number Diff line Loading @@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject private const val SUGGESTION_DEBOUNCE_MS = 200L private const val SUGGESTION_DEBOUNCE_MS = 500L private const val FAKE_RESULTS_PER_TAB = 6 enum class SearchTabType { Loading Loading
app/src/main/java/foundation/e/apps/ui/compose/components/SearchPlaceholder.kt 0 → 100644 +75 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.ui.compose.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.tooling.preview.Preview import androidx.compose.ui.unit.dp import foundation.e.apps.R import foundation.e.apps.ui.compose.theme.AppTheme @Composable fun SearchPlaceholder(modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxSize(), contentAlignment = Alignment.Center, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), ) { Icon( imageVector = Icons.Outlined.Search, contentDescription = null, tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.45f), modifier = Modifier .padding(bottom = 4.dp) .size(72.dp), ) Text( text = stringResource(id = R.string.search_hint), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.72f), ) } } } @Preview(showBackground = false) @Composable private fun SearchPlaceholderPreview() { AppTheme(darkTheme = true) { SearchPlaceholder() } }
app/src/main/java/foundation/e/apps/ui/compose/screens/SearchScreen.kt 0 → 100644 +158 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.ui.compose.screens import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold 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.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.repeatOnLifecycle import foundation.e.apps.ui.compose.components.SearchResultsContent import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.search.v2.SearchTabType import foundation.e.apps.ui.search.v2.SearchUiState @Composable fun SearchScreen( uiState: SearchUiState, onQueryChange: (String) -> Unit, onBackClick: () -> Unit, onClearQuery: () -> Unit, onSubmitSearch: (String) -> Unit, onSuggestionSelect: (String) -> Unit, onTabSelect: (SearchTabType) -> Unit, modifier: Modifier = Modifier, ) { val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current val lifecycleOwner = LocalLifecycleOwner.current val focusRequester = remember { FocusRequester() } val shouldAutoFocus = !uiState.hasSubmittedSearch var isSearchExpanded by rememberSaveable { mutableStateOf(shouldAutoFocus) } var hasRequestedInitialFocus by rememberSaveable { mutableStateOf(false) } val selectedTab = uiState.selectedTab val showSuggestions = isSearchExpanded && uiState.isSuggestionVisible val showResults = uiState.hasSubmittedSearch && selectedTab != null && uiState.availableTabs.isNotEmpty() LaunchedEffect(lifecycleOwner, shouldAutoFocus) { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { if (shouldAutoFocus && !hasRequestedInitialFocus) { hasRequestedInitialFocus = true withFrameNanos { focusRequester.requestFocus() keyboardController?.show() } } else { // Intentionally no-op; the initial focus request has already happened. } } } LaunchedEffect(uiState.hasSubmittedSearch) { if (uiState.hasSubmittedSearch) { isSearchExpanded = false focusManager.clearFocus() } else { // Intentionally no-op; results are not active so focus remains unchanged. } } Scaffold( modifier = modifier, topBar = { SearchTopBar( uiState = uiState, expanded = isSearchExpanded, showSuggestions = showSuggestions, focusRequester = focusRequester, focusManager = focusManager, onQueryChange = onQueryChange, onClearQuery = onClearQuery, onSearchSubmit = onSubmitSearch, onSuggestionSelect = onSuggestionSelect, onExpandedChange = { expanded -> isSearchExpanded = expanded if (expanded) { focusRequester.requestFocus() } else { focusManager.clearFocus() } }, onBack = onBackClick, ) }, ) { innerPadding -> Column( modifier = Modifier .fillMaxSize() .padding(innerPadding) ) { when { showResults && selectedTab != null -> { SearchResultsContent( tabs = uiState.availableTabs, selectedTab = selectedTab, resultsByTab = uiState.resultsByTab, onTabSelect = onTabSelect, modifier = Modifier.fillMaxSize(), ) } else -> { // Suggestions render in the top bar dropdown; leave body empty. } } } } } @Preview(showBackground = false) @Composable private fun SearchScreenPreview() { AppTheme(darkTheme = true) { SearchScreen( uiState = SearchUiState( query = "telegram", suggestions = listOf("telegram", "telegram messenger") ), onQueryChange = {}, onBackClick = {}, onClearQuery = {}, onSubmitSearch = {}, onSuggestionSelect = {}, onTabSelect = {}, ) } }
app/src/main/java/foundation/e/apps/ui/compose/screens/SearchTopBar.kt 0 → 100644 +216 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.ui.compose.screens import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable 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.focus.FocusManager import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import foundation.e.apps.R import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.search.v2.SearchTabType import foundation.e.apps.ui.search.v2.SearchUiState @OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchTopBar( uiState: SearchUiState, expanded: Boolean, showSuggestions: Boolean, focusRequester: FocusRequester, focusManager: FocusManager, onQueryChange: (String) -> Unit, onClearQuery: () -> Unit, onSearchSubmit: (String) -> Unit, onSuggestionSelect: (String) -> Unit, onExpandedChange: (Boolean) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, ) { SearchBar( modifier = modifier.fillMaxWidth(), colors = SearchBarDefaults.colors( containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), dividerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), ), expanded = expanded, onExpandedChange = { isExpanded -> onExpandedChange(isExpanded) }, inputField = { SearchBarDefaults.InputField( modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester), query = uiState.query, onQueryChange = { query -> onExpandedChange(true) onQueryChange(query) }, onSearch = { query -> onExpandedChange(false) focusManager.clearFocus() onSearchSubmit(query) }, expanded = expanded, onExpandedChange = { isExpanded -> onExpandedChange(isExpanded) }, placeholder = { Text(text = stringResource(id = R.string.search_hint)) }, leadingIcon = { IconButton(onClick = { focusManager.clearFocus() onBack() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, ) } }, trailingIcon = { if (uiState.query.isNotEmpty()) { IconButton(onClick = { onClearQuery() onExpandedChange(true) focusRequester.requestFocus() }) { Icon( imageVector = Icons.Filled.Close, contentDescription = null, ) } } }, ) }, ) { if (showSuggestions) { SuggestionList( suggestions = uiState.suggestions, onSuggestionSelect = { suggestion -> onExpandedChange(false) focusManager.clearFocus() onSuggestionSelect(suggestion) }, modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)), ) } } } @Composable private fun SuggestionList( suggestions: List<String>, onSuggestionSelect: (String) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( modifier = modifier, ) { items( items = suggestions, key = { suggestion -> suggestion }, ) { suggestion -> ListItem( headlineContent = { Text(text = suggestion) }, leadingContent = { Icon( imageVector = Icons.Filled.Search, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, ) }, modifier = Modifier .fillMaxWidth() .clickable { onSuggestionSelect(suggestion) }, ) } } } @Preview(showBackground = true) @Composable private fun SearchTopBarPreview() { AppTheme(darkTheme = true) { val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current var expanded by remember { mutableStateOf(true) } val sampleState = SearchUiState( query = "browser", suggestions = listOf( "browser", "browser apps", "browser downloader", "browser for android", ), isSuggestionVisible = true, availableTabs = listOf( SearchTabType.COMMON_APPS, SearchTabType.OPEN_SOURCE, SearchTabType.WEB_APPS, ), selectedTab = SearchTabType.OPEN_SOURCE, hasSubmittedSearch = false, ) SearchTopBar( uiState = sampleState, expanded = expanded, showSuggestions = expanded && sampleState.suggestions.isNotEmpty(), focusRequester = focusRequester, focusManager = focusManager, onQueryChange = { expanded = true }, onClearQuery = { expanded = true }, onSearchSubmit = { expanded = false }, onSuggestionSelect = { expanded = false }, onExpandedChange = { expanded = it }, onBack = {}, ) } }
app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt +20 −1 Original line number Diff line number Diff line Loading @@ -20,19 +20,38 @@ package foundation.e.apps.ui.search.v2 import android.os.Bundle import android.view.View import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.ui.compose.screens.SearchScreen import foundation.e.apps.ui.compose.theme.AppTheme @AndroidEntryPoint class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) { private val searchViewModel: SearchViewModelV2 by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val composeView = view.findViewById<ComposeView>(R.id.composeView) composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) composeView.setContent { AppTheme { } AppTheme { val uiState by searchViewModel.uiState.collectAsStateWithLifecycle() SearchScreen( uiState = uiState, onQueryChange = searchViewModel::onQueryChanged, onBackClick = { requireActivity().onBackPressedDispatcher.onBackPressed() }, onClearQuery = searchViewModel::onQueryCleared, onSubmitSearch = searchViewModel::onSearchSubmitted, onSuggestionSelect = searchViewModel::onSuggestionSelected, onTabSelect = searchViewModel::onTabSelected, ) } } } }
app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +1 −1 Original line number Diff line number Diff line Loading @@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject private const val SUGGESTION_DEBOUNCE_MS = 200L private const val SUGGESTION_DEBOUNCE_MS = 500L private const val FAKE_RESULTS_PER_TAB = 6 enum class SearchTabType { Loading