Going through tabs...

This commit is contained in:
Kgothatso Ngako
2026-04-01 22:39:11 +02:00
parent 2901b9209d
commit 4c81d6985b
4 changed files with 284 additions and 168 deletions

View File

@@ -3,77 +3,54 @@ 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 ac.aux.compose.ui.view.model.SearchResultType
import ac.aux.compose.ui.view.model.SearchResultViewModel
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.height
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.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
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.mutableIntStateOf
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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SearchResultScreen(
searchQuery: String,
@@ -81,9 +58,9 @@ fun SearchResultScreen(
searchRepository: SearchRepository,
onNavigateBack: () -> Unit,
) {
val searchViewModel: SearchViewModel = viewModel(
factory = SearchViewModel.factory(
initialSearchUIState = SearchUIState.Prompt,
val searchViewModel: SearchResultViewModel = viewModel(
factory = SearchResultViewModel.factory(
initialSearchResultType = SearchResultType.Posts,
nostrRepository = nostrRepository,
searchRepository = searchRepository
),
@@ -146,7 +123,6 @@ fun SearchResultScreen(
searchViewModel.performSearch(
textFieldState.text.toString()
)
expanded = false
},
enabled = false,
expanded = expanded,
@@ -155,6 +131,10 @@ fun SearchResultScreen(
leadingIcon = {
if (expanded) {
IconButton(
enabled = true,
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.primary
),
onClick = {
// TODO: onNavigateBack
onNavigateBack.invoke()
@@ -175,6 +155,9 @@ fun SearchResultScreen(
},
trailingIcon = {
IconButton(
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.primary
),
onClick = {
// TODO: Adjust search
}
@@ -193,49 +176,39 @@ fun SearchResultScreen(
},
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")
}
PrimaryScrollableTabRow(
modifier = Modifier,
edgePadding = 10.dp,
selectedTabIndex = searchViewModel.searchResultType.ordinal,
) {
SearchResultType.entries.forEach { searchResultType ->
Tab(
selected = searchViewModel.searchResultType == searchResultType,
onClick = {
searchViewModel.updateSearchResultType(searchResultType)
},
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 {
text = {
Text(
text = searchResultType.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
}
.fillMaxWidth(),
LazyColumn(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
item {
Spacer(
modifier = Modifier.height(50.dp)
)
)
LoadingIndicator()
}
}
}

View File

@@ -29,6 +29,7 @@ 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.Close
import androidx.compose.material.icons.filled.ManageSearch
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
@@ -59,6 +60,7 @@ import androidx.compose.material3.TopSearchBar
import androidx.compose.material3.rememberSearchBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -118,35 +120,38 @@ fun SearchScreen(
"Zinfandel",
)
val recentSearches = listOf(
"Alligator",
"Baboon",
"Camel",
"Deer",
"Earthworm",
"Falcon",
"Gazelle",
"Hamster",
"Iguana",
"Jellyfish",
"Koala",
"Leopard",
"Meerkat",
"Nilgai",
"Octopus",
"Panda",
"Quokka",
"Panda",
"Rabbit",
"Scorpion",
"Tarantula",
"Uakari",
"Viper",
"Walrus",
"Xenops",
"Yak",
"Zebra",
)
val recentSearches = remember {
mutableStateListOf(
"Alligator",
"Baboon",
"Camel",
"Deer",
"Earthworm",
"Falcon",
"Gazelle",
"Hamster",
"Iguana",
"Jellyfish",
"Koala",
"Leopard",
"Meerkat",
"Nilgai",
"Octopus",
"Panda",
"Quokka",
"Panda",
"Rabbit",
"Scorpion",
"Tarantula",
"Uakari",
"Viper",
"Walrus",
"Xenops",
"Yak",
"Zebra",
)
}
val profileSearches = listOf(
"Afghanistan",
@@ -177,7 +182,7 @@ fun SearchScreen(
)
val textFieldState = rememberTextFieldState()
// Controls expansion state of the search bar
var expanded by rememberSaveable { mutableStateOf(false) }
var expanded by rememberSaveable { mutableStateOf(true) }
Scaffold { innerPadding ->
Column(
@@ -242,68 +247,101 @@ fun SearchScreen(
if (textFieldState.text.isNotBlank()) {
// Display search results in a scrollable column
LazyColumn {
item {
ListItem(
headlineContent = {
Text(
textFieldState.text.toString()
)
},
supportingContent = {
Text(
text = "Search"
)
},
leadingContent = {
IconButton(
onClick = {
onNavigateToSearchResult.invoke(
SearchResultRoute(
textFieldState.text.toString()
)
)
}
) {
Icon(
Icons.Default.TravelExplore,
contentDescription = "Search globally"
)
}
},
modifier = Modifier
.clickable {
onNavigateToSearchResult.invoke(
SearchResultRoute(
textFieldState.text.toString()
)
)
}
.fillMaxWidth(),
)
}
item {
ListItem(
headlineContent = { Text(textFieldState.text.toString()) },
supportingContent = {
Text(
text = "Search hashtags"
)
},
leadingContent = {
IconButton(
onClick = {
onNavigateToSearchResult.invoke(
SearchResultRoute(
query = "#${textFieldState.text.toString().replace("#","")}"
)
)
}
) {
Icon(
Icons.Default.Tag,
contentDescription = "Search hashtags"
)
}
},
modifier = Modifier
.clickable {
onNavigateToSearchResult.invoke(
SearchResultRoute(
"#${textFieldState.text.toString().replaceFirst("#","")}"
)
)
}
.fillMaxWidth(),
)
}
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")
}
Text("Profile")
},
leadingContent = {
if (index == 0) {
IconButton(
onClick = {
onNavigateToSearchResult.invoke(
SearchResultRoute(
textFieldState.text.toString()
)
)
}
) {
Icon(
Icons.Default.TravelExplore,
contentDescription = "Search globally"
)
}
} else if (index == 1) {
IconButton(
onClick = {
onNavigateToSearchResult.invoke(
SearchResultRoute(
query = "#${textFieldState.text.toString().replace("#","")}"
)
)
}
) {
Icon(
Icons.Default.Tag,
contentDescription = "Search hashtags"
)
}
} else {
IconButton(
onClick = {
onNavigateToProfile.invoke(
ImplementationPendingRoute("Profile result")
)
}
) {
Icon(
Icons.Default.Person,
contentDescription = "Person"
IconButton(
onClick = {
onNavigateToProfile.invoke(
ImplementationPendingRoute("Profile result")
)
}
) {
Icon(
Icons.Default.Person,
contentDescription = "Person"
)
}
},
modifier = Modifier
@@ -316,7 +354,7 @@ fun SearchScreen(
}
.fillMaxWidth(),
)
)
}
}
} else {
@@ -398,6 +436,18 @@ fun SearchScreen(
)
}
.fillMaxWidth(),
trailingContent = {
IconButton(
onClick = {
recentSearches.removeAt(index)
}
) {
Icon(
Icons.Default.Close,
contentDescription = "Remove search term"
)
}
}
)
}
}

View File

@@ -34,6 +34,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
@@ -46,6 +47,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.input.nestedscroll.nestedScroll
@@ -169,22 +171,23 @@ fun MetadataEventDetail(
}
item {
val destinations = arrayOf("Notes", "Follows", "Following")
val navController = rememberNavController()
val destinations = arrayOf("Notes", "Replies", "Articles", "Follows", "Following", "Photos", "Videos")
val startDestination = 0
val selectedDestination by rememberSaveable {
var selectedDestination by rememberSaveable {
mutableIntStateOf(startDestination)
}
PrimaryTabRow(
PrimaryScrollableTabRow(
modifier = Modifier.padding(10.dp),
edgePadding = 10.dp,
selectedTabIndex = selectedDestination,
) {
destinations.forEachIndexed { index, destination ->
Tab(
selected = selectedDestination == index,
onClick = {
selectedDestination = index
},
text = {
Text(

View File

@@ -0,0 +1,90 @@
package ac.aux.compose.ui.view.model
import ac.aux.compose.database.model.SynchronizeNostrEventRequest
import ac.aux.compose.database.model.types.SynchronizationFilter
import ac.aux.compose.nostr.Relays
import ac.aux.compose.repository.NostrRepository
import ac.aux.compose.repository.SearchRepository
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
enum class SearchResultType {
Posts, Articles, Profiles, Photos, Videos
}
class SearchResultViewModel(
initialSearchResultType: SearchResultType,
val nostrRepository: NostrRepository,
val searchRepository: SearchRepository,
): ViewModel() {
val logger = Logger.withTag(TAG)
var searchResultType: SearchResultType by mutableStateOf(initialSearchResultType)
private set
fun updateSearchResultType (searchResultType: SearchResultType) {
this.searchResultType = searchResultType
// TODO: Reload search results...
}
fun performSearch(searchQuery: String) {
viewModelScope.launch(Dispatchers.IO) {
// Sync Notifications... might want to also run this in the background
searchRepository.submitSearch(
Relays.eventPublishRelaySet.map { normalizedRelay ->
SynchronizeNostrEventRequest(
purpose = "search",
synchronizationFilters = arrayOf(
SynchronizationFilter(
kinds = arrayOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
ReactionEvent.KIND,
LnZapEvent.KIND,
),
search = "shoes",
limit = 50
)
),
relayURL = normalizedRelay.url
)
}
)
}
}
companion object {
const val TAG = "SearchViewModel"
fun factory(
initialSearchResultType: SearchResultType = SearchResultType.Posts,
nostrRepository: NostrRepository,
searchRepository: SearchRepository,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
SearchResultViewModel(
initialSearchResultType = initialSearchResultType,
nostrRepository = nostrRepository,
searchRepository = searchRepository
)
}
}
}
}