Mock search result functionality

This commit is contained in:
Kgothatso Ngako
2026-04-01 16:34:11 +02:00
parent bc69954c93
commit 3212c174ea
4 changed files with 367 additions and 24 deletions

View File

@@ -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 = {}
)
}
}
}

View File

@@ -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 = {}
)
}
}

View File

@@ -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<SearchResultRoute> { backStackEntry ->
val route = backStackEntry.toRoute<SearchResultRoute>()
SearchResultScreen(
searchQuery = route.query,
nostrRepository = nostrRepository,
searchRepository = searchRepository,
onNavigateBack = {
navController.popBackStack()
}
)
}
composable<NostrEventDetailRoute> { backStackEntry ->

View File

@@ -0,0 +1,8 @@
package ac.aux.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable
@Serializable
data class SearchResultRoute(
val query: String
): Route()