feat: 查看吧信息

This commit is contained in:
HuanCheng65 2023-10-06 01:10:29 +08:00
parent 58459c8f8d
commit 8ec45b90db
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
4 changed files with 381 additions and 49 deletions

View File

@ -9,7 +9,7 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.ExperimentalFoundationApi 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.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable 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.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator 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.ForumThreadListPage
import com.huanchengfly.tieba.post.ui.page.forum.threadlist.ForumThreadListUiEvent import com.huanchengfly.tieba.post.ui.page.forum.threadlist.ForumThreadListUiEvent
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar 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.ClickMenu
import com.huanchengfly.tieba.post.ui.widgets.compose.ConfirmDialog 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.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.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.MenuScope import com.huanchengfly.tieba.post.ui.widgets.compose.MenuScope
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold 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 @Composable
private fun ForumHeader( private fun ForumHeader(
forumInfoImmutableHolder: ImmutableHolder<ForumInfo>, forumInfoImmutableHolder: ImmutableHolder<ForumInfo>,
onOpenForumInfo: () -> Unit,
onBtnClick: () -> Unit, onBtnClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
val (forum) = forumInfoImmutableHolder val (forum) = forumInfoImmutableHolder
Column( Column(
@ -239,12 +225,27 @@ private fun ForumHeader(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
Text( Row(
text = stringResource(id = R.string.title_forum, forum.name), verticalAlignment = Alignment.CenterVertically,
style = MaterialTheme.typography.h6, horizontalArrangement = Arrangement.spacedBy(4.dp),
maxLines = 1, modifier = Modifier.clickable(
overflow = TextOverflow.Ellipsis, 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) { AnimatedVisibility(visible = forum.is_like == 1) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
LinearProgressIndicator( LinearProgressIndicator(
@ -301,28 +302,28 @@ private fun ForumHeader(
} }
} }
} }
Row( // Row(
modifier = Modifier // modifier = Modifier
.clip(RoundedCornerShape(8.dp)) // .clip(RoundedCornerShape(8.dp))
.background(color = ExtendedTheme.colors.chip) // .background(color = ExtendedTheme.colors.chip)
.padding(vertical = 20.dp), // .padding(vertical = 20.dp),
verticalAlignment = Alignment.CenterVertically, // verticalAlignment = Alignment.CenterVertically,
) { // ) {
StatCardItem( // StatCardItem(
statNum = forum.member_num, // statNum = forum.member_num,
statText = stringResource(id = R.string.text_stat_follow) // statText = stringResource(id = R.string.text_stat_follow)
) // )
HorizontalDivider(color = Color(if (ExtendedTheme.colors.isNightMode) 0xFF808080 else 0xFFDEDEDE)) // HorizontalDivider(color = Color(if (ExtendedTheme.colors.isNightMode) 0xFF808080 else 0xFFDEDEDE))
StatCardItem( // StatCardItem(
statNum = forum.thread_num, // statNum = forum.thread_num,
statText = stringResource(id = R.string.text_stat_threads) // statText = stringResource(id = R.string.text_stat_threads)
) // )
HorizontalDivider(color = Color(if (ExtendedTheme.colors.isNightMode) 0xFF808080 else 0xFFDEDEDE)) // HorizontalDivider(color = Color(if (ExtendedTheme.colors.isNightMode) 0xFF808080 else 0xFFDEDEDE))
StatCardItem( // StatCardItem(
statNum = forum.post_num, // statNum = forum.post_num,
statText = stringResource(id = R.string.title_stat_posts_num) // statText = stringResource(id = R.string.title_stat_posts_num)
) // )
} // }
} }
} }
@ -651,11 +652,14 @@ fun ForumPage(
), ),
exit = shrinkVertically() exit = shrinkVertically()
) { ) {
if (forumInfo != null) { forumInfo?.let {
ForumHeader( ForumHeader(
forumInfoImmutableHolder = forumInfo!!, forumInfoImmutableHolder = it,
onOpenForumInfo = {
navigator.navigate(ForumDetailPageDestination(forumId = it.get { this.id }))
},
onBtnClick = { onBtnClick = {
val (forum) = forumInfo!! val (forum) = it
when { when {
forum.is_like != 1 -> viewModel.send( forum.is_like != 1 -> viewModel.send(
ForumUiIntent.Like( ForumUiIntent.Like(

View File

@ -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<RecommendForumInfo>,
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()
)
}
}

View File

@ -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<ForumDetailUiIntent, ForumDetailPartialChange, ForumDetailUiState, UiEvent>() {
override fun createInitialState(): ForumDetailUiState = ForumDetailUiState()
override fun createPartialChangeProducer(): PartialChangeProducer<ForumDetailUiIntent, ForumDetailPartialChange, ForumDetailUiState> =
ForumDetailPartialChangeProducer
private object ForumDetailPartialChangeProducer :
PartialChangeProducer<ForumDetailUiIntent, ForumDetailPartialChange, ForumDetailUiState> {
@OptIn(ExperimentalCoroutinesApi::class)
override fun toPartialChangeFlow(intentFlow: Flow<ForumDetailUiIntent>): Flow<ForumDetailPartialChange> =
merge(
intentFlow.filterIsInstance<ForumDetailUiIntent.Load>()
.flatMapConcat { it.producePartialChange() }
)
private fun ForumDetailUiIntent.Load.producePartialChange(): Flow<ForumDetailPartialChange.Load> =
TiebaApi.getInstance()
.getForumDetailFlow(forumId)
.map<GetForumDetailResponse, ForumDetailPartialChange.Load> {
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<ForumDetailUiState> {
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<Throwable>? = null,
val forumInfo: ImmutableHolder<RecommendForumInfo>? = null,
) : UiState

View File

@ -742,6 +742,7 @@
<string name="button_open">打开</string> <string name="button_open">打开</string>
<string name="btn_hide">隐藏</string> <string name="btn_hide">隐藏</string>
<string name="desc_search_sug">搜索联想:%s</string> <string name="desc_search_sug">搜索联想:%s</string>
<string name="title_forum_intro">本吧简介</string>
<string name="title_forum_rule">本吧吧规</string> <string name="title_forum_rule">本吧吧规</string>
<string name="desc_forum_rule">吧规</string> <string name="desc_forum_rule">吧规</string>
</resources> </resources>