diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/models/UserLikeForumBean.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/models/UserLikeForumBean.kt index b5491c99..fd3682c8 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/models/UserLikeForumBean.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/models/UserLikeForumBean.kt @@ -1,5 +1,6 @@ package com.huanchengfly.tieba.post.api.models +import androidx.compose.runtime.Immutable import com.google.gson.annotations.SerializedName import com.huanchengfly.tieba.post.models.BaseBean import kotlinx.collections.immutable.persistentListOf @@ -39,9 +40,10 @@ data class UserLikeForumBean( val forumList: List = persistentListOf(), ) + @Immutable @Serializable data class ForumBean( - val id: String? = null, + val id: String = "", @JvmField val name: String? = null, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/user/UserProfilePage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/user/UserProfilePage.kt index 631397c0..4103e5b8 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/user/UserProfilePage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/user/UserProfilePage.kt @@ -69,6 +69,7 @@ import com.huanchengfly.tieba.post.arch.getOrNull import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.page.ProvideNavigator +import com.huanchengfly.tieba.post.ui.page.user.likeforum.UserLikeForumPage import com.huanchengfly.tieba.post.ui.page.user.post.UserPostPage import com.huanchengfly.tieba.post.ui.widgets.Chip import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar @@ -283,7 +284,7 @@ fun UserProfilePage( ) }, content = { - Text(text = "concern_forums") + UserLikeForumPage(uid = it.get { id }) } ), ).toImmutableList() diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/user/likeforum/UserLikeForumPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/user/likeforum/UserLikeForumPage.kt new file mode 100644 index 00000000..3d0ac0f8 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/user/likeforum/UserLikeForumPage.kt @@ -0,0 +1,199 @@ +package com.huanchengfly.tieba.post.ui.page.user.likeforum + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.runtime.Composable +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.unit.dp +import com.huanchengfly.tieba.post.api.models.UserLikeForumBean +import com.huanchengfly.tieba.post.arch.GlobalEvent +import com.huanchengfly.tieba.post.arch.collectPartialAsState +import com.huanchengfly.tieba.post.arch.getOrNull +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.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.LoadMoreLayout +import com.huanchengfly.tieba.post.ui.widgets.compose.MyLazyColumn +import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes +import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun UserLikeForumPage( + uid: Long, + viewModel: UserLikeForumViewModel = pageViewModel(), +) { + val navigator = LocalNavigator.current + + LazyLoad(loaded = viewModel.initialized) { + viewModel.send(UserLikeForumUiIntent.Refresh(uid)) + viewModel.initialized = true + } + + val isRefreshing by viewModel.uiState.collectPartialAsState( + prop1 = UserLikeForumUiState::isRefreshing, + initial = true + ) + val isLoadingMore by viewModel.uiState.collectPartialAsState( + prop1 = UserLikeForumUiState::isLoadingMore, + initial = false + ) + val error by viewModel.uiState.collectPartialAsState( + prop1 = UserLikeForumUiState::error, + initial = null + ) + val currentPage by viewModel.uiState.collectPartialAsState( + prop1 = UserLikeForumUiState::currentPage, + initial = 1 + ) + val hasMore by viewModel.uiState.collectPartialAsState( + prop1 = UserLikeForumUiState::hasMore, + initial = false + ) + val forums by viewModel.uiState.collectPartialAsState( + prop1 = UserLikeForumUiState::forums, + initial = persistentListOf() + ) + + val isEmpty by remember { + derivedStateOf { forums.isEmpty() } + } + val isError by remember { + derivedStateOf { error != null } + } + + onGlobalEvent( + filter = { it.key == "user_profile" } + ) { + viewModel.send(UserLikeForumUiIntent.Refresh(uid)) + } + + StateScreen( + isEmpty = isEmpty, + isError = isError, + isLoading = isRefreshing, + onReload = { + viewModel.send(UserLikeForumUiIntent.Refresh(uid)) + }, + errorScreen = { ErrorScreen(error = error.getOrNull()) }, + ) { + val pullRefreshState = rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = ::reload + ) + + val lazyListState = rememberLazyListState() + + Box { + LoadMoreLayout( + isLoading = isLoadingMore, + onLoadMore = { + viewModel.send(UserLikeForumUiIntent.LoadMore(uid, currentPage)) + }, + loadEnd = !hasMore, + lazyListState = lazyListState + ) { + UserLikeForumList( + forums = { forums }, + onClickForum = { forumBean -> + forumBean.name?.let { + navigator.navigate(ForumPageDestination(it)) + } + }, + lazyListState = lazyListState + ) + } + + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + backgroundColor = ExtendedTheme.colors.pullRefreshIndicator, + contentColor = ExtendedTheme.colors.primary, + ) + } + } +} + +@Composable +private fun UserLikeForumList( + forums: () -> ImmutableList, + onClickForum: (UserLikeForumBean.ForumBean) -> Unit, + lazyListState: LazyListState = rememberLazyListState(), +) { + val data = remember(forums) { forums() } + MyLazyColumn(state = lazyListState) { + items( + items = data, + key = { it.id } + ) { + UserLikeForumItem( + item = it, + onClick = { + onClickForum(it) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + } +} + +@Composable +private fun UserLikeForumItem( + item: UserLikeForumBean.ForumBean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = Modifier + .clickable(onClick = onClick) + .then(modifier), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Avatar( + data = item.avatar, + size = Sizes.Medium, + contentDescription = null + ) + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = item.name.orEmpty(), style = MaterialTheme.typography.subtitle1) + item.slogan.takeUnless { it.isNullOrEmpty() }?.let { + Text( + text = it, + style = MaterialTheme.typography.body2, + color = ExtendedTheme.colors.textSecondary + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/user/likeforum/UserLikeForumViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/user/likeforum/UserLikeForumViewModel.kt new file mode 100644 index 00000000..d8f35914 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/user/likeforum/UserLikeForumViewModel.kt @@ -0,0 +1,168 @@ +package com.huanchengfly.tieba.post.ui.page.user.likeforum + +import androidx.compose.runtime.Immutable +import com.huanchengfly.tieba.post.api.TiebaApi +import com.huanchengfly.tieba.post.api.models.UserLikeForumBean +import com.huanchengfly.tieba.post.arch.BaseViewModel +import com.huanchengfly.tieba.post.arch.ImmutableHolder +import com.huanchengfly.tieba.post.arch.PartialChange +import com.huanchengfly.tieba.post.arch.PartialChangeProducer +import com.huanchengfly.tieba.post.arch.UiEvent +import com.huanchengfly.tieba.post.arch.UiIntent +import com.huanchengfly.tieba.post.arch.UiState +import com.huanchengfly.tieba.post.arch.wrapImmutable +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject + +@HiltViewModel +class UserLikeForumViewModel @Inject constructor() : + BaseViewModel() { + override fun createInitialState(): UserLikeForumUiState = UserLikeForumUiState() + + override fun createPartialChangeProducer(): PartialChangeProducer = + UserLikeForumPartialChangeProducer + + private object UserLikeForumPartialChangeProducer : + PartialChangeProducer { + @OptIn(ExperimentalCoroutinesApi::class) + override fun toPartialChangeFlow(intentFlow: Flow): Flow = + merge( + intentFlow.filterIsInstance() + .flatMapConcat { it.toPartialChangeFlow() }, + intentFlow.filterIsInstance() + .flatMapConcat { it.toPartialChangeFlow() }, + ) + + private fun UserLikeForumUiIntent.Refresh.toPartialChangeFlow(): Flow = + TiebaApi.getInstance() + .userLikeForumFlow(uid.toString()) + .map { + UserLikeForumPartialChange.Refresh.Success( + page = 1, + hasMore = it.hasMore == "1", + forums = it.forumList.forumList, + ) + } + .onStart { emit(UserLikeForumPartialChange.Refresh.Start) } + .catch { emit(UserLikeForumPartialChange.Refresh.Failure(it)) } + + private fun UserLikeForumUiIntent.LoadMore.toPartialChangeFlow(): Flow = + TiebaApi.getInstance() + .userLikeForumFlow(uid.toString(), page + 1) + .map { + UserLikeForumPartialChange.LoadMore.Success( + page = page + 1, + hasMore = it.hasMore == "1", + forums = it.forumList.forumList, + ) + } + .onStart { emit(UserLikeForumPartialChange.LoadMore.Start) } + .catch { emit(UserLikeForumPartialChange.LoadMore.Failure(it)) } + } +} + +sealed interface UserLikeForumUiIntent : UiIntent { + data class Refresh(val uid: Long) : UserLikeForumUiIntent + data class LoadMore( + val uid: Long, + val page: Int, + ) : UserLikeForumUiIntent +} + +sealed interface UserLikeForumPartialChange : PartialChange { + sealed class Refresh : UserLikeForumPartialChange { + override fun reduce(oldState: UserLikeForumUiState): UserLikeForumUiState = when (this) { + is Start -> { + oldState.copy( + isRefreshing = true, + ) + } + + is Success -> { + oldState.copy( + isRefreshing = false, + error = null, + currentPage = page, + hasMore = hasMore, + forums = forums.toImmutableList(), + ) + } + + is Failure -> { + oldState.copy( + isRefreshing = false, + error = error.wrapImmutable(), + ) + } + } + + data object Start : Refresh() + + data class Success( + val page: Int, + val hasMore: Boolean, + val forums: List, + ) : Refresh() + + data class Failure(val error: Throwable) : Refresh() + } + + sealed class LoadMore : UserLikeForumPartialChange { + override fun reduce(oldState: UserLikeForumUiState): UserLikeForumUiState = when (this) { + is Start -> { + oldState.copy( + isLoadingMore = true, + ) + } + + is Success -> { + val uniqueForums = (oldState.forums + forums).distinctBy { it.id } + oldState.copy( + isLoadingMore = false, + error = null, + currentPage = page, + hasMore = hasMore, + forums = uniqueForums.toImmutableList(), + ) + } + + is Failure -> { + oldState.copy( + isLoadingMore = false, + error = error.wrapImmutable(), + ) + } + } + + data object Start : LoadMore() + + data class Success( + val page: Int, + val hasMore: Boolean, + val forums: List, + ) : LoadMore() + + data class Failure(val error: Throwable) : LoadMore() + } +} + +@Immutable +data class UserLikeForumUiState( + val isRefreshing: Boolean = false, + val isLoadingMore: Boolean = false, + val error: ImmutableHolder? = null, + val currentPage: Int = 1, + val hasMore: Boolean = false, + val forums: ImmutableList = persistentListOf(), +) : UiState \ No newline at end of file