diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/ITiebaApi.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/ITiebaApi.kt index 58a62a20..e269dd88 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/ITiebaApi.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/ITiebaApi.kt @@ -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.hotThreadList.HotThreadListResponse 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.web.ForumBean import com.huanchengfly.tieba.post.api.models.web.ForumHome @@ -1084,6 +1085,11 @@ interface ITiebaApi { tabCode: String ): Flow + /** + * 话题榜 + */ + fun topicListFlow(): Flow + /** * 吧页面 * diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/impls/MixedTiebaApiImpl.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/impls/MixedTiebaApiImpl.kt index e643e41a..d1b1f928 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/impls/MixedTiebaApiImpl.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/impls/MixedTiebaApiImpl.kt @@ -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.PersonalizedRequestData 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.UserLikeRequestData import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeResponse @@ -798,6 +801,22 @@ object MixedTiebaApiImpl : ITiebaApi { ) } + override fun topicListFlow(): Flow { + 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( forumName: String, goodClassifyId: Int? diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialProtobufTiebaApi.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialProtobufTiebaApi.kt index 8dd5934e..8b5a70d1 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialProtobufTiebaApi.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialProtobufTiebaApi.kt @@ -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.hotThreadList.HotThreadListResponse 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.retrofit.body.MyMultipartBody import kotlinx.coroutines.flow.Flow @@ -25,6 +26,11 @@ interface OfficialProtobufTiebaApi { @Body body: MyMultipartBody, ): Flow + @POST("/c/f/recommend/topicList?cmd=309289") + fun topicListFlow( + @Body body: MyMultipartBody, + ): Flow + @POST("/c/f/frs/page?cmd=301001") fun frsPageFlow( @Body body: MyMultipartBody, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/hottopic/list/HotTopicListPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/hottopic/list/HotTopicListPage.kt new file mode 100644 index 00000000..af096ad9 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/hottopic/list/HotTopicListPage.kt @@ -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( + 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) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/hottopic/list/HotTopicListViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/hottopic/list/HotTopicListViewModel.kt new file mode 100644 index 00000000..5e0467f3 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/hottopic/list/HotTopicListViewModel.kt @@ -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() { + override fun createInitialState(): HotTopicListUiState = HotTopicListUiState() + + override fun createPartialChangeProducer(): PartialChangeProducer = + HotTopicListPartialChangeProducer + + private object HotTopicListPartialChangeProducer : + PartialChangeProducer { + @OptIn(FlowPreview::class) + override fun toPartialChangeFlow(intentFlow: Flow): Flow = + merge( + intentFlow.filterIsInstance() + .flatMapConcat { produceLoadPartialChange() } + ) + + private fun produceLoadPartialChange(): Flow = + TiebaApi.getInstance().topicListFlow() + .map { + 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 { + 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 + ) : Load() + + data class Failure( + val error: Throwable + ) : Load() + } +} + +data class HotTopicListUiState( + val isRefreshing: Boolean = true, + val topicList: List = emptyList() +) : UiState \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/concern/ConcernPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/concern/ConcernPage.kt index 104cc651..ed24fa43 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/concern/ConcernPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/concern/ConcernPage.kt @@ -8,22 +8,25 @@ import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed 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.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext 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.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.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.LoadMoreLayout -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @Composable fun ConcernPage( viewModel: ConcernViewModel = pageViewModel() @@ -49,10 +52,11 @@ fun ConcernPage( prop1 = ConcernUiState::data, initial = emptyList() ) - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isRefreshing) - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { viewModel.send(ConcernUiIntent.Refresh) } + val pullRefreshState = rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = { viewModel.send(ConcernUiIntent.Refresh) }) + Box( + modifier = Modifier.pullRefresh(pullRefreshState) ) { LoadMoreLayout( isLoading = isLoadingMore, @@ -100,5 +104,11 @@ fun ConcernPage( } } } + + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt index bcee3792..70b54924 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt @@ -21,16 +21,21 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ContentAlpha import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons 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.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +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 @@ -40,8 +45,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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.api.models.protos.ThreadInfo 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.White 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.ramcosta.composedestinations.annotation.Destination -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @Destination @Composable fun HotPage( @@ -65,6 +75,7 @@ fun HotPage( viewModel.send(HotUiIntent.Load) viewModel.initialized = true } + val navigator = LocalNavigator.current val isLoading by viewModel.uiState.collectPartialAsState( prop1 = HotUiState::isRefreshing, initial = true @@ -89,8 +100,10 @@ fun HotPage( prop1 = HotUiState::isLoadingThreadList, initial = false ) - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isLoading) - SwipeRefresh(state = swipeRefreshState, onRefresh = { viewModel.send(HotUiIntent.Load) }) { + val pullRefreshState = rememberPullRefreshState( + refreshing = isLoading, + onRefresh = { viewModel.send(HotUiIntent.Load) }) + Box(modifier = Modifier.pullRefresh(pullRefreshState)) { LazyColumn( modifier = Modifier .fillMaxWidth(), @@ -170,6 +183,9 @@ fun HotPage( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .fillMaxWidth() + .clickable { + navigator.navigate(HotTopicListPageDestination) + } .padding(vertical = 8.dp) ) { 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( imageUri = item.media.first().dynamicPic, contentDescription = null, - modifier = heightModifier.aspectRatio(16f / 9) + modifier = heightModifier.aspectRatio(16f / 9), + contentScale = ContentScale.Crop ) } } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/DislikeBtn.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/DislikeBtn.kt new file mode 100644 index 00000000..89eab0a7 --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/DislikeBtn.kt @@ -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) -> Unit, +) { + var clickTime by remember { mutableStateOf(0L) } + val selectedReasons = remember { mutableStateListOf() } + 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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt index 8a74b453..7dd148ae 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt @@ -1,44 +1,64 @@ 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.background 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.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons 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.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource 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.activities.ThreadActivity 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.page.main.explore.Dislike -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.LoadMoreLayout import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @Composable fun PersonalizedPage( viewModel: PersonalizedViewModel = pageViewModel() @@ -76,7 +96,10 @@ fun PersonalizedPage( prop1 = PersonalizedUiState::hiddenThreadIds, initial = emptyList() ) - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isRefreshing) + val pullRefreshState = rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) } + ) val lazyStaggeredGridState = rememberLazyStaggeredGridState() var refreshCount by remember { mutableStateOf(0) @@ -99,103 +122,107 @@ fun PersonalizedPage( } } - Box { - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) }, + Box(modifier = Modifier.pullRefresh(pullRefreshState)) { + LoadMoreLayout( + isLoading = isLoadingMore, + loadEnd = false, + onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) }, ) { - LoadMoreLayout( - isLoading = isLoadingMore, - loadEnd = false, - onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) }, + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(240.dp), + state = lazyStaggeredGridState ) { - LazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Adaptive(240.dp), - state = lazyStaggeredGridState - ) { - itemsIndexed( - items = data, - key = { _, item -> "${item.id}" }, - contentType = { _, item -> - when { - item.videoInfo != null -> "Video" - item.media.isNotEmpty() -> "Media" - else -> "PlainText" - } + itemsIndexed( + items = data, + key = { _, item -> "${item.id}" }, + contentType = { _, item -> + when { + item.videoInfo != null -> "Video" + item.media.isNotEmpty() -> "Media" + else -> "PlainText" } - ) { index, item -> - Column { - AnimatedVisibility( - visible = !hiddenThreadIds.contains(item.threadId), - enter = EnterTransition.None, - exit = shrinkVertically() + fadeOut() + } + ) { index, item -> + Column { + AnimatedVisibility( + visible = !hiddenThreadIds.contains(item.threadId), + 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( - item = item, - onClick = { - ThreadActivity.launch(context, item.threadId.toString()) - }, - onAgree = { + Dislike( + personalized = threadPersonalizedData[index], + onDislike = { clickTime, reasons -> viewModel.send( - PersonalizedUiIntent.Agree( + PersonalizedUiIntent.Dislike( + item.forumInfo?.id ?: 0, item.threadId, - item.firstPostId, - item.agree?.hasAgree ?: 0 + reasons, + 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) { - Divider( - color = ExtendedTheme.colors.divider, - modifier = Modifier.padding(horizontal = 16.dp), - thickness = 2.dp - ) - } + } + if (!hiddenThreadIds.contains(item.threadId)) { + if ((refreshPosition == 0 || index + 1 != refreshPosition) && index < data.size - 1) { + Divider( + color = ExtendedTheme.colors.divider, + modifier = Modifier.padding(horizontal = 16.dp), + thickness = 2.dp + ) } - if (refreshPosition != 0 && index + 1 == refreshPosition) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { viewModel.send(PersonalizedUiIntent.Refresh) } - .padding(8.dp), - ) { - Icon( - imageVector = Icons.Rounded.Refresh, - contentDescription = null - ) - Spacer(modifier = Modifier.width(16.dp)) - Text(text = stringResource(id = R.string.tip_refresh), style = MaterialTheme.typography.subtitle1) - } + } + if (refreshPosition != 0 && index + 1 == refreshPosition) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { viewModel.send(PersonalizedUiIntent.Refresh) } + .padding(8.dp), + ) { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(id = R.string.tip_refresh), + style = MaterialTheme.typography.subtitle1 + ) } } } } - LaunchedEffect(data.firstOrNull()?.id) { - //delay(50) - lazyStaggeredGridState.scrollToItem(0, 0) - } + } + LaunchedEffect(data.firstOrNull()?.id) { + //delay(50) + lazyStaggeredGridState.scrollToItem(0, 0) } } + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) + AnimatedVisibility( visible = showRefreshTip, enter = fadeIn() + slideInVertically(), diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt index 00ff50d7..57855b0f 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.Scaffold 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.rounded.Check 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.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -40,6 +44,7 @@ import androidx.compose.runtime.getValue 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.Companion.Center import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier @@ -55,8 +60,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.google.accompanist.drawablepainter.rememberDrawablePainter 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.activities.ForumActivity 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.compose.AccountNavIconIfCompact 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.LongClickMenu import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar @@ -307,30 +311,7 @@ private fun ForumItem( ) { AnimatedVisibility(visible = showAvatar) { Row { - coil.compose.AsyncImage( - 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() -// } + Avatar(data = item.avatar, size = 40.dp, contentDescription = null) Spacer(modifier = Modifier.width(14.dp)) } } @@ -382,6 +363,7 @@ private fun ForumItem( } } +@OptIn(ExperimentalMaterialApi::class) @Composable fun HomePage( viewModel: HomeViewModel = pageViewModel( @@ -431,11 +413,12 @@ fun HomePage( }, modifier = Modifier.fillMaxSize(), ) { contentPaddings -> - SwipeRefresh( - state = rememberSwipeRefreshState(isRefreshing = isLoading), - onRefresh = { viewModel.send(HomeUiIntent.Refresh) }, - modifier = Modifier.padding(contentPaddings) - ) { + val pullRefreshState = rememberPullRefreshState( + refreshing = isLoading, + onRefresh = { viewModel.send(HomeUiIntent.Refresh) }) + Box(modifier = Modifier + .padding(contentPaddings) + .pullRefresh(pullRefreshState)) { val gridState = rememberLazyGridState() LazyVerticalGrid( state = gridState, @@ -512,7 +495,10 @@ fun HomePage( Header( text = stringResource(id = R.string.forum_list_title), 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)) } @@ -522,6 +508,12 @@ fun HomePage( } } } + + PullRefreshIndicator( + refreshing = isLoading, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Avatars.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Avatars.kt index 151708d8..70cc30d1 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Avatars.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Avatars.kt @@ -19,7 +19,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext 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.huanchengfly.tieba.post.utils.ImageUtil @@ -83,16 +84,22 @@ fun Avatar( is String? -> { AsyncImage( - imageUri = data, + model = ImageRequest.Builder(LocalContext.current) + .data(data) + .crossfade(true) + .build(), contentDescription = contentDescription, + placeholder = rememberDrawablePainter( + drawable = ImageUtil.getPlaceHolder( + context, + 0 + ) + ), modifier = modifier .size(size) .clip(CircleShape), contentScale = ContentScale.Crop - ) { - placeholder(ImageUtil.getPlaceHolder(context, 0)) - crossfade() - } + ) } else -> throw IllegalArgumentException("不支持该类型") diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/Widgets.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt similarity index 66% rename from app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/Widgets.kt rename to app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt index 7d3c9d42..adaea79b 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/Widgets.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt @@ -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.foundation.background @@ -16,23 +16,17 @@ 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.LocalContentColor import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Favorite 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.SwapCalls 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 @@ -42,13 +36,10 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource 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.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView -import androidx.constraintlayout.compose.ConstraintLayout import cn.jzvd.Jzvd import com.github.panpf.sketch.displayImage 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.api.models.protos.Media 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.utils.getPhotoViewData 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.ImageUtil import com.huanchengfly.tieba.post.utils.StringUtil @@ -79,142 +60,6 @@ private val Media.url: String @Composable get() = 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) -> Unit, -) { - var clickTime by remember { mutableStateOf(0L) } - val selectedReasons = remember { mutableStateListOf() } - 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 fun FeedCard( item: ThreadInfo, @@ -432,4 +277,27 @@ private fun ActionBtn( Spacer(modifier = Modifier.width(8.dp)) 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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt index 51a2e578..6c8d0eb8 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt @@ -8,7 +8,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale 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.models.protos.PhotoViewData import com.huanchengfly.tieba.post.ui.page.photoview.PhotoViewActivity @@ -37,11 +39,13 @@ fun NetworkImage( } } else Modifier AsyncImage( - imageUri = imageUri, + model = ImageRequest.Builder(LocalContext.current) + .data(imageUri) + .crossfade(true) + .build(), contentDescription = contentDescription, + placeholder = rememberDrawablePainter(drawable = ImageUtil.getPlaceHolder(context, 0)), modifier = modifier.then(clickableModifier), contentScale = contentScale, - ) { - placeholder(ImageUtil.getPlaceHolder(context, 0)) - } + ) } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Toolbar.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Toolbar.kt index ae273630..b7dd6934 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Toolbar.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Toolbar.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight 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.huanchengfly.tieba.post.R 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.utils.AccountUtil 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.compose.calcStatusBarColor @@ -136,16 +134,11 @@ fun AccountNavIcon( onClick = onClick, shape = CircleShape ) { - AsyncImage( - imageUri = StringUtil.getAvatarUrl(currentAccount.portrait), - contentDescription = stringResource(id = R.string.title_switch_account_long_press), - modifier = Modifier - .clip(CircleShape) - .size(size), - ) { - placeholder(ImageUtil.getPlaceHolder(context, 0)) - crossfade() - } + Avatar( + data = StringUtil.getAvatarUrl(currentAccount.portrait), + size = size, + contentDescription = stringResource(id = R.string.title_switch_account_long_press) + ) } } } diff --git a/app/src/main/protos/TopicList.proto b/app/src/main/protos/TopicList.proto new file mode 100644 index 00000000..955f50aa --- /dev/null +++ b/app/src/main/protos/TopicList.proto @@ -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; +} \ 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 23fdd287..e3c9bfb9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -276,7 +276,7 @@ 确定要取消关注%s吧吗? 保存语音 贴吧话题 - 热议话题 + 话题榜 (每小时更新一次,以实时讨论量排序) 图片保存在 %1$s 吧默认排序方式