From 3212c174ea8a6e20986c382c66912f997c1b1f23 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Wed, 1 Apr 2026 16:34:11 +0200 Subject: [PATCH] Mock search result functionality --- .../ui/composable/SearchResultScreen.kt | 250 ++++++++++++++++++ .../aux/compose/ui/composable/SearchScreen.kt | 107 ++++++-- .../ui/composable/navigation/AuxNavHost.kt | 26 +- .../navigation/routes/SearchResultRoute.kt | 8 + 4 files changed, 367 insertions(+), 24 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchResultScreen.kt create mode 100755 composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/SearchResultRoute.kt diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchResultScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchResultScreen.kt new file mode 100644 index 00000000..3c157d48 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchResultScreen.kt @@ -0,0 +1,250 @@ +package ac.aux.compose.ui.composable + +import ac.aux.compose.repository.NostrRepository +import ac.aux.compose.repository.SearchRepository +import ac.aux.compose.ui.theme.AuxTheme +import ac.aux.compose.ui.view.model.FeedListViewModel +import ac.aux.compose.ui.view.model.SearchViewModel +import ac.aux.compose.ui.view.state.SearchUIState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.filled.DocumentScanner +import androidx.compose.material.icons.filled.ManageSearch +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.SettingsInputComponent +import androidx.compose.material.icons.filled.Tag +import androidx.compose.material.icons.filled.TravelExplore +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.filled.YoutubeSearchedFor +import androidx.compose.material3.AppBarWithSearch +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExpandedFullScreenSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopSearchBar +import androidx.compose.material3.rememberSearchBarState +import androidx.compose.runtime.Composable +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchResultScreen( + searchQuery: String, + nostrRepository: NostrRepository, + searchRepository: SearchRepository, + onNavigateBack: () -> Unit, +) { + val searchViewModel: SearchViewModel = viewModel( + factory = SearchViewModel.factory( + initialSearchUIState = SearchUIState.Prompt, + nostrRepository = nostrRepository, + searchRepository = searchRepository + ), + ) + + val searchResults = listOf( + "Apple", + "Banna", + "Carrot", + "Durian", + "Eggplant", + "Fig", + "Grape", + "Honeydew", + "Jackfruit", + "Kiwi", + "Lemon", + "Mango", + "Nectarine", + "Orange", + "Papaya", + "Quince", + "Raspberry", + "Tangerine", + "Ugli Fruit", + "Vanilla Bean", + "Watermelon", + "Xigua", + "Yellow Passion Fruit", + "Zinfandel", + ) + + val textFieldState = rememberTextFieldState(searchQuery) + // Controls expansion state of the search bar + var expanded by rememberSaveable { mutableStateOf(true) } + + + Scaffold { innerPadding -> + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Box( + modifier = Modifier + .fillMaxSize() + .semantics { isTraversalGroup = true } + ) { + + SearchBar( + modifier = Modifier + .align(Alignment.TopCenter) + .semantics { traversalIndex = 0f }, + inputField = { + SearchBarDefaults.InputField( + query = textFieldState.text.toString(), + onQueryChange = { textFieldState.edit { replace(0, length, it) } }, + onSearch = { + searchViewModel.performSearch( + textFieldState.text.toString() + ) + expanded = false + }, + enabled = false, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text("Search") }, + leadingIcon = { + if (expanded) { + IconButton( + onClick = { + // TODO: onNavigateBack + onNavigateBack.invoke() + } + ) { + Icon( + Icons.Default.ArrowBackIosNew, + contentDescription = "Back" + ) + } + + } else { + Icon( + Icons.Default.Search, + contentDescription = "Search" + ) + } + }, + ) + }, + expanded = expanded, + onExpandedChange = { + onNavigateBack.invoke() + }, + windowInsets = TopAppBarDefaults.windowInsets + ) { + // TODO: Tabs + LazyColumn { + itemsIndexed(searchResults) { index, item -> + ListItem( + headlineContent = { Text(item) }, + supportingContent = { + if (index == 0) { + Text( + text = "Search" + ) + } else if (index == 1) { + Text( + text = "Search hashtags" + ) + } else { + Text("Profile") + } + }, + leadingContent = { + if (index == 0) { + Icon( + Icons.Default.TravelExplore, + contentDescription = "Search globally" + ) + } else if (index == 1) { + Icon( + Icons.Default.Tag, + contentDescription = "Search hashtags" + ) + } else { + Icon( + Icons.Default.Person, + contentDescription = "Person" + ) + } + }, + modifier = Modifier + .clickable { + + } + .fillMaxWidth(), + + ) + } + } + } + } + } + } +} + +@Preview +@Composable +private fun SearchResultScreenPreview() { + AuxTheme { + Surface( + modifier = Modifier.padding(20.dp) + ) { + SearchResultScreen( + searchQuery = "What happened...", + nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, + searchRepository = SearchRepository.NO_OP_SEARCH_REPOSITORY, + onNavigateBack = {} + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchScreen.kt index 882a1e29..6d059288 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SearchScreen.kt @@ -2,6 +2,9 @@ package ac.aux.compose.ui.composable import ac.aux.compose.repository.NostrRepository import ac.aux.compose.repository.SearchRepository +import ac.aux.compose.ui.composable.navigation.routes.ImplementationPendingRoute +import ac.aux.compose.ui.composable.navigation.routes.Route +import ac.aux.compose.ui.composable.navigation.routes.SearchResultRoute import ac.aux.compose.ui.theme.AuxTheme import ac.aux.compose.ui.view.model.FeedListViewModel import ac.aux.compose.ui.view.model.SearchViewModel @@ -31,6 +34,7 @@ import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Tag import androidx.compose.material.icons.filled.TravelExplore +import androidx.compose.material.icons.filled.Tune import androidx.compose.material.icons.filled.YoutubeSearchedFor import androidx.compose.material3.AppBarWithSearch import androidx.compose.material3.Button @@ -75,7 +79,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel fun SearchScreen( initialSearchUIState: SearchUIState, nostrRepository: NostrRepository, - searchRepository: SearchRepository + searchRepository: SearchRepository, + onNavigateToSearchResult: (Route) -> Unit, + onNavigateToProfile: (Route) -> Unit ) { val searchViewModel: SearchViewModel = viewModel( factory = SearchViewModel.factory( @@ -173,7 +179,6 @@ fun SearchScreen( // Controls expansion state of the search bar var expanded by rememberSaveable { mutableStateOf(false) } - Scaffold { innerPadding -> Column( modifier = Modifier.fillMaxSize(), @@ -199,7 +204,11 @@ fun SearchScreen( searchViewModel.performSearch( textFieldState.text.toString() ) - expanded = false + + if (textFieldState.text.isBlank()) { + expanded = false + } + }, expanded = expanded, onExpandedChange = { expanded = it }, @@ -223,6 +232,18 @@ fun SearchScreen( contentDescription = "Search" ) } + }, + trailingIcon = { + IconButton( + onClick = { + // TODO: Adjust search + } + ) { + Icon( + Icons.Default.Tune, + contentDescription = "Adjust Search" + ) + } } ) }, @@ -251,26 +272,59 @@ fun SearchScreen( }, leadingContent = { if (index == 0) { - Icon( - Icons.Default.TravelExplore, - contentDescription = "Search globally" - ) + IconButton( + onClick = { + onNavigateToSearchResult.invoke( + SearchResultRoute( + textFieldState.text.toString() + ) + ) + } + ) { + Icon( + Icons.Default.TravelExplore, + contentDescription = "Search globally" + ) + } + } else if (index == 1) { - Icon( - Icons.Default.Tag, - contentDescription = "Search hashtags" - ) + IconButton( + onClick = { + onNavigateToSearchResult.invoke( + SearchResultRoute( + query = "#${textFieldState.text.toString().replace("#","")}" + ) + ) + } + ) { + Icon( + Icons.Default.Tag, + contentDescription = "Search hashtags" + ) + } + } else { - Icon( - Icons.Default.Person, - contentDescription = "Person" - ) + IconButton( + onClick = { + onNavigateToProfile.invoke( + ImplementationPendingRoute("Profile result") + ) + } + ) { + Icon( + Icons.Default.Person, + contentDescription = "Person" + ) + } } }, modifier = Modifier .clickable { - textFieldState.edit { replace(0, length, item) } - expanded = false + onNavigateToSearchResult.invoke( + SearchResultRoute( + item + ) + ) } .fillMaxWidth(), @@ -304,6 +358,9 @@ fun SearchScreen( IconButton( onClick = { // TODO: Open profile... + onNavigateToProfile.invoke( + ImplementationPendingRoute("Profile search result") + ) } ) { Icon( @@ -325,8 +382,7 @@ fun SearchScreen( item { Column( modifier = Modifier.padding(10.dp), - - ) { + ) { Text( text = "Recents", style = MaterialTheme.typography.titleMedium @@ -342,13 +398,16 @@ fun SearchScreen( leadingContent = { Icon( Icons.Default.YoutubeSearchedFor, - contentDescription = "Person" + contentDescription = "Recent search" ) }, modifier = Modifier .clickable { - textFieldState.edit { replace(0, length, item) } - expanded = false + onNavigateToSearchResult.invoke( + SearchResultRoute( + query = item + ) + ) } .fillMaxWidth(), ) @@ -387,7 +446,9 @@ private fun SearchScreenPreview() { SearchScreen( initialSearchUIState = SearchUIState.Prompt, nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, - searchRepository = SearchRepository.NO_OP_SEARCH_REPOSITORY + searchRepository = SearchRepository.NO_OP_SEARCH_REPOSITORY, + onNavigateToSearchResult = {}, + onNavigateToProfile = {} ) } } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt index dda8a51b..bf7afeb6 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt @@ -10,6 +10,7 @@ import ac.aux.compose.ui.composable.ImplementationPendingScreen import ac.aux.compose.ui.composable.LandingScreen import ac.aux.compose.ui.composable.LoadingScreen import ac.aux.compose.ui.composable.NostrEventDetailScreen +import ac.aux.compose.ui.composable.SearchResultScreen import ac.aux.compose.ui.composable.SearchScreen import ac.aux.compose.ui.composable.SignInToProfileScreen import ac.aux.compose.ui.composable.SocialPreconditionScreen @@ -27,6 +28,7 @@ import ac.aux.compose.ui.composable.navigation.routes.ImplementationPendingRoute import ac.aux.compose.ui.composable.navigation.routes.LandingRoute import ac.aux.compose.ui.composable.navigation.routes.LoadingRoute import ac.aux.compose.ui.composable.navigation.routes.NostrEventDetailRoute +import ac.aux.compose.ui.composable.navigation.routes.SearchResultRoute import ac.aux.compose.ui.composable.navigation.routes.SearchRoute import ac.aux.compose.ui.composable.navigation.routes.SignInRoute import androidx.compose.runtime.Composable @@ -333,7 +335,29 @@ fun AuxNavHost( SearchScreen( initialSearchUIState = SearchUIState.Prompt, nostrRepository = nostrRepository, - searchRepository = searchRepository + searchRepository = searchRepository, + onNavigateToProfile = { route -> + navController.navigate( + route = route + ) + }, + onNavigateToSearchResult = { route -> + navController.navigate( + route = route + ) + } + ) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + + SearchResultScreen( + searchQuery = route.query, + nostrRepository = nostrRepository, + searchRepository = searchRepository, + onNavigateBack = { + navController.popBackStack() + } ) } composable { backStackEntry -> diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/SearchResultRoute.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/SearchResultRoute.kt new file mode 100755 index 00000000..eb75f3aa --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/SearchResultRoute.kt @@ -0,0 +1,8 @@ +package ac.aux.compose.ui.composable.navigation.routes + +import kotlinx.serialization.Serializable + +@Serializable +data class SearchResultRoute( + val query: String +): Route() \ No newline at end of file