mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-15 05:17:03 +05:30
feat(android): wire up search UI into navigation graph
This commit is contained in:
parent
8d6aba9865
commit
d3c3e3ca08
3 changed files with 79 additions and 29 deletions
|
@ -18,9 +18,11 @@ import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
import androidx.compose.material.icons.filled.ImportExport
|
import androidx.compose.material.icons.filled.ImportExport
|
||||||
import androidx.compose.material.icons.filled.NewReleases
|
import androidx.compose.material.icons.filled.NewReleases
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Whatshot
|
import androidx.compose.material.icons.filled.Whatshot
|
||||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||||
import androidx.compose.material.icons.outlined.NewReleases
|
import androidx.compose.material.icons.outlined.NewReleases
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
import androidx.compose.material.icons.outlined.Whatshot
|
import androidx.compose.material.icons.outlined.Whatshot
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
@ -59,6 +61,7 @@ import dev.msfjarvis.claw.android.ui.decorations.NavigationItem
|
||||||
import dev.msfjarvis.claw.android.ui.decorations.TransparentSystemBars
|
import dev.msfjarvis.claw.android.ui.decorations.TransparentSystemBars
|
||||||
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
import dev.msfjarvis.claw.android.ui.lists.DatabasePosts
|
||||||
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
import dev.msfjarvis.claw.android.ui.lists.NetworkPosts
|
||||||
|
import dev.msfjarvis.claw.android.ui.lists.SearchList
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
import dev.msfjarvis.claw.android.ui.navigation.ClawNavigationType
|
||||||
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
import dev.msfjarvis.claw.android.ui.navigation.Destinations
|
||||||
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
|
||||||
|
@ -86,6 +89,7 @@ fun LobstersApp(
|
||||||
val hottestListState = rememberLazyListState()
|
val hottestListState = rememberLazyListState()
|
||||||
val newestListState = rememberLazyListState()
|
val newestListState = rememberLazyListState()
|
||||||
val savedListState = rememberLazyListState()
|
val savedListState = rememberLazyListState()
|
||||||
|
val searchListState = rememberLazyListState()
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
@ -97,6 +101,7 @@ fun LobstersApp(
|
||||||
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
|
val hottestPosts = viewModel.hottestPosts.collectAsLazyPagingItems()
|
||||||
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
val newestPosts = viewModel.newestPosts.collectAsLazyPagingItems()
|
||||||
val savedPosts by viewModel.savedPosts.collectAsState(persistentMapOf())
|
val savedPosts by viewModel.savedPosts.collectAsState(persistentMapOf())
|
||||||
|
val searchResults = viewModel.searchResults.collectAsLazyPagingItems()
|
||||||
|
|
||||||
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
|
val navigationType = ClawNavigationType.fromSize(windowSizeClass.widthSizeClass)
|
||||||
|
|
||||||
|
@ -130,44 +135,54 @@ fun LobstersApp(
|
||||||
) {
|
) {
|
||||||
coroutineScope.launch { savedListState.animateScrollToItem(index = 0) }
|
coroutineScope.launch { savedListState.animateScrollToItem(index = 0) }
|
||||||
},
|
},
|
||||||
|
NavigationItem(
|
||||||
|
label = "Search",
|
||||||
|
route = Destinations.Search.route,
|
||||||
|
icon = Icons.Outlined.Search,
|
||||||
|
selectedIcon = Icons.Filled.Search,
|
||||||
|
) {
|
||||||
|
coroutineScope.launch { searchListState.animateScrollToItem(index = 0) }
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
TransparentSystemBars()
|
TransparentSystemBars()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
ClawAppBar(
|
if (currentDestination != Destinations.Search.route) {
|
||||||
navigationIcon = {
|
ClawAppBar(
|
||||||
if (
|
navigationIcon = {
|
||||||
navController.previousBackStackEntry != null &&
|
if (
|
||||||
navItems.none { it.route == currentDestination }
|
navController.previousBackStackEntry != null &&
|
||||||
) {
|
navItems.none { it.route == currentDestination }
|
||||||
IconButton(
|
|
||||||
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() }
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
IconButton(
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
onClick = { if (!navController.popBackStack()) context.getActivity()?.finish() },
|
||||||
contentDescription = "Go back to previous screen",
|
) {
|
||||||
)
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
|
contentDescription = "Go back to previous screen",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
title = {
|
||||||
title = {
|
if (navItems.any { it.route == currentDestination }) {
|
||||||
if (navItems.any { it.route == currentDestination }) {
|
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
||||||
Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
if (navItems.any { it.route == currentDestination }) {
|
|
||||||
IconButton(onClick = { navController.navigate(Destinations.DataTransfer.route) }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.ImportExport,
|
|
||||||
contentDescription = "Data transfer options"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
actions = {
|
||||||
)
|
if (navItems.any { it.route == currentDestination }) {
|
||||||
|
IconButton(onClick = { navController.navigate(Destinations.DataTransfer.route) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ImportExport,
|
||||||
|
contentDescription = "Data transfer options",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
AnimatedVisibility(visible = navigationType == ClawNavigationType.BOTTOM_NAVIGATION) {
|
||||||
|
@ -224,6 +239,21 @@ fun LobstersApp(
|
||||||
postActions = postActions,
|
postActions = postActions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(Destinations.Search.route) {
|
||||||
|
setWebUri("https://lobste.rs/search")
|
||||||
|
SearchList(
|
||||||
|
items = searchResults,
|
||||||
|
listState = searchListState,
|
||||||
|
isPostSaved = viewModel::isPostSaved,
|
||||||
|
postActions = postActions,
|
||||||
|
searchQuery = viewModel.searchQuery,
|
||||||
|
setSearchQuery = { query -> viewModel.searchQuery = query },
|
||||||
|
triggerSearch = { query ->
|
||||||
|
viewModel.searchQuery = query
|
||||||
|
searchResults.refresh()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
composable(
|
composable(
|
||||||
route = Destinations.Comments.route,
|
route = Destinations.Comments.route,
|
||||||
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
|
arguments = listOf(navArgument("postId") { type = NavType.StringType }),
|
||||||
|
|
|
@ -21,6 +21,10 @@ sealed class Destinations {
|
||||||
override val route = "saved"
|
override val route = "saved"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Search : Destinations() {
|
||||||
|
override val route = "search"
|
||||||
|
}
|
||||||
|
|
||||||
object Comments : Destinations() {
|
object Comments : Destinations() {
|
||||||
const val placeholder = "{postId}"
|
const val placeholder = "{postId}"
|
||||||
override val route = "comments/$placeholder"
|
override val route = "comments/$placeholder"
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
*/
|
*/
|
||||||
package dev.msfjarvis.claw.android.viewmodel
|
package dev.msfjarvis.claw.android.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
|
@ -16,7 +19,9 @@ import com.slack.eithernet.ApiResult.Success
|
||||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource
|
import dev.msfjarvis.claw.android.paging.LobstersPagingSource
|
||||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE
|
||||||
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX
|
||||||
|
import dev.msfjarvis.claw.android.paging.SearchPagingSource
|
||||||
import dev.msfjarvis.claw.api.LobstersApi
|
import dev.msfjarvis.claw.api.LobstersApi
|
||||||
|
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||||
import dev.msfjarvis.claw.core.injection.IODispatcher
|
import dev.msfjarvis.claw.core.injection.IODispatcher
|
||||||
import dev.msfjarvis.claw.database.local.SavedPost
|
import dev.msfjarvis.claw.database.local.SavedPost
|
||||||
import dev.msfjarvis.claw.model.Comment
|
import dev.msfjarvis.claw.model.Comment
|
||||||
|
@ -41,11 +46,13 @@ class ClawViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val api: LobstersApi,
|
private val api: LobstersApi,
|
||||||
|
private val searchApi: LobstersSearchApi,
|
||||||
private val savedPostsRepository: SavedPostsRepository,
|
private val savedPostsRepository: SavedPostsRepository,
|
||||||
private val commentsRepository: CommentsRepository,
|
private val commentsRepository: CommentsRepository,
|
||||||
private val linkMetadataRepository: LinkMetadataRepository,
|
private val linkMetadataRepository: LinkMetadataRepository,
|
||||||
private val dataTransferRepository: DataTransferRepository,
|
private val dataTransferRepository: DataTransferRepository,
|
||||||
private val pagingSourceFactory: LobstersPagingSource.Factory,
|
private val pagingSourceFactory: LobstersPagingSource.Factory,
|
||||||
|
private val searchPagingSourceFactory: SearchPagingSource.Factory,
|
||||||
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IODispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val hottestPostsPager =
|
private val hottestPostsPager =
|
||||||
|
@ -56,6 +63,10 @@ constructor(
|
||||||
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||||
pagingSourceFactory.create(api::getNewestPosts)
|
pagingSourceFactory.create(api::getNewestPosts)
|
||||||
}
|
}
|
||||||
|
private val searchResultsPager =
|
||||||
|
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
|
||||||
|
searchPagingSourceFactory.create { searchApi.searchPosts(searchQuery, it) }
|
||||||
|
}
|
||||||
|
|
||||||
val hottestPosts
|
val hottestPosts
|
||||||
get() = hottestPostsPager.flow
|
get() = hottestPostsPager.flow
|
||||||
|
@ -69,6 +80,11 @@ constructor(
|
||||||
val savedPosts
|
val savedPosts
|
||||||
get() = savedPostsFlow.map(::mapSavedPosts)
|
get() = savedPostsFlow.map(::mapSavedPosts)
|
||||||
|
|
||||||
|
val searchResults
|
||||||
|
get() = searchResultsPager.flow
|
||||||
|
|
||||||
|
var searchQuery by mutableStateOf("")
|
||||||
|
|
||||||
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<Month, List<SavedPost>> {
|
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<Month, List<SavedPost>> {
|
||||||
val sorted = items.sortedByDescending { post -> post.createdAt.toLocalDateTime() }
|
val sorted = items.sortedByDescending { post -> post.createdAt.toLocalDateTime() }
|
||||||
return sorted.groupBy { post -> post.createdAt.toLocalDateTime().month }.toImmutableMap()
|
return sorted.groupBy { post -> post.createdAt.toLocalDateTime().month }.toImmutableMap()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue