From 8ec45b90db330349c46439a2a173c6eb974eb4f2 Mon Sep 17 00:00:00 2001 From: HuanCheng65 <22636177+HuanCheng65@users.noreply.github.com> Date: Fri, 6 Oct 2023 01:10:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9F=A5=E7=9C=8B=E5=90=A7=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tieba/post/ui/page/forum/ForumPage.kt | 102 ++++---- .../ui/page/forum/detail/ForumDetailPage.kt | 232 ++++++++++++++++++ .../page/forum/detail/ForumDetailViewModel.kt | 95 +++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 381 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/detail/ForumDetailPage.kt create mode 100644 app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/detail/ForumDetailViewModel.kt diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/ForumPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/ForumPage.kt index 65720961..eb7dbe37 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/ForumPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/ForumPage.kt @@ -9,7 +9,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollable @@ -104,6 +104,7 @@ import com.huanchengfly.tieba.post.toastShort import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.ProvideNavigator +import com.huanchengfly.tieba.post.ui.page.destinations.ForumDetailPageDestination import com.huanchengfly.tieba.post.ui.page.forum.threadlist.ForumThreadListPage import com.huanchengfly.tieba.post.ui.page.forum.threadlist.ForumThreadListUiEvent import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar @@ -113,7 +114,6 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.Button import com.huanchengfly.tieba.post.ui.widgets.compose.ClickMenu import com.huanchengfly.tieba.post.ui.widgets.compose.ConfirmDialog import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCardPlaceholder -import com.huanchengfly.tieba.post.ui.widgets.compose.HorizontalDivider import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.MenuScope import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold @@ -197,29 +197,15 @@ private fun ForumHeaderPlaceholder( } } } - Row( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .placeholder( - visible = true, - highlight = PlaceholderHighlight.fade(), - ) - .padding(vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - StatCardItem( - statNum = 0, - statText = stringResource(id = R.string.text_stat_follow) - ) - } } } @Composable private fun ForumHeader( forumInfoImmutableHolder: ImmutableHolder, + onOpenForumInfo: () -> Unit, onBtnClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val (forum) = forumInfoImmutableHolder Column( @@ -239,12 +225,27 @@ private fun ForumHeader( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp) ) { - Text( - text = stringResource(id = R.string.title_forum, forum.name), - style = MaterialTheme.typography.h6, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onOpenForumInfo + ) + ) { + Text( + text = stringResource(id = R.string.title_forum, forum.name), + style = MaterialTheme.typography.h6, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +// Icon( +// imageVector = Icons.Rounded.KeyboardArrowRight, +// contentDescription = null, +// modifier = Modifier.size(16.dp) +// ) + } AnimatedVisibility(visible = forum.is_like == 1) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { LinearProgressIndicator( @@ -301,28 +302,28 @@ private fun ForumHeader( } } } - Row( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(color = ExtendedTheme.colors.chip) - .padding(vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - StatCardItem( - statNum = forum.member_num, - statText = stringResource(id = R.string.text_stat_follow) - ) - HorizontalDivider(color = Color(if (ExtendedTheme.colors.isNightMode) 0xFF808080 else 0xFFDEDEDE)) - StatCardItem( - statNum = forum.thread_num, - statText = stringResource(id = R.string.text_stat_threads) - ) - HorizontalDivider(color = Color(if (ExtendedTheme.colors.isNightMode) 0xFF808080 else 0xFFDEDEDE)) - StatCardItem( - statNum = forum.post_num, - statText = stringResource(id = R.string.title_stat_posts_num) - ) - } +// Row( +// modifier = Modifier +// .clip(RoundedCornerShape(8.dp)) +// .background(color = ExtendedTheme.colors.chip) +// .padding(vertical = 20.dp), +// verticalAlignment = Alignment.CenterVertically, +// ) { +// StatCardItem( +// statNum = forum.member_num, +// statText = stringResource(id = R.string.text_stat_follow) +// ) +// HorizontalDivider(color = Color(if (ExtendedTheme.colors.isNightMode) 0xFF808080 else 0xFFDEDEDE)) +// StatCardItem( +// statNum = forum.thread_num, +// statText = stringResource(id = R.string.text_stat_threads) +// ) +// HorizontalDivider(color = Color(if (ExtendedTheme.colors.isNightMode) 0xFF808080 else 0xFFDEDEDE)) +// StatCardItem( +// statNum = forum.post_num, +// statText = stringResource(id = R.string.title_stat_posts_num) +// ) +// } } } @@ -651,11 +652,14 @@ fun ForumPage( ), exit = shrinkVertically() ) { - if (forumInfo != null) { + forumInfo?.let { ForumHeader( - forumInfoImmutableHolder = forumInfo!!, + forumInfoImmutableHolder = it, + onOpenForumInfo = { + navigator.navigate(ForumDetailPageDestination(forumId = it.get { this.id })) + }, onBtnClick = { - val (forum) = forumInfo!! + val (forum) = it when { forum.is_like != 1 -> viewModel.send( ForumUiIntent.Like( diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/detail/ForumDetailPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/detail/ForumDetailPage.kt new file mode 100644 index 00000000..883cc2ad --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/detail/ForumDetailPage.kt @@ -0,0 +1,232 @@ +package com.huanchengfly.tieba.post.ui.page.forum.detail + +import android.graphics.Typeface +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.huanchengfly.tieba.post.R +import com.huanchengfly.tieba.post.api.models.protos.PbContent +import com.huanchengfly.tieba.post.api.models.protos.RecommendForumInfo +import com.huanchengfly.tieba.post.api.models.protos.plainText +import com.huanchengfly.tieba.post.arch.ImmutableHolder +import com.huanchengfly.tieba.post.arch.collectPartialAsState +import com.huanchengfly.tieba.post.arch.getOrNull +import com.huanchengfly.tieba.post.arch.pageViewModel +import com.huanchengfly.tieba.post.arch.wrapImmutable +import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme +import com.huanchengfly.tieba.post.ui.common.theme.compose.TiebaLiteTheme +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.BackNavigationIcon +import com.huanchengfly.tieba.post.ui.widgets.compose.Container +import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen +import com.huanchengfly.tieba.post.ui.widgets.compose.HorizontalDivider +import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad +import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold +import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes +import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar +import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen +import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator + +@Destination +@Composable +fun ForumDetailPage( + forumId: Long, + navigator: DestinationsNavigator, + viewModel: ForumDetailViewModel = pageViewModel(), +) { + LazyLoad(loaded = viewModel.initialized) { + viewModel.send(ForumDetailUiIntent.Load(forumId)) + viewModel.initialized = true + } + + val isLoading by viewModel.uiState.collectPartialAsState( + prop1 = ForumDetailUiState::isLoading, + initial = true + ) + val error by viewModel.uiState.collectPartialAsState( + prop1 = ForumDetailUiState::error, + initial = null + ) + val forumInfo by viewModel.uiState.collectPartialAsState( + prop1 = ForumDetailUiState::forumInfo, + initial = null + ) + + val isEmpty by remember { + derivedStateOf { forumInfo == null } + } + val isError by remember { + derivedStateOf { error != null } + } + + StateScreen( + isEmpty = isEmpty, + isError = isError, + isLoading = isLoading, + onReload = { + viewModel.send(ForumDetailUiIntent.Load(forumId)) + }, + errorScreen = { + ErrorScreen(error = error.getOrNull()) + } + ) { + MyScaffold( + topBar = { + TitleCentredToolbar( + title = { + Text(text = stringResource(id = R.string.title_forum_info)) + }, + navigationIcon = { + BackNavigationIcon { + navigator.navigateUp() + } + } + ) + } + ) { paddingValues -> + Container(modifier = Modifier.padding(paddingValues)) { + forumInfo?.let { + ForumDetailContent( + forumInfo = it, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + } +} + +@Composable +private fun ForumDetailContent( + forumInfo: ImmutableHolder, + modifier: Modifier = Modifier, +) { + val intro = remember(forumInfo) { + forumInfo.get { content.plainText } + } + Column( + modifier = modifier + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Avatar( + data = forumInfo.get { avatar }, + size = Sizes.Medium, + contentDescription = null, + ) + Text( + text = stringResource(id = R.string.title_forum, forumInfo.get { forum_name }), + style = MaterialTheme.typography.h6 + ) + } + Row( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(color = ExtendedTheme.colors.chip) + .padding(vertical = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + StatCardItem( + statNum = forumInfo.get { member_count }, + statText = stringResource(id = R.string.text_stat_follow) + ) + HorizontalDivider(color = Color(if (ExtendedTheme.colors.isNightMode) 0xFF808080 else 0xFFDEDEDE)) + StatCardItem( + statNum = forumInfo.get { thread_count }, + statText = stringResource(id = R.string.text_stat_threads) + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Chip(text = stringResource(id = R.string.title_forum_intro)) + Column { + Text(text = forumInfo.get { slogan }, style = MaterialTheme.typography.body1) + Text(text = intro, style = MaterialTheme.typography.body1) + } + } + } +} + +@Composable +private fun RowScope.StatCardItem( + statNum: Int, + statText: String, +) { + Column( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = statNum.getShortNumString(), + fontSize = 20.sp, + fontFamily = FontFamily( + Typeface.createFromAsset( + LocalContext.current.assets, + "bebas.ttf" + ) + ), + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = statText, + fontSize = 12.sp, + color = ExtendedTheme.colors.textSecondary + ) + } +} + +@Preview("ForumDetailPage", backgroundColor = 0xFFFFFFFF, showBackground = true) +@Composable +fun PreviewForumDetailPage() { + TiebaLiteTheme { + ForumDetailContent( + forumInfo = RecommendForumInfo( + forum_name = "minecraft", + slogan = "位于百度贴吧的像素点之家", + content = listOf( + PbContent( + type = 0, + text = "minecraft……", + ) + ), + member_count = 2520287, + thread_count = 31531580 + ).wrapImmutable(), + modifier = Modifier.fillMaxWidth() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/detail/ForumDetailViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/detail/ForumDetailViewModel.kt new file mode 100644 index 00000000..2ac59a14 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/detail/ForumDetailViewModel.kt @@ -0,0 +1,95 @@ +package com.huanchengfly.tieba.post.ui.page.forum.detail + +import androidx.compose.runtime.Immutable +import com.huanchengfly.tieba.post.api.TiebaApi +import com.huanchengfly.tieba.post.api.models.protos.RecommendForumInfo +import com.huanchengfly.tieba.post.api.models.protos.getForumDetail.GetForumDetailResponse +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.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 ForumDetailViewModel @Inject constructor() : + BaseViewModel() { + override fun createInitialState(): ForumDetailUiState = ForumDetailUiState() + + override fun createPartialChangeProducer(): PartialChangeProducer = + ForumDetailPartialChangeProducer + + private object ForumDetailPartialChangeProducer : + PartialChangeProducer { + @OptIn(ExperimentalCoroutinesApi::class) + override fun toPartialChangeFlow(intentFlow: Flow): Flow = + merge( + intentFlow.filterIsInstance() + .flatMapConcat { it.producePartialChange() } + ) + + private fun ForumDetailUiIntent.Load.producePartialChange(): Flow = + TiebaApi.getInstance() + .getForumDetailFlow(forumId) + .map { + val forumInfo = it.data_?.forum_info + checkNotNull(forumInfo) { "forumInfo is null" } + ForumDetailPartialChange.Load.Success( + forumInfo + ) + } + .onStart { emit(ForumDetailPartialChange.Load.Start) } + .catch { emit(ForumDetailPartialChange.Load.Failure(it)) } + } +} + +sealed interface ForumDetailUiIntent : UiIntent { + data class Load(val forumId: Long) : ForumDetailUiIntent +} + +sealed interface ForumDetailPartialChange : PartialChange { + sealed class Load : ForumDetailPartialChange { + override fun reduce(oldState: ForumDetailUiState): ForumDetailUiState = when (this) { + Start -> oldState.copy( + isLoading = true, + ) + + is Success -> oldState.copy( + isLoading = false, + error = null, + forumInfo = forumInfo.wrapImmutable() + ) + + is Failure -> oldState.copy( + isLoading = false, + error = error.wrapImmutable() + ) + } + + data object Start : Load() + + data class Success(val forumInfo: RecommendForumInfo) : Load() + + data class Failure(val error: Throwable) : Load() + } +} + +@Immutable +data class ForumDetailUiState( + val isLoading: Boolean = true, + val error: ImmutableHolder? = null, + + val forumInfo: ImmutableHolder? = null, +) : UiState \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4161a69d..385a3488 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -742,6 +742,7 @@ 打开 隐藏 搜索联想:%s + 本吧简介 本吧吧规 吧规