Loading app/src/main/java/foundation/e/apps/di/SearchSuggestionModule.kt 0 → 100644 +35 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.di import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.ui.search.v2.PlayStoreSuggestionSource import foundation.e.apps.ui.search.v2.SuggestionSource import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) abstract class SearchSuggestionModule { @Binds @Singleton abstract fun bindSuggestionSource(impl: PlayStoreSuggestionSource): SuggestionSource } app/src/main/java/foundation/e/apps/ui/compose/components/SearchInitialState.kt→app/src/main/java/foundation/e/apps/ui/compose/components/SearchEmptyState.kt +3 −3 Original line number Diff line number Diff line Loading @@ -39,7 +39,7 @@ import foundation.e.apps.R import foundation.e.apps.ui.compose.theme.AppTheme @Composable fun SearchInitialState(modifier: Modifier = Modifier) { fun SearchEmptyState(modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxSize(), Loading Loading @@ -68,8 +68,8 @@ fun SearchInitialState(modifier: Modifier = Modifier) { @Preview(showBackground = false) @Composable private fun SearchInitialStatePreview() { private fun SearchEmptyStatePreview() { AppTheme(darkTheme = true) { SearchInitialState() SearchEmptyState() } } app/src/main/java/foundation/e/apps/ui/compose/components/SearchSuggestionsDropdown.kt 0 → 100644 +92 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import foundation.e.apps.ui.compose.theme.AppTheme @Composable fun SearchSuggestionsDropdown( suggestions: List<String>, onSuggestionClick: (String) -> Unit, modifier: Modifier = Modifier, ) { if (suggestions.isEmpty()) { return } Card( modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface, ), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), ) { Column { suggestions.forEachIndexed { index, suggestion -> SuggestionRow( text = suggestion, isLast = index == suggestions.lastIndex, onClick = { onSuggestionClick(suggestion) }, ) } } } } @Composable private fun SuggestionRow( text: String, isLast: Boolean, onClick: () -> Unit, ) { val verticalPadding = if (isLast) 14.dp else 12.dp Text( text = text, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier .fillMaxWidth() .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = verticalPadding), ) } @Preview(showBackground = true) @Composable private fun SearchSuggestionsDropdownPreview() { AppTheme(darkTheme = true) { SearchSuggestionsDropdown( suggestions = listOf("Telegram", "Telegram FOSS", "Telegram X"), onSuggestionClick = {}, modifier = Modifier.padding(16.dp), ) } } app/src/main/java/foundation/e/apps/ui/compose/components/SearchTopBar.kt +6 −2 Original line number Diff line number Diff line Loading @@ -101,7 +101,11 @@ fun SearchTopBar( }, trailingIcon = { if (query.isNotEmpty()) { IconButton(onClick = onClearQuery) { IconButton(onClick = { onClearQuery() focusRequester.requestFocus() keyboardController?.show() }) { Icon( imageVector = Icons.Filled.Cancel, contentDescription = stringResource(id = R.string.clear), Loading Loading @@ -153,7 +157,7 @@ fun SearchTopBar( } } @Preview(showBackground = false) @Preview(showBackground = true) @Composable private fun SearchTopBarPreview() { AppTheme(darkTheme = false) { Loading app/src/main/java/foundation/e/apps/ui/compose/screens/SearchScreen.kt +26 −6 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ package foundation.e.apps.ui.compose.screens import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable Loading @@ -29,17 +30,20 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import foundation.e.apps.ui.compose.components.SearchInitialState import foundation.e.apps.ui.compose.components.SearchEmptyState import foundation.e.apps.ui.compose.components.SearchSuggestionsDropdown import foundation.e.apps.ui.compose.components.SearchTopBar import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.search.v2.SearchUiState @Composable fun SearchScreen( query: String, state: SearchUiState, onQueryChange: (String) -> Unit, onBackClick: () -> Unit, onClearQuery: () -> Unit, onSubmitSearch: (String) -> Unit, onSuggestionSelected: (String) -> Unit, ) { val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } Loading @@ -47,7 +51,7 @@ fun SearchScreen( Scaffold( topBar = { SearchTopBar( query = query, query = state.query, onQueryChange = onQueryChange, onBackClick = onBackClick, onClearQuery = { Loading @@ -65,9 +69,20 @@ fun SearchScreen( .padding(innerPadding) .padding(horizontal = 16.dp), ) { SearchInitialState( if (state.isSuggestionVisible) { SearchSuggestionsDropdown( suggestions = state.suggestions, onSuggestionClick = { onSuggestionSelected(it) focusManager.clearFocus(force = false) }, modifier = Modifier.padding(top = 8.dp), ) } SearchEmptyState( modifier = Modifier .fillMaxSize(), .weight(1f) .fillMaxWidth(), ) } } Loading @@ -78,11 +93,16 @@ fun SearchScreen( private fun SearchScreenPreview() { AppTheme(darkTheme = true) { SearchScreen( query = "", state = SearchUiState( query = "t", suggestions = listOf("Telegram", "Telegram FOSS", "Telegram X"), isSuggestionVisible = true, ), onQueryChange = {}, onBackClick = {}, onClearQuery = {}, onSubmitSearch = {}, onSuggestionSelected = {}, ) } } Loading
app/src/main/java/foundation/e/apps/di/SearchSuggestionModule.kt 0 → 100644 +35 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.di import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.ui.search.v2.PlayStoreSuggestionSource import foundation.e.apps.ui.search.v2.SuggestionSource import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) abstract class SearchSuggestionModule { @Binds @Singleton abstract fun bindSuggestionSource(impl: PlayStoreSuggestionSource): SuggestionSource }
app/src/main/java/foundation/e/apps/ui/compose/components/SearchInitialState.kt→app/src/main/java/foundation/e/apps/ui/compose/components/SearchEmptyState.kt +3 −3 Original line number Diff line number Diff line Loading @@ -39,7 +39,7 @@ import foundation.e.apps.R import foundation.e.apps.ui.compose.theme.AppTheme @Composable fun SearchInitialState(modifier: Modifier = Modifier) { fun SearchEmptyState(modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxSize(), Loading Loading @@ -68,8 +68,8 @@ fun SearchInitialState(modifier: Modifier = Modifier) { @Preview(showBackground = false) @Composable private fun SearchInitialStatePreview() { private fun SearchEmptyStatePreview() { AppTheme(darkTheme = true) { SearchInitialState() SearchEmptyState() } }
app/src/main/java/foundation/e/apps/ui/compose/components/SearchSuggestionsDropdown.kt 0 → 100644 +92 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import foundation.e.apps.ui.compose.theme.AppTheme @Composable fun SearchSuggestionsDropdown( suggestions: List<String>, onSuggestionClick: (String) -> Unit, modifier: Modifier = Modifier, ) { if (suggestions.isEmpty()) { return } Card( modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface, ), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), ) { Column { suggestions.forEachIndexed { index, suggestion -> SuggestionRow( text = suggestion, isLast = index == suggestions.lastIndex, onClick = { onSuggestionClick(suggestion) }, ) } } } } @Composable private fun SuggestionRow( text: String, isLast: Boolean, onClick: () -> Unit, ) { val verticalPadding = if (isLast) 14.dp else 12.dp Text( text = text, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier .fillMaxWidth() .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = verticalPadding), ) } @Preview(showBackground = true) @Composable private fun SearchSuggestionsDropdownPreview() { AppTheme(darkTheme = true) { SearchSuggestionsDropdown( suggestions = listOf("Telegram", "Telegram FOSS", "Telegram X"), onSuggestionClick = {}, modifier = Modifier.padding(16.dp), ) } }
app/src/main/java/foundation/e/apps/ui/compose/components/SearchTopBar.kt +6 −2 Original line number Diff line number Diff line Loading @@ -101,7 +101,11 @@ fun SearchTopBar( }, trailingIcon = { if (query.isNotEmpty()) { IconButton(onClick = onClearQuery) { IconButton(onClick = { onClearQuery() focusRequester.requestFocus() keyboardController?.show() }) { Icon( imageVector = Icons.Filled.Cancel, contentDescription = stringResource(id = R.string.clear), Loading Loading @@ -153,7 +157,7 @@ fun SearchTopBar( } } @Preview(showBackground = false) @Preview(showBackground = true) @Composable private fun SearchTopBarPreview() { AppTheme(darkTheme = false) { Loading
app/src/main/java/foundation/e/apps/ui/compose/screens/SearchScreen.kt +26 −6 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ package foundation.e.apps.ui.compose.screens import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable Loading @@ -29,17 +30,20 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import foundation.e.apps.ui.compose.components.SearchInitialState import foundation.e.apps.ui.compose.components.SearchEmptyState import foundation.e.apps.ui.compose.components.SearchSuggestionsDropdown import foundation.e.apps.ui.compose.components.SearchTopBar import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.search.v2.SearchUiState @Composable fun SearchScreen( query: String, state: SearchUiState, onQueryChange: (String) -> Unit, onBackClick: () -> Unit, onClearQuery: () -> Unit, onSubmitSearch: (String) -> Unit, onSuggestionSelected: (String) -> Unit, ) { val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } Loading @@ -47,7 +51,7 @@ fun SearchScreen( Scaffold( topBar = { SearchTopBar( query = query, query = state.query, onQueryChange = onQueryChange, onBackClick = onBackClick, onClearQuery = { Loading @@ -65,9 +69,20 @@ fun SearchScreen( .padding(innerPadding) .padding(horizontal = 16.dp), ) { SearchInitialState( if (state.isSuggestionVisible) { SearchSuggestionsDropdown( suggestions = state.suggestions, onSuggestionClick = { onSuggestionSelected(it) focusManager.clearFocus(force = false) }, modifier = Modifier.padding(top = 8.dp), ) } SearchEmptyState( modifier = Modifier .fillMaxSize(), .weight(1f) .fillMaxWidth(), ) } } Loading @@ -78,11 +93,16 @@ fun SearchScreen( private fun SearchScreenPreview() { AppTheme(darkTheme = true) { SearchScreen( query = "", state = SearchUiState( query = "t", suggestions = listOf("Telegram", "Telegram FOSS", "Telegram X"), isSuggestionVisible = true, ), onQueryChange = {}, onBackClick = {}, onClearQuery = {}, onSubmitSearch = {}, onSuggestionSelected = {}, ) } }