feat: 新版搜索状态屏

This commit is contained in:
HuanCheng65 2023-09-23 15:00:32 +08:00
parent 23f73f2e1a
commit 5f7203228e
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
9 changed files with 308 additions and 194 deletions

View File

@ -72,6 +72,8 @@ import androidx.compose.ui.util.fastForEachIndexed
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
import com.huanchengfly.tieba.post.arch.emitGlobalEventSuspend
import com.huanchengfly.tieba.post.arch.onEvent
import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.models.database.SearchHistory
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
@ -134,12 +136,17 @@ fun SearchPage(
derivedStateOf { keyword.isEmpty() }
}
var inputKeyword by remember { mutableStateOf("") }
var expanded by remember { mutableStateOf(false) }
val initialSortType = remember { SearchThreadSortType.SORT_TYPE_NEWEST }
var searchThreadSortType by remember { mutableIntStateOf(initialSortType) }
LaunchedEffect(searchThreadSortType) {
emitGlobalEvent(SearchThreadUiEvent.SwitchSortType(searchThreadSortType))
}
viewModel.onEvent<SearchUiEvent.KeywordChanged> {
inputKeyword = it.keyword
emitGlobalEventSuspend(it)
}
val pages by remember {
derivedStateOf {
@ -234,6 +241,8 @@ fun SearchPage(
inputKeyword = it.content
viewModel.send(SearchUiIntent.SubmitKeyword(it.content))
},
expanded = expanded,
onToggleExpand = { expanded = !expanded },
onClear = { viewModel.send(SearchUiIntent.ClearSearchHistory) }
)
}

View File

@ -47,6 +47,8 @@ class SearchViewModel :
App.INSTANCE.getString(R.string.toast_clear_failure, partialChange.errorMessage)
)
is SearchPartialChange.SubmitKeyword -> SearchUiEvent.KeywordChanged(partialChange.keyword)
else -> null
}
@ -145,4 +147,6 @@ data class SearchUiState(
val searchHistories: ImmutableList<SearchHistory> = persistentListOf(),
) : UiState
sealed interface SearchUiEvent : UiEvent
sealed interface SearchUiEvent : UiEvent {
data class KeywordChanged(val keyword: String) : SearchUiEvent
}

View File

