feat: 热榜页面贴子卡片 & 点赞

This commit is contained in:
HuanCheng65 2023-07-18 22:19:25 +08:00
parent dff92beadc
commit a5c32056c0
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
5 changed files with 228 additions and 29 deletions

View File

@ -57,6 +57,12 @@ val ThreadInfo.abstractText: String
}
}
val ThreadInfo.hasAgree: Int
get() = agree?.hasAgree ?: 0
val ThreadInfo.hasAgreed: Boolean
get() = hasAgree == 1
val ThreadInfo.hasAbstract: Boolean
get() = richAbstract.any { (it.type == 0 && it.text.isNotBlank()) || it.type == 2 }

View File

@ -31,6 +31,7 @@ import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
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
@ -45,7 +46,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.placeholder.material.placeholder
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.hasAgree
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onEvent
import com.huanchengfly.tieba.post.arch.pageViewModel
@ -56,7 +59,9 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.White
import com.huanchengfly.tieba.post.ui.common.theme.compose.Yellow
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.HotTopicListPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.NetworkImage
import com.huanchengfly.tieba.post.ui.widgets.compose.ProvideContentColor
@ -155,12 +160,12 @@ fun HotPage(
modifier = Modifier.padding(bottom = 2.dp)
)
Text(
text = item.topicName,
text = item.get { topicName },
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
when (item.tag) {
when (item.get { tag }) {
2 -> Text(
text = stringResource(id = R.string.topic_tag_hot),
fontSize = 10.sp,
@ -220,7 +225,7 @@ fun HotPage(
}
if (threadList.isNotEmpty()) {
if (tabList.isNotEmpty()) {
stickyHeader(key = "ThreadTabs") {
item(key = "ThreadTabs") {
VerticalGrid(
column = 5,
modifier = Modifier
@ -239,9 +244,9 @@ fun HotPage(
}
items(tabList) {
ThreadListTab(
text = it.tabName,
selected = currentTabCode == it.tabCode,
onSelected = { viewModel.send(HotUiIntent.RefreshThreadList(it.tabCode)) }
text = it.get { tabName },
selected = currentTabCode == it.get { tabCode },
onSelected = { viewModel.send(HotUiIntent.RefreshThreadList(it.get { tabCode })) }
)
}
}
@ -266,8 +271,68 @@ fun HotPage(
} else {
itemsIndexed(
items = threadList,
key = { _, item -> "Thread_${item.threadId}" }) { index, item ->
ThreadListItem(index = index, item = item)
key = { _, item -> "Thread_${item.get { threadId }}" }
) { index, item ->
FeedCard(
item = item,
onClick = {
navigator.navigate(
ThreadPageDestination(
threadId = it.id,
threadInfo = it
)
)
},
onAgree = {
viewModel.send(
HotUiIntent.Agree(
threadId = it.threadId,
postId = it.firstPostId,
hasAgree = it.hasAgree
)
)
},
) {
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
val color = when (index) {
0 -> RedA700
1 -> OrangeA700
2 -> Yellow
else -> MaterialTheme.colors.onBackground.copy(
ContentAlpha.medium
)
}
Text(
text = "${index + 1}",
color = color,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
Text(
text = stringResource(
id = R.string.hot_num,
item.get { hotNum }.getShortNumString()
),
style = MaterialTheme.typography.caption,
color = color
)
}
}
// ThreadListItem(
// index = index,
// itemHolder = item,
// onClick = {
// navigator.navigate(
// ThreadPageDestination(
// threadId = it.id,
// threadInfo = it
// )
// )
// }
// )
}
}
}
@ -322,13 +387,14 @@ private fun ThreadListItemPlaceholder() {
@Composable
private fun ThreadListItem(
index: Int,
item: ThreadInfo,
onClick: () -> Unit = {}
itemHolder: ImmutableHolder<ThreadInfo>,
onClick: (ThreadInfo) -> Unit = {}
) {
val item = remember(itemHolder) { itemHolder.get() }
val heightModifier = if (item.media.isEmpty()) Modifier else Modifier.height(80.dp)
Row(
modifier = Modifier
.clickable(onClick = onClick)
.clickable { onClick(item) }
.padding(all = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
@ -368,7 +434,14 @@ private fun ThreadListItem(
)
}
}
if (item.media.isNotEmpty()) {
if (!item.videoInfo?.thumbnailUrl.isNullOrBlank()) {
NetworkImage(
imageUri = item.videoInfo?.thumbnailUrl!!,
contentDescription = null,
modifier = heightModifier.aspectRatio(16f / 9),
contentScale = ContentScale.Crop
)
} else if (!item.media.firstOrNull()?.dynamicPic.isNullOrBlank()) {
NetworkImage(
imageUri = item.media.first().dynamicPic,
contentDescription = null,
@ -397,7 +470,7 @@ private fun ThreadListTab(
.clip(RoundedCornerShape(100))
.background(backgroundColor)
.clickable(onClick = onSelected)
.padding(horizontal = 16.dp, vertical = 4.dp),
.padding(vertical = 4.dp),
fontSize = 12.sp,
fontWeight = FontWeight.Bold
)

View File

@ -2,17 +2,22 @@ package com.huanchengfly.tieba.post.ui.page.main.explore.hot
import androidx.compose.runtime.Stable
import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.AgreeBean
import com.huanchengfly.tieba.post.api.models.protos.FrsTabInfo
import com.huanchengfly.tieba.post.api.models.protos.RecommendTopicList
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.api.models.protos.hotThreadList.HotThreadListResponse
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.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
@ -41,6 +46,8 @@ class HotViewModel @Inject constructor() :
.flatMapConcat { produceLoadPartialChange() },
intentFlow.filterIsInstance<HotUiIntent.RefreshThreadList>()
.flatMapConcat { it.producePartialChange() },
intentFlow.filterIsInstance<HotUiIntent.Agree>()
.flatMapConcat { it.producePartialChange() },
)
private fun produceLoadPartialChange(): Flow<HotPartialChange.Load> =
@ -65,6 +72,20 @@ class HotViewModel @Inject constructor() :
}
.onStart { emit(HotPartialChange.RefreshThreadList.Start(tabCode)) }
.catch { emit(HotPartialChange.RefreshThreadList.Failure(tabCode, it)) }
private fun HotUiIntent.Agree.producePartialChange(): Flow<HotPartialChange.Agree> =
TiebaApi.getInstance()
.opAgreeFlow(
threadId.toString(), postId.toString(), hasAgree, objType = 3
)
.map<AgreeBean, HotPartialChange.Agree> {
HotPartialChange.Agree.Success(
threadId,
hasAgree xor 1
)
}
.onStart { emit(HotPartialChange.Agree.Start(threadId, hasAgree xor 1)) }
.catch { emit(HotPartialChange.Agree.Failure(threadId, hasAgree, it)) }
}
}
@ -72,6 +93,12 @@ sealed interface HotUiIntent : UiIntent {
object Load : HotUiIntent
data class RefreshThreadList(val tabCode: String) : HotUiIntent
data class Agree(
val threadId: Long,
val postId: Long,
val hasAgree: Int
) : HotUiIntent
}
sealed interface HotPartialChange : PartialChange<HotUiState> {
@ -81,9 +108,10 @@ sealed interface HotPartialChange : PartialChange<HotUiState> {
Start -> oldState.copy(isRefreshing = true)
is Success -> oldState.copy(
isRefreshing = false,
topicList = topicList,
tabList = tabList,
threadList = threadList
currentTabCode = "all",
topicList = topicList.wrapImmutable(),
tabList = tabList.wrapImmutable(),
threadList = threadList.wrapImmutable()
)
is Failure -> oldState.copy(isRefreshing = false)
@ -109,7 +137,7 @@ sealed interface HotPartialChange : PartialChange<HotUiState> {
is Success -> oldState.copy(
isLoadingThreadList = false,
currentTabCode = tabCode,
threadList = threadList
threadList = threadList.wrapImmutable()
)
is Failure -> oldState.copy(isLoadingThreadList = false)
@ -127,15 +155,105 @@ sealed interface HotPartialChange : PartialChange<HotUiState> {
val error: Throwable
) : RefreshThreadList()
}
sealed class Agree private constructor() : HotPartialChange {
private fun List<ImmutableHolder<ThreadInfo>>.updateAgreeStatus(
threadId: Long,
hasAgree: Int
): ImmutableList<ImmutableHolder<ThreadInfo>> {
return map {
val threadInfo = it.get()
if (threadInfo.threadId == threadId) {
if (threadInfo.agree != null) {
if (hasAgree != threadInfo.agree.hasAgree) {
if (hasAgree == 1) {
threadInfo.copy(
agreeNum = threadInfo.agreeNum + 1,
agree = threadInfo.agree.copy(
agreeNum = threadInfo.agree.agreeNum + 1,
diffAgreeNum = threadInfo.agree.diffAgreeNum + 1,
hasAgree = 1
)
)
} else {
threadInfo.copy(
agreeNum = threadInfo.agreeNum - 1,
agree = threadInfo.agree.copy(
agreeNum = threadInfo.agree.agreeNum - 1,
diffAgreeNum = threadInfo.agree.diffAgreeNum - 1,
hasAgree = 0
)
)
}
} else {
threadInfo
}
} else {
threadInfo.copy(
agreeNum = if (hasAgree == 1) threadInfo.agreeNum + 1 else threadInfo.agreeNum - 1
)
}
} else {
threadInfo
}
}.wrapImmutable()
}
override fun reduce(oldState: HotUiState): HotUiState =
when (this) {
is Start -> {
oldState.copy(
threadList = oldState.threadList.updateAgreeStatus(
threadId,
hasAgree
)
)
}
is Success -> {
oldState.copy(
threadList = oldState.threadList.updateAgreeStatus(
threadId,
hasAgree
)
)
}
is Failure -> {
oldState.copy(
threadList = oldState.threadList.updateAgreeStatus(
threadId,
hasAgree
)
)
}
}
data class Start(
val threadId: Long,
val hasAgree: Int
) : Agree()
data class Success(
val threadId: Long,
val hasAgree: Int
) : Agree()
data class Failure(
val threadId: Long,
val hasAgree: Int,
val error: Throwable
) : Agree()
}
}
data class HotUiState(
val isRefreshing: Boolean = true,
val currentTabCode: String = "all",
val isLoadingThreadList: Boolean = false,
val topicList: List<RecommendTopicList> = emptyList(),
val tabList: List<FrsTabInfo> = emptyList(),
val threadList: List<ThreadInfo> = emptyList(),
val topicList: ImmutableList<ImmutableHolder<RecommendTopicList>> = persistentListOf(),
val tabList: ImmutableList<ImmutableHolder<FrsTabInfo>> = persistentListOf(),
val threadList: ImmutableList<ImmutableHolder<ThreadInfo>> = persistentListOf(),
) : UiState
sealed interface HotUiEvent : UiEvent

View File

@ -114,9 +114,11 @@ class PersonalizedViewModel @Inject constructor() :
.onStart { emit(PersonalizedPartialChange.Dislike.Start(threadId)) }
private fun PersonalizedUiIntent.Agree.producePartialChange(): Flow<PersonalizedPartialChange.Agree> =
TiebaApi.getInstance().opAgreeFlow(
TiebaApi.getInstance()
.opAgreeFlow(
threadId.toString(), postId.toString(), hasAgree, objType = 3
).map<AgreeBean, PersonalizedPartialChange.Agree> {
)
.map<AgreeBean, PersonalizedPartialChange.Agree> {
PersonalizedPartialChange.Agree.Success(
threadId,
hasAgree xor 1

View File

@ -485,7 +485,7 @@ private fun ThreadShareBtn(
fun FeedCard(
item: ImmutableHolder<ThreadInfo>,
onClick: (ThreadInfo) -> Unit,
onAgree: () -> Unit,
onAgree: (ThreadInfo) -> Unit,
onClickForum: () -> Unit = {},
dislikeAction: @Composable () -> Unit = {},
) {
@ -526,7 +526,7 @@ fun FeedCard(
ThreadAgreeBtn(
hasAgree = item.get { agree?.hasAgree == 1 },
agreeNum = item.get { agreeNum },
onClick = onAgree,
onClick = { onAgree(item.get()) },
modifier = Modifier.weight(1f)
)