feat: 话题榜

This commit is contained in:
HuanCheng65 2023-01-05 17:16:08 +08:00
parent a6c57037e8
commit 876ae60140
No known key found for this signature in database
GPG Key ID: E9031EF91A805148
16 changed files with 819 additions and 316 deletions

View File

@ -7,6 +7,7 @@ import com.huanchengfly.tieba.post.api.models.*
import com.huanchengfly.tieba.post.api.models.protos.frsPage.FrsPageResponse import com.huanchengfly.tieba.post.api.models.protos.frsPage.FrsPageResponse
import com.huanchengfly.tieba.post.api.models.protos.hotThreadList.HotThreadListResponse import com.huanchengfly.tieba.post.api.models.protos.hotThreadList.HotThreadListResponse
import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedResponse import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedResponse
import com.huanchengfly.tieba.post.api.models.protos.topicList.TopicListResponse
import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeResponse import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeResponse
import com.huanchengfly.tieba.post.api.models.web.ForumBean import com.huanchengfly.tieba.post.api.models.web.ForumBean
import com.huanchengfly.tieba.post.api.models.web.ForumHome import com.huanchengfly.tieba.post.api.models.web.ForumHome
@ -1084,6 +1085,11 @@ interface ITiebaApi {
tabCode: String tabCode: String
): Flow<HotThreadListResponse> ): Flow<HotThreadListResponse>
/**
* 话题榜
*/
fun topicListFlow(): Flow<TopicListResponse>
/** /**
* 吧页面 * 吧页面
* *

View File

@ -54,6 +54,9 @@ import com.huanchengfly.tieba.post.api.models.protos.hotThreadList.HotThreadList
import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedRequest import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedRequest
import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedRequestData import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedRequestData
import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedResponse import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedResponse
import com.huanchengfly.tieba.post.api.models.protos.topicList.TopicListRequest
import com.huanchengfly.tieba.post.api.models.protos.topicList.TopicListRequestData
import com.huanchengfly.tieba.post.api.models.protos.topicList.TopicListResponse
import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeRequest import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeRequest
import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeRequestData import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeRequestData
import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeResponse import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeResponse
@ -798,6 +801,22 @@ object MixedTiebaApiImpl : ITiebaApi {
) )
} }
override fun topicListFlow(): Flow<TopicListResponse> {
return RetrofitTiebaApi.OFFICIAL_PROTOBUF_TIEBA_API.topicListFlow(
buildProtobufRequestBody(
TopicListRequest(
TopicListRequestData(
common = buildCommonRequest(),
call_from = "newbang",
list_type = "all",
need_tab_list = "0",
fid = 0L
)
)
)
)
}
override fun frsPage( override fun frsPage(
forumName: String, forumName: String,
goodClassifyId: Int? goodClassifyId: Int?

View File

@ -3,6 +3,7 @@ package com.huanchengfly.tieba.post.api.retrofit.interfaces
import com.huanchengfly.tieba.post.api.models.protos.frsPage.FrsPageResponse import com.huanchengfly.tieba.post.api.models.protos.frsPage.FrsPageResponse
import com.huanchengfly.tieba.post.api.models.protos.hotThreadList.HotThreadListResponse import com.huanchengfly.tieba.post.api.models.protos.hotThreadList.HotThreadListResponse
import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedResponse import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedResponse
import com.huanchengfly.tieba.post.api.models.protos.topicList.TopicListResponse
import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeResponse import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeResponse
import com.huanchengfly.tieba.post.api.retrofit.body.MyMultipartBody import com.huanchengfly.tieba.post.api.retrofit.body.MyMultipartBody
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -25,6 +26,11 @@ interface OfficialProtobufTiebaApi {
@Body body: MyMultipartBody, @Body body: MyMultipartBody,
): Flow<HotThreadListResponse> ): Flow<HotThreadListResponse>
@POST("/c/f/recommend/topicList?cmd=309289")
fun topicListFlow(
@Body body: MyMultipartBody,
): Flow<TopicListResponse>
@POST("/c/f/frs/page?cmd=301001") @POST("/c/f/frs/page?cmd=301001")
fun frsPageFlow( fun frsPageFlow(
@Body body: MyMultipartBody, @Body body: MyMultipartBody,

View File

@ -0,0 +1,230 @@
package com.huanchengfly.tieba.post.ui.page.hottopic.list
import android.graphics.Typeface
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha
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.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.insets.ui.Scaffold
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.protos.topicList.NewTopicList
import com.huanchengfly.tieba.post.arch.collectPartialAsState
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.OrangeA700
import com.huanchengfly.tieba.post.ui.common.theme.compose.RedA700
import com.huanchengfly.tieba.post.ui.common.theme.compose.Shapes
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.widgets.compose.BackNavigationIcon
import com.huanchengfly.tieba.post.ui.widgets.compose.NetworkImage
import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar
import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@Composable
private fun TopicImage(
index: Int,
imageUri: String
) {
val boxModifier = if (index < 3) {
Modifier
.fillMaxWidth()
.aspectRatio(2.39f)
.clip(Shapes.medium)
} else {
Modifier
.size(Sizes.Medium)
.aspectRatio(1f)
.clip(Shapes.small)
}
Box(
modifier = boxModifier
) {
NetworkImage(
imageUri = imageUri,
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.Crop
)
Text(
text = "${index + 1}",
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.background,
fontFamily = FontFamily(
Typeface.createFromAsset(
LocalContext.current.assets,
"bebas.ttf"
)
),
modifier = Modifier
.background(
when (index) {
0 -> RedA700
1 -> OrangeA700
2 -> Yellow
else -> MaterialTheme.colors.onBackground.copy(ContentAlpha.medium)
}
)
.padding(4.dp)
)
}
}
@Composable
private fun TopicBody(
index: Int,
item: NewTopicList
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = item.topic_name, style = MaterialTheme.typography.subtitle1)
when (item.topic_tag) {
2 -> Text(
text = stringResource(id = R.string.topic_tag_hot),
fontSize = 10.sp,
color = White,
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.background(RedA700)
.padding(vertical = 2.dp, horizontal = 4.dp)
)
1 -> Text(
text = stringResource(id = R.string.topic_tag_new),
fontSize = 10.sp,
color = White,
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.background(OrangeA700)
.padding(vertical = 2.dp, horizontal = 4.dp)
)
}
}
Text(
text = item.topic_desc,
maxLines = if (index < 3) 3 else 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2
)
Text(
text = stringResource(id = R.string.hot_num, item.discuss_num.getShortNumString()),
style = MaterialTheme.typography.caption,
color = ExtendedTheme.colors.textSecondary
)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Destination
@Composable
fun HotTopicListPage(
viewModel: HotTopicListViewModel = pageViewModel<HotTopicListUiIntent, HotTopicListViewModel>(
listOf(HotTopicListUiIntent.Load)
),
navigator: DestinationsNavigator,
) {
val isRefreshing by viewModel.uiState.collectPartialAsState(
prop1 = HotTopicListUiState::isRefreshing,
initial = true
)
val topicList by viewModel.uiState.collectPartialAsState(
prop1 = HotTopicListUiState::topicList,
initial = emptyList()
)
Scaffold(
backgroundColor = Color.Transparent,
topBar = {
TitleCentredToolbar(
title = stringResource(id = R.string.title_hot_message_list),
navigationIcon = {
BackNavigationIcon(onBackPressed = { navigator.navigateUp() })
}
)
},
) { contentPaddings ->
val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing,
onRefresh = { viewModel.send(HotTopicListUiIntent.Load) }
)
Box(
modifier = Modifier
.fillMaxSize()
.padding(contentPaddings)
.pullRefresh(pullRefreshState)
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(16.dp)
) {
itemsIndexed(
items = topicList,
key = { _, item -> item.topic_id },
) { index, item ->
if (index < 3) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
TopicImage(index = index, imageUri = item.topic_image)
TopicBody(index = index, item = item)
}
} else {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TopicImage(index = index, imageUri = item.topic_image)
TopicBody(index = index, item = item)
}
}
}
}
PullRefreshIndicator(
refreshing = isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}

View File

@ -0,0 +1,77 @@
package com.huanchengfly.tieba.post.ui.page.hottopic.list
import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.protos.topicList.NewTopicList
import com.huanchengfly.tieba.post.api.models.protos.topicList.TopicListResponse
import com.huanchengfly.tieba.post.arch.BaseViewModel
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.FlowPreview
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 HotTopicListViewModel @Inject constructor() :
BaseViewModel<HotTopicListUiIntent, HotTopicListPartialChange, HotTopicListUiState, UiEvent>() {
override fun createInitialState(): HotTopicListUiState = HotTopicListUiState()
override fun createPartialChangeProducer(): PartialChangeProducer<HotTopicListUiIntent, HotTopicListPartialChange, HotTopicListUiState> =
HotTopicListPartialChangeProducer
private object HotTopicListPartialChangeProducer :
PartialChangeProducer<HotTopicListUiIntent, HotTopicListPartialChange, HotTopicListUiState> {
@OptIn(FlowPreview::class)
override fun toPartialChangeFlow(intentFlow: Flow<HotTopicListUiIntent>): Flow<HotTopicListPartialChange> =
merge(
intentFlow.filterIsInstance<HotTopicListUiIntent.Load>()
.flatMapConcat { produceLoadPartialChange() }
)
private fun produceLoadPartialChange(): Flow<HotTopicListPartialChange.Load> =
TiebaApi.getInstance().topicListFlow()
.map<TopicListResponse, HotTopicListPartialChange.Load> {
HotTopicListPartialChange.Load.Success(it.data_?.topic_list ?: emptyList())
}
.onStart { emit(HotTopicListPartialChange.Load.Start) }
.catch { emit(HotTopicListPartialChange.Load.Failure(it)) }
}
}
sealed interface HotTopicListUiIntent : UiIntent {
object Load : HotTopicListUiIntent
}
sealed interface HotTopicListPartialChange : PartialChange<HotTopicListUiState> {
sealed class Load : HotTopicListPartialChange {
override fun reduce(oldState: HotTopicListUiState): HotTopicListUiState = when (this) {
Start -> oldState.copy(isRefreshing = true)
is Success -> oldState.copy(isRefreshing = false, topicList = topicList)
is Failure -> oldState.copy(isRefreshing = false)
}
object Start : Load()
data class Success(
val topicList: List<NewTopicList>
) : Load()
data class Failure(
val error: Throwable
) : Load()
}
}
data class HotTopicListUiState(
val isRefreshing: Boolean = true,
val topicList: List<NewTopicList> = emptyList()
) : UiState

View File

@ -8,22 +8,25 @@ import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.huanchengfly.tieba.post.activities.ThreadActivity import com.huanchengfly.tieba.post.activities.ThreadActivity
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.pageViewModel 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.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.main.explore.FeedCard 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.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable @Composable
fun ConcernPage( fun ConcernPage(
viewModel: ConcernViewModel = pageViewModel() viewModel: ConcernViewModel = pageViewModel()
@ -49,10 +52,11 @@ fun ConcernPage(
prop1 = ConcernUiState::data, prop1 = ConcernUiState::data,
initial = emptyList() initial = emptyList()
) )
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isRefreshing) val pullRefreshState = rememberPullRefreshState(
SwipeRefresh( refreshing = isRefreshing,
state = swipeRefreshState, onRefresh = { viewModel.send(ConcernUiIntent.Refresh) })
onRefresh = { viewModel.send(ConcernUiIntent.Refresh) } Box(
modifier = Modifier.pullRefresh(pullRefreshState)
) { ) {
LoadMoreLayout( LoadMoreLayout(
isLoading = isLoadingMore, isLoading = isLoadingMore,
@ -100,5 +104,11 @@ fun ConcernPage(
} }
} }
} }
PullRefreshIndicator(
refreshing = isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
} }
} }

View File

@ -21,16 +21,21 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha import androidx.compose.material.ContentAlpha
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowRight import androidx.compose.material.icons.rounded.KeyboardArrowRight
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
@ -40,8 +45,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.placeholder.material.placeholder import com.google.accompanist.placeholder.material.placeholder
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
@ -51,11 +54,18 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.OrangeA700
import com.huanchengfly.tieba.post.ui.common.theme.compose.RedA700 import com.huanchengfly.tieba.post.ui.common.theme.compose.RedA700
import com.huanchengfly.tieba.post.ui.common.theme.compose.White 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.common.theme.compose.Yellow
import com.huanchengfly.tieba.post.ui.widgets.compose.* import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.HotTopicListPageDestination
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
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalGrid
import com.huanchengfly.tieba.post.ui.widgets.compose.items
import com.huanchengfly.tieba.post.ui.widgets.compose.itemsIndexed
import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Destination @Destination
@Composable @Composable
fun HotPage( fun HotPage(
@ -65,6 +75,7 @@ fun HotPage(
viewModel.send(HotUiIntent.Load) viewModel.send(HotUiIntent.Load)
viewModel.initialized = true viewModel.initialized = true
} }
val navigator = LocalNavigator.current
val isLoading by viewModel.uiState.collectPartialAsState( val isLoading by viewModel.uiState.collectPartialAsState(
prop1 = HotUiState::isRefreshing, prop1 = HotUiState::isRefreshing,
initial = true initial = true
@ -89,8 +100,10 @@ fun HotPage(
prop1 = HotUiState::isLoadingThreadList, prop1 = HotUiState::isLoadingThreadList,
initial = false initial = false
) )
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isLoading) val pullRefreshState = rememberPullRefreshState(
SwipeRefresh(state = swipeRefreshState, onRefresh = { viewModel.send(HotUiIntent.Load) }) { refreshing = isLoading,
onRefresh = { viewModel.send(HotUiIntent.Load) })
Box(modifier = Modifier.pullRefresh(pullRefreshState)) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
@ -170,6 +183,9 @@ fun HotPage(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable {
navigator.navigate(HotTopicListPageDestination)
}
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
) { ) {
Text( Text(
@ -250,6 +266,12 @@ fun HotPage(
} }
} }
} }
PullRefreshIndicator(
refreshing = isLoading,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
} }
} }
@ -344,7 +366,8 @@ private fun ThreadListItem(
NetworkImage( NetworkImage(
imageUri = item.media.first().dynamicPic, imageUri = item.media.first().dynamicPic,
contentDescription = null, contentDescription = null,
modifier = heightModifier.aspectRatio(16f / 9) modifier = heightModifier.aspectRatio(16f / 9),
contentScale = ContentScale.Crop
) )
} }
} }

View File

@ -0,0 +1,158 @@
package com.huanchengfly.tieba.post.ui.page.main.explore.personalized
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.protos.personalized.DislikeReason
import com.huanchengfly.tieba.post.api.models.protos.personalized.ThreadPersonalized
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.widgets.compose.ClickMenu
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalGrid
import com.huanchengfly.tieba.post.ui.widgets.compose.items
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
@Composable
fun Dislike(
personalized: ThreadPersonalized,
onDislike: (clickTime: Long, reasons: List<DislikeReason>) -> Unit,
) {
var clickTime by remember { mutableStateOf(0L) }
val selectedReasons = remember { mutableStateListOf<DislikeReason>() }
val menuState = rememberMenuState()
ClickMenu(
menuState = menuState,
menuContent = {
DisposableEffect(personalized) {
clickTime = System.currentTimeMillis()
onDispose {
selectedReasons.clear()
}
}
ConstraintLayout(
modifier = Modifier.padding(vertical = 8.dp)
) {
val (title, grid) = createRefs()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.constrainAs(title) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(parent.top)
}
.padding(horizontal = 16.dp),
) {
Text(
text = stringResource(id = R.string.title_dislike),
style = MaterialTheme.typography.subtitle1,
modifier = Modifier.weight(1f),
)
Spacer(modifier = Modifier.width(32.dp))
Text(
text = stringResource(id = R.string.button_submit_dislike),
modifier = Modifier
.clip(RoundedCornerShape(6.dp))
.background(color = ExtendedTheme.colors.accent)
.clickable {
dismiss()
onDislike(clickTime, selectedReasons)
}
.padding(vertical = 4.dp, horizontal = 8.dp),
color = ExtendedTheme.colors.onAccent,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.subtitle2,
)
}
VerticalGrid(
column = 2,
modifier = Modifier
.constrainAs(grid) {
start.linkTo(title.start)
end.linkTo(title.end)
top.linkTo(title.bottom, 16.dp)
bottom.linkTo(parent.bottom)
}
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(
items = personalized.dislikeResource,
span = { if (it.dislikeId == 7) 2 else 1 }
) {
val backgroundColor by animateColorAsState(
targetValue = if (selectedReasons.contains(it)) ExtendedTheme.colors.accent else ExtendedTheme.colors.chip
)
val contentColor by animateColorAsState(
targetValue = if (selectedReasons.contains(it)) ExtendedTheme.colors.onAccent else ExtendedTheme.colors.onChip
)
Text(
text = it.dislikeReason,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.background(color = backgroundColor)
.clickable {
if (selectedReasons.contains(it)) {
selectedReasons.remove(it)
} else {
selectedReasons.add(it)
}
}
.padding(vertical = 8.dp, horizontal = 16.dp),
color = contentColor,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.subtitle2,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
},
) {
IconButton(
onClick = { menuState.expanded = true },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null,
tint = ExtendedTheme.colors.textSecondary
)
}
}
}

View File

@ -1,44 +1,64 @@
package com.huanchengfly.tieba.post.ui.page.main.explore.personalized package com.huanchengfly.tieba.post.ui.page.main.explore.personalized
import androidx.compose.animation.* import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* 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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.* import androidx.compose.material.pullrefresh.PullRefreshIndicator
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.ThreadActivity import com.huanchengfly.tieba.post.activities.ThreadActivity
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.pageViewModel 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.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.main.explore.Dislike import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
import com.huanchengfly.tieba.post.ui.page.main.explore.FeedCard
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.LoadMoreLayout import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable @Composable
fun PersonalizedPage( fun PersonalizedPage(
viewModel: PersonalizedViewModel = pageViewModel() viewModel: PersonalizedViewModel = pageViewModel()
@ -76,7 +96,10 @@ fun PersonalizedPage(
prop1 = PersonalizedUiState::hiddenThreadIds, prop1 = PersonalizedUiState::hiddenThreadIds,
initial = emptyList() initial = emptyList()
) )
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isRefreshing) val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing,
onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) }
)
val lazyStaggeredGridState = rememberLazyStaggeredGridState() val lazyStaggeredGridState = rememberLazyStaggeredGridState()
var refreshCount by remember { var refreshCount by remember {
mutableStateOf(0) mutableStateOf(0)
@ -99,103 +122,107 @@ fun PersonalizedPage(
} }
} }
Box { Box(modifier = Modifier.pullRefresh(pullRefreshState)) {
SwipeRefresh( LoadMoreLayout(
state = swipeRefreshState, isLoading = isLoadingMore,
onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) }, loadEnd = false,
onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) },
) { ) {
LoadMoreLayout( LazyVerticalStaggeredGrid(
isLoading = isLoadingMore, columns = StaggeredGridCells.Adaptive(240.dp),
loadEnd = false, state = lazyStaggeredGridState
onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) },
) { ) {
LazyVerticalStaggeredGrid( itemsIndexed(
columns = StaggeredGridCells.Adaptive(240.dp), items = data,
state = lazyStaggeredGridState key = { _, item -> "${item.id}" },
) { contentType = { _, item ->
itemsIndexed( when {
items = data, item.videoInfo != null -> "Video"
key = { _, item -> "${item.id}" }, item.media.isNotEmpty() -> "Media"
contentType = { _, item -> else -> "PlainText"
when {
item.videoInfo != null -> "Video"
item.media.isNotEmpty() -> "Media"
else -> "PlainText"
}
} }
) { index, item -> }
Column { ) { index, item ->
AnimatedVisibility( Column {
visible = !hiddenThreadIds.contains(item.threadId), AnimatedVisibility(
enter = EnterTransition.None, visible = !hiddenThreadIds.contains(item.threadId),
exit = shrinkVertically() + fadeOut() enter = EnterTransition.None,
exit = shrinkVertically() + fadeOut()
) {
FeedCard(
item = item,
onClick = {
ThreadActivity.launch(context, item.threadId.toString())
},
onAgree = {
viewModel.send(
PersonalizedUiIntent.Agree(
item.threadId,
item.firstPostId,
item.agree?.hasAgree ?: 0
)
)
},
) { ) {
FeedCard( Dislike(
item = item, personalized = threadPersonalizedData[index],
onClick = { onDislike = { clickTime, reasons ->
ThreadActivity.launch(context, item.threadId.toString())
},
onAgree = {
viewModel.send( viewModel.send(
PersonalizedUiIntent.Agree( PersonalizedUiIntent.Dislike(
item.forumInfo?.id ?: 0,
item.threadId, item.threadId,
item.firstPostId, reasons,
item.agree?.hasAgree ?: 0 clickTime
) )
) )
}, }
) { )
Dislike(
personalized = threadPersonalizedData[index],
onDislike = { clickTime, reasons ->
viewModel.send(
PersonalizedUiIntent.Dislike(
item.forumInfo?.id ?: 0,
item.threadId,
reasons,
clickTime
)
)
}
)
}
} }
if (!hiddenThreadIds.contains(item.threadId)) { }
if ((refreshPosition == 0 || index + 1 != refreshPosition) && index < data.size - 1) { if (!hiddenThreadIds.contains(item.threadId)) {
Divider( if ((refreshPosition == 0 || index + 1 != refreshPosition) && index < data.size - 1) {
color = ExtendedTheme.colors.divider, Divider(
modifier = Modifier.padding(horizontal = 16.dp), color = ExtendedTheme.colors.divider,
thickness = 2.dp modifier = Modifier.padding(horizontal = 16.dp),
) thickness = 2.dp
} )
} }
if (refreshPosition != 0 && index + 1 == refreshPosition) { }
Row( if (refreshPosition != 0 && index + 1 == refreshPosition) {
horizontalArrangement = Arrangement.Center, Row(
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center,
modifier = Modifier verticalAlignment = Alignment.CenterVertically,
.fillMaxWidth() modifier = Modifier
.clickable { viewModel.send(PersonalizedUiIntent.Refresh) } .fillMaxWidth()
.padding(8.dp), .clickable { viewModel.send(PersonalizedUiIntent.Refresh) }
) { .padding(8.dp),
Icon( ) {
imageVector = Icons.Rounded.Refresh, Icon(
contentDescription = null imageVector = Icons.Rounded.Refresh,
) contentDescription = null
Spacer(modifier = Modifier.width(16.dp)) )
Text(text = stringResource(id = R.string.tip_refresh), style = MaterialTheme.typography.subtitle1) Spacer(modifier = Modifier.width(16.dp))
} Text(
text = stringResource(id = R.string.tip_refresh),
style = MaterialTheme.typography.subtitle1
)
} }
} }
} }
} }
LaunchedEffect(data.firstOrNull()?.id) { }
//delay(50) LaunchedEffect(data.firstOrNull()?.id) {
lazyStaggeredGridState.scrollToItem(0, 0) //delay(50)
} lazyStaggeredGridState.scrollToItem(0, 0)
} }
} }
PullRefreshIndicator(
refreshing = isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
AnimatedVisibility( AnimatedVisibility(
visible = showRefreshTip, visible = showRefreshTip,
enter = fadeIn() + slideInVertically(), enter = fadeIn() + slideInVertically(),

View File

@ -25,6 +25,7 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenuItem import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.Surface import androidx.compose.material.Surface
@ -33,6 +34,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ViewAgenda import androidx.compose.material.icons.outlined.ViewAgenda
import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -40,6 +44,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -55,8 +60,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.google.accompanist.placeholder.placeholder import com.google.accompanist.placeholder.placeholder
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.ForumActivity import com.huanchengfly.tieba.post.activities.ForumActivity
import com.huanchengfly.tieba.post.activities.NewSearchActivity import com.huanchengfly.tieba.post.activities.NewSearchActivity
@ -69,6 +72,7 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.widgets.Chip import com.huanchengfly.tieba.post.ui.widgets.Chip
import com.huanchengfly.tieba.post.ui.widgets.compose.AccountNavIconIfCompact import com.huanchengfly.tieba.post.ui.widgets.compose.AccountNavIconIfCompact
import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
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.LongClickMenu import com.huanchengfly.tieba.post.ui.widgets.compose.LongClickMenu
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
@ -307,30 +311,7 @@ private fun ForumItem(
) { ) {
AnimatedVisibility(visible = showAvatar) { AnimatedVisibility(visible = showAvatar) {
Row { Row {
coil.compose.AsyncImage( Avatar(data = item.avatar, size = 40.dp, contentDescription = null)
model = item.avatar,
contentDescription = null,
modifier = Modifier
.clip(CircleShape)
.size(40.dp)
.align(CenterVertically),
)
// AsyncImage(
// imageUri = item.avatar,
// contentDescription = null,
// modifier = Modifier
// .clip(CircleShape)
// .size(40.dp)
// .align(CenterVertically),
// ) {
// resultCachePolicy(CachePolicy.DISABLED)
// memoryCachePolicy(CachePolicy.DISABLED)
// disallowReuseBitmap()
// }
// {
// placeholder(ImageUtil.getPlaceHolder(context, 0))
// crossfade()
// }
Spacer(modifier = Modifier.width(14.dp)) Spacer(modifier = Modifier.width(14.dp))
} }
} }
@ -382,6 +363,7 @@ private fun ForumItem(
} }
} }
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun HomePage( fun HomePage(
viewModel: HomeViewModel = pageViewModel<HomeUiIntent, HomeViewModel>( viewModel: HomeViewModel = pageViewModel<HomeUiIntent, HomeViewModel>(
@ -431,11 +413,12 @@ fun HomePage(
}, },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { contentPaddings -> ) { contentPaddings ->
SwipeRefresh( val pullRefreshState = rememberPullRefreshState(
state = rememberSwipeRefreshState(isRefreshing = isLoading), refreshing = isLoading,
onRefresh = { viewModel.send(HomeUiIntent.Refresh) }, onRefresh = { viewModel.send(HomeUiIntent.Refresh) })
modifier = Modifier.padding(contentPaddings) Box(modifier = Modifier
) { .padding(contentPaddings)
.pullRefresh(pullRefreshState)) {
val gridState = rememberLazyGridState() val gridState = rememberLazyGridState()
LazyVerticalGrid( LazyVerticalGrid(
state = gridState, state = gridState,
@ -512,7 +495,10 @@ fun HomePage(
Header( Header(
text = stringResource(id = R.string.forum_list_title), text = stringResource(id = R.string.forum_list_title),
invert = true, invert = true,
modifier = Modifier.placeholder(visible = true, color = ExtendedTheme.colors.chip) modifier = Modifier.placeholder(
visible = true,
color = ExtendedTheme.colors.chip
)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
@ -522,6 +508,12 @@ fun HomePage(
} }
} }
} }
PullRefreshIndicator(
refreshing = isLoading,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
} }
} }
} }

View File

@ -19,7 +19,8 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.github.panpf.sketch.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.huanchengfly.tieba.post.utils.ImageUtil import com.huanchengfly.tieba.post.utils.ImageUtil
@ -83,16 +84,22 @@ fun Avatar(
is String? -> { is String? -> {
AsyncImage( AsyncImage(
imageUri = data, model = ImageRequest.Builder(LocalContext.current)
.data(data)
.crossfade(true)
.build(),
contentDescription = contentDescription, contentDescription = contentDescription,
placeholder = rememberDrawablePainter(
drawable = ImageUtil.getPlaceHolder(
context,
0
)
),
modifier = modifier modifier = modifier
.size(size) .size(size)
.clip(CircleShape), .clip(CircleShape),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) { )
placeholder(ImageUtil.getPlaceHolder(context, 0))
crossfade()
}
} }
else -> throw IllegalArgumentException("不支持该类型") else -> throw IllegalArgumentException("不支持该类型")

View File

@ -1,4 +1,4 @@
package com.huanchengfly.tieba.post.ui.page.main.explore package com.huanchengfly.tieba.post.ui.widgets.compose
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -16,23 +16,17 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentColor import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Favorite import androidx.compose.material.icons.rounded.Favorite
import androidx.compose.material.icons.rounded.FavoriteBorder import androidx.compose.material.icons.rounded.FavoriteBorder
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.PhotoSizeSelectActual import androidx.compose.material.icons.rounded.PhotoSizeSelectActual
import androidx.compose.material.icons.rounded.SwapCalls import androidx.compose.material.icons.rounded.SwapCalls
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -42,13 +36,10 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.constraintlayout.compose.ConstraintLayout
import cn.jzvd.Jzvd import cn.jzvd.Jzvd
import com.github.panpf.sketch.displayImage import com.github.panpf.sketch.displayImage
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
@ -56,19 +47,9 @@ import com.huanchengfly.tieba.post.activities.ForumActivity
import com.huanchengfly.tieba.post.activities.UserActivity import com.huanchengfly.tieba.post.activities.UserActivity
import com.huanchengfly.tieba.post.api.models.protos.Media import com.huanchengfly.tieba.post.api.models.protos.Media
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.api.models.protos.personalized.DislikeReason
import com.huanchengfly.tieba.post.api.models.protos.personalized.ThreadPersonalized
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.utils.getPhotoViewData import com.huanchengfly.tieba.post.ui.utils.getPhotoViewData
import com.huanchengfly.tieba.post.ui.widgets.VideoPlayerStandard import com.huanchengfly.tieba.post.ui.widgets.VideoPlayerStandard
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
import com.huanchengfly.tieba.post.ui.widgets.compose.ClickMenu
import com.huanchengfly.tieba.post.ui.widgets.compose.NetworkImage
import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalGrid
import com.huanchengfly.tieba.post.ui.widgets.compose.items
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
import com.huanchengfly.tieba.post.utils.DateTimeUtils import com.huanchengfly.tieba.post.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.ImageUtil import com.huanchengfly.tieba.post.utils.ImageUtil
import com.huanchengfly.tieba.post.utils.StringUtil import com.huanchengfly.tieba.post.utils.StringUtil
@ -79,142 +60,6 @@ private val Media.url: String
@Composable get() = @Composable get() =
ImageUtil.getUrl(LocalContext.current, true, originPic, dynamicPic, bigPic, srcPic) ImageUtil.getUrl(LocalContext.current, true, originPic, dynamicPic, bigPic, srcPic)
@Composable
fun VideoPlayer(
videoUrl: String,
thumbnailUrl: String,
title: String = "",
modifier: Modifier = Modifier
) {
AndroidView(
factory = { context ->
VideoPlayerStandard(context)
},
modifier = modifier
) {
it.setUp(videoUrl, title)
it.posterImageView.displayImage(thumbnailUrl)
}
DisposableEffect(videoUrl) {
onDispose {
Jzvd.releaseAllVideos()
}
}
}
@Composable
fun Dislike(
personalized: ThreadPersonalized,
onDislike: (clickTime: Long, reasons: List<DislikeReason>) -> Unit,
) {
var clickTime by remember { mutableStateOf(0L) }
val selectedReasons = remember { mutableStateListOf<DislikeReason>() }
val menuState = rememberMenuState()
ClickMenu(
menuState = menuState,
menuContent = {
DisposableEffect(personalized) {
clickTime = System.currentTimeMillis()
onDispose {
selectedReasons.clear()
}
}
ConstraintLayout(
modifier = Modifier.padding(vertical = 8.dp)
) {
val (title, grid) = createRefs()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.constrainAs(title) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(parent.top)
}
.padding(horizontal = 16.dp),
) {
Text(
text = stringResource(id = R.string.title_dislike),
style = MaterialTheme.typography.subtitle1,
modifier = Modifier.weight(1f),
)
Spacer(modifier = Modifier.width(32.dp))
Text(
text = stringResource(id = R.string.button_submit_dislike),
modifier = Modifier
.clip(RoundedCornerShape(6.dp))
.background(color = ExtendedTheme.colors.accent)
.clickable {
dismiss()
onDislike(clickTime, selectedReasons)
}
.padding(vertical = 4.dp, horizontal = 8.dp),
color = ExtendedTheme.colors.onAccent,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.subtitle2,
)
}
VerticalGrid(
column = 2,
modifier = Modifier
.constrainAs(grid) {
start.linkTo(title.start)
end.linkTo(title.end)
top.linkTo(title.bottom, 16.dp)
bottom.linkTo(parent.bottom)
}
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(
items = personalized.dislikeResource,
span = { if (it.dislikeId == 7) 2 else 1 }
) {
val backgroundColor by animateColorAsState(
targetValue = if (selectedReasons.contains(it)) ExtendedTheme.colors.accent else ExtendedTheme.colors.chip
)
val contentColor by animateColorAsState(
targetValue = if (selectedReasons.contains(it)) ExtendedTheme.colors.onAccent else ExtendedTheme.colors.onChip
)
Text(
text = it.dislikeReason,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.background(color = backgroundColor)
.clickable {
if (selectedReasons.contains(it)) {
selectedReasons.remove(it)
} else {
selectedReasons.add(it)
}
}
.padding(vertical = 8.dp, horizontal = 16.dp),
color = contentColor,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.subtitle2,
)
}
}
}
},
) {
IconButton(
onClick = { menuState.expanded = true },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null,
tint = ExtendedTheme.colors.textSecondary
)
}
}
}
@Composable @Composable
fun FeedCard( fun FeedCard(
item: ThreadInfo, item: ThreadInfo,
@ -432,4 +277,27 @@ private fun ActionBtn(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(text = text, style = MaterialTheme.typography.caption, color = animatedColor) Text(text = text, style = MaterialTheme.typography.caption, color = animatedColor)
} }
}
@Composable
fun VideoPlayer(
videoUrl: String,
thumbnailUrl: String,
title: String = "",
modifier: Modifier = Modifier
) {
AndroidView(
factory = { context ->
VideoPlayerStandard(context)
},
modifier = modifier
) {
it.setUp(videoUrl, title)
it.posterImageView.displayImage(thumbnailUrl)
}
DisposableEffect(videoUrl) {
onDispose {
Jzvd.releaseAllVideos()
}
}
} }

View File

@ -8,7 +8,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import com.github.panpf.sketch.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.goToActivity
import com.huanchengfly.tieba.post.models.protos.PhotoViewData import com.huanchengfly.tieba.post.models.protos.PhotoViewData
import com.huanchengfly.tieba.post.ui.page.photoview.PhotoViewActivity import com.huanchengfly.tieba.post.ui.page.photoview.PhotoViewActivity
@ -37,11 +39,13 @@ fun NetworkImage(
} }
} else Modifier } else Modifier
AsyncImage( AsyncImage(
imageUri = imageUri, model = ImageRequest.Builder(LocalContext.current)
.data(imageUri)
.crossfade(true)
.build(),
contentDescription = contentDescription, contentDescription = contentDescription,
placeholder = rememberDrawablePainter(drawable = ImageUtil.getPlaceHolder(context, 0)),
modifier = modifier.then(clickableModifier), modifier = modifier.then(clickableModifier),
contentScale = contentScale, contentScale = contentScale,
) { )
placeholder(ImageUtil.getPlaceHolder(context, 0))
}
} }

View File

@ -42,7 +42,6 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.github.panpf.sketch.compose.AsyncImage
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.LoginActivity import com.huanchengfly.tieba.post.activities.LoginActivity
@ -52,7 +51,6 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass.Companion.Compact import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass.Companion.Compact
import com.huanchengfly.tieba.post.utils.AccountUtil import com.huanchengfly.tieba.post.utils.AccountUtil
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
import com.huanchengfly.tieba.post.utils.ImageUtil
import com.huanchengfly.tieba.post.utils.StringUtil import com.huanchengfly.tieba.post.utils.StringUtil
import com.huanchengfly.tieba.post.utils.compose.calcStatusBarColor import com.huanchengfly.tieba.post.utils.compose.calcStatusBarColor
@ -136,16 +134,11 @@ fun AccountNavIcon(
onClick = onClick, onClick = onClick,
shape = CircleShape shape = CircleShape
) { ) {
AsyncImage( Avatar(
imageUri = StringUtil.getAvatarUrl(currentAccount.portrait), data = StringUtil.getAvatarUrl(currentAccount.portrait),
contentDescription = stringResource(id = R.string.title_switch_account_long_press), size = size,
modifier = Modifier contentDescription = stringResource(id = R.string.title_switch_account_long_press)
.clip(CircleShape) )
.size(size),
) {
placeholder(ImageUtil.getPlaceHolder(context, 0))
crossfade()
}
} }
} }
} }

View File

@ -0,0 +1,83 @@
syntax = "proto3";
package protos;
option java_package = "com.huanchengfly.tieba.post.api.models.protos.topicList";
import "Common.proto";
import "CommonRequest.proto";
import "ThreadInfo.proto";
message TopicListRequestData {
CommonRequest common = 1;
string call_from = 2;
string list_type = 3;
string need_tab_list = 4;
int64 fid = 5;
}
message TopicListRequest {
TopicListRequestData data = 1;
}
message TopicListModule {
string module_title = 1;
repeated TopicList topic_list = 2;
string tips = 3;
string rule_jump_url = 4;
}
message MediaTopic {
uint64 topic_id = 1;
string topic_name = 2;
VideoInfo video_info = 3;
string pic_url = 4;
}
message TabList {
string tab_name = 1;
string tab_type = 2;
string share_pic = 3;
string share_title = 4;
string share_desc = 5;
string share_url = 6;
}
message TopicList {
uint64 topic_id = 1;
string topic_name = 2;
uint64 discuss_num = 3;
int32 tag = 4;
string topic_desc = 5;
string topic_pic = 6;
int64 update_time = 7;
string topic_user_name = 8;
repeated Media media = 9;
int64 topic_tid = 10;
string topic_h5_url = 11;
VideoInfo video_info = 12;
int32 topic_thread_types = 13;
}
message NewTopicList {
int64 topic_id = 1;
string topic_name = 2;
string topic_desc = 3;
int64 discuss_num = 4;
string topic_image = 5;
int32 topic_tag = 6;
}
message TopicListResponseData {
TopicListModule topic_bang = 1;
TopicListModule topic_manual = 2;
MediaTopic media_topic = 3;
repeated TabList tab_list = 6;
repeated TopicList frs_tab_topic = 7;
repeated NewTopicList topic_list = 8;
}
message TopicListResponse {
Error error = 1;
TopicListResponseData data = 2;
}

View File

@ -276,7 +276,7 @@
<string name="title_dialog_unfollow_forum">确定要取消关注%s吧吗</string> <string name="title_dialog_unfollow_forum">确定要取消关注%s吧吗</string>
<string name="menu_save_audio">保存语音</string> <string name="menu_save_audio">保存语音</string>
<string name="title_hot_message">贴吧话题</string> <string name="title_hot_message">贴吧话题</string>
<string name="title_hot_message_list">热议话题</string> <string name="title_hot_message_list">话题</string>
<string name="desc_hot_message_list">(每小时更新一次,以实时讨论量排序)</string> <string name="desc_hot_message_list">(每小时更新一次,以实时讨论量排序)</string>
<string name="toast_photo_saved">图片保存在 %1$s</string> <string name="toast_photo_saved">图片保存在 %1$s</string>
<string name="title_settings_default_sort_type">吧默认排序方式</string> <string name="title_settings_default_sort_type">吧默认排序方式</string>