@ -30,16 +30,20 @@ import androidx.compose.ui.unit.dp
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.SearchForumBean
import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
import com.huanchengfly.tieba.post.ui.page.search.SearchUiEvent
import com.huanchengfly.tieba.post.ui.widgets.Chip
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LocalShouldLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@ -53,20 +57,17 @@ fun SearchForumPage(
viewModel.send(SearchForumUiIntent.Refresh(keyword))
viewModel.initialized = true
}
val shouldLoad = LocalShouldLoad.current
LaunchedEffect(keyword) {
if (viewModel.initialized) {
if (shouldLoad) {
viewModel.send(SearchForumUiIntent.Refresh(keyword))
} else {
viewModel.initialized = false
}
}
}
val currentKeyword by viewModel.uiState.collectPartialAsState(
prop1 = SearchForumUiState::keyword,
initial = ""
)
val isRefreshing by viewModel.uiState.collectPartialAsState(
prop1 = SearchForumUiState::isRefreshing,
initial = false
initial = true
)
val error by viewModel.uiState.collectPartialAsState(
prop1 = SearchForumUiState::error,
initial = null
)
val exactMatchForum by viewModel.uiState.collectPartialAsState(
prop1 = SearchForumUiState::exactMatchForum,
@ -89,6 +90,36 @@ fun SearchForumPage(
onRefresh = { viewModel.send(SearchForumUiIntent.Refresh(keyword)) }
)
val isEmpty by remember {
derivedStateOf { !showExactMatchResult && !showFuzzyMatchResult }
}
onGlobalEvent<SearchUiEvent.KeywordChanged> {
viewModel.send(SearchForumUiIntent.Refresh(it.keyword))
}
val shouldLoad = LocalShouldLoad.current
LaunchedEffect(currentKeyword) {
if (currentKeyword.isNotEmpty() && keyword != currentKeyword) {
if (shouldLoad) {
viewModel.send(SearchForumUiIntent.Refresh(keyword))
} else {
viewModel.initialized = false
}
}
}
StateScreen(
isEmpty = isEmpty,
isError = error != null,
isLoading = isRefreshing,
onReload = { viewModel.send(SearchForumUiIntent.Refresh(keyword)) },
errorScreen = {
error?.let {
val (e) = it
ErrorScreen(error = e)
}
}
) {
Box(
modifier = Modifier
.fillMaxSize()
@ -153,6 +184,8 @@ fun SearchForumPage(
}
}
}
@Composable
private fun SearchForumItem(
item: SearchForumBean.ForumInfoBean,

View File

@ -93,7 +93,7 @@ data class SearchForumUiState(
val keyword: String = "",
val exactMatchForum: SearchForumBean.ForumInfoBean? = null,
val fuzzyMatchForumList: List<SearchForumBean.ForumInfoBean> = persistentListOf(),
val isRefreshing: Boolean = false,
val isRefreshing: Boolean = true,
val error: ImmutableHolder<Throwable>? = null,
) : UiState

View File

@ -16,7 +16,9 @@ import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@ -30,8 +32,10 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
import com.huanchengfly.tieba.post.ui.page.search.SearchUiEvent
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
import com.huanchengfly.tieba.post.ui.widgets.compose.Card
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.ForumInfoChip
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
@ -42,6 +46,7 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadContent
import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadReplyBtn
import com.huanchengfly.tieba.post.ui.widgets.compose.ThreadShareBtn
import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
import com.huanchengfly.tieba.post.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.StringUtil
import kotlinx.collections.immutable.ImmutableList
@ -60,9 +65,13 @@ fun SearchThreadPage(
viewModel.send(SearchThreadUiIntent.Refresh(keyword, initialSortType))
viewModel.initialized = true
}
val currentKeyword by viewModel.uiState.collectPartialAsState(
prop1 = SearchThreadUiState::keyword,
initial = ""
)
val isRefreshing by viewModel.uiState.collectPartialAsState(
prop1 = SearchThreadUiState::isRefreshing,
initial = false
initial = true
)
val isLoadingMore by viewModel.uiState.collectPartialAsState(
prop1 = SearchThreadUiState::isLoadingMore,
@ -88,9 +97,13 @@ fun SearchThreadPage(
prop1 = SearchThreadUiState::sortType,
initial = initialSortType
)
onGlobalEvent<SearchThreadUiEvent.SwitchSortType> {
viewModel.send(SearchThreadUiIntent.Refresh(keyword, it.sortType))
}
val shouldLoad = LocalShouldLoad.current
LaunchedEffect(keyword) {
if (viewModel.initialized) {
LaunchedEffect(currentKeyword) {
if (currentKeyword.isNotEmpty() && keyword != currentKeyword) {
if (shouldLoad) {
viewModel.send(SearchThreadUiIntent.Refresh(keyword, sortType))
} else {
@ -99,16 +112,32 @@ fun SearchThreadPage(
}
}
onGlobalEvent<SearchThreadUiEvent.SwitchSortType> {
viewModel.send(SearchThreadUiIntent.Refresh(keyword, it.sortType))
}
val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing,
onRefresh = { viewModel.send(SearchThreadUiIntent.Refresh(keyword, sortType)) }
)
val lazyListState = rememberLazyListState()
val isEmpty by remember {
derivedStateOf { data.isEmpty() }
}
onGlobalEvent<SearchUiEvent.KeywordChanged> {
viewModel.send(SearchThreadUiIntent.Refresh(it.keyword, sortType))
}
StateScreen(
isEmpty = isEmpty,
isError = error != null,
isLoading = isRefreshing,
onReload = { viewModel.send(SearchThreadUiIntent.Refresh(keyword, sortType)) },
errorScreen = {
error?.let {
val (e) = it
ErrorScreen(error = e)
}
}
) {
Box(
modifier = Modifier
.fillMaxSize()
@ -156,6 +185,7 @@ fun SearchThreadPage(
}
}
}
}
@Composable
private fun SearchThreadList(

View File

@ -136,7 +136,7 @@ sealed interface SearchThreadPartialChange : PartialChange<SearchThreadUiState>
}
data class SearchThreadUiState(
val isRefreshing: Boolean = false,
val isRefreshing: Boolean = true,
val isLoadingMore: Boolean = false,
val error: ImmutableHolder<Throwable>? = null,
val currentPage: Int = 1,

View File

@ -31,15 +31,19 @@ import androidx.compose.ui.unit.dp
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.SearchUserBean
import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.search.SearchUiEvent
import com.huanchengfly.tieba.post.ui.widgets.Chip
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LocalShouldLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
import com.huanchengfly.tieba.post.utils.StringUtil
import kotlinx.collections.immutable.persistentListOf
@ -55,19 +59,17 @@ fun SearchUserPage(
viewModel.send(SearchUserUiIntent.Refresh(keyword))
viewModel.initialized = true
}
val shouldLoad = LocalShouldLoad.current
LaunchedEffect(keyword) {
if (viewModel.initialized) {
if (shouldLoad) {
viewModel.send(SearchUserUiIntent.Refresh(keyword))
} else {
viewModel.initialized = false
}
}
}
val currentKeyword by viewModel.uiState.collectPartialAsState(
prop1 = SearchUserUiState::keyword,
initial = ""
)
val isRefreshing by viewModel.uiState.collectPartialAsState(
prop1 = SearchUserUiState::isRefreshing,
initial = false
initial = true
)
val error by viewModel.uiState.collectPartialAsState(
prop1 = SearchUserUiState::error,
initial = null
)
val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing,
@ -89,6 +91,36 @@ fun SearchUserPage(
derivedStateOf { fuzzyMatch.isNotEmpty() }
}
onGlobalEvent<SearchUiEvent.KeywordChanged> {
viewModel.send(SearchUserUiIntent.Refresh(it.keyword))
}
val shouldLoad = LocalShouldLoad.current
LaunchedEffect(currentKeyword) {
if (currentKeyword.isNotEmpty() && keyword != currentKeyword) {
if (shouldLoad) {
viewModel.send(SearchUserUiIntent.Refresh(keyword))
} else {
viewModel.initialized = false
}
}
}
val isEmpty by remember {
derivedStateOf { !showExactMatchResult && !showFuzzyMatchResult }
}
StateScreen(
isEmpty = isEmpty,
isError = error != null,
isLoading = isRefreshing,
onReload = { viewModel.send(SearchUserUiIntent.Refresh(keyword)) },
errorScreen = {
error?.let {
val (e) = it
ErrorScreen(error = e)
}
}
) {
Box(
modifier = Modifier
.fillMaxSize()
@ -150,6 +182,7 @@ fun SearchUserPage(
)
}
}
}
@Composable
private fun SearchUserItem(

View File

@ -91,7 +91,7 @@ sealed interface SearchUserPartialChange : PartialChange<SearchUserUiState> {
}
data class SearchUserUiState(
val isRefreshing: Boolean = false,
val isRefreshing: Boolean = true,
val error: ImmutableHolder<Throwable>? = null,
val keyword: String = "",
val exactMatch: SearchUserBean.UserBean? = null,

View File

@ -12,7 +12,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@ -27,8 +29,11 @@ fun LazyLoad(
onLoad: () -> Unit,
) {
val shouldLoad = LocalShouldLoad.current
LaunchedEffect(loaded, shouldLoad, onLoad) {
if (!loaded && shouldLoad) onLoad()
val curOnLoad by rememberUpdatedState(newValue = onLoad)
LaunchedEffect(loaded, shouldLoad) {
if (!loaded && shouldLoad) {
curOnLoad()
}
}
}