@@ -32,6 +32,21 @@ data class Moment(
|
||||
val time: String,
|
||||
@SerializedName("isFollowed")
|
||||
val isFollowed: Boolean,
|
||||
// 新闻相关字段
|
||||
@SerializedName("isNews")
|
||||
val isNews: Boolean = false,
|
||||
@SerializedName("newsTitle")
|
||||
val newsTitle: String? = null,
|
||||
@SerializedName("newsUrl")
|
||||
val newsUrl: String? = null,
|
||||
@SerializedName("newsSource")
|
||||
val newsSource: String? = null,
|
||||
@SerializedName("newsCategory")
|
||||
val newsCategory: String? = null,
|
||||
@SerializedName("newsLanguage")
|
||||
val newsLanguage: String? = null,
|
||||
@SerializedName("newsContent")
|
||||
val newsContent: String? = null,
|
||||
) {
|
||||
fun toMomentItem(): MomentEntity {
|
||||
return MomentEntity(
|
||||
@@ -60,6 +75,14 @@ data class Moment(
|
||||
authorId = user.id.toInt(),
|
||||
liked = isLiked,
|
||||
isFavorite = isFavorite,
|
||||
// 新闻相关字段
|
||||
isNews = isNews,
|
||||
newsTitle = newsTitle ?: "",
|
||||
newsUrl = newsUrl ?: "",
|
||||
newsSource = newsSource ?: "",
|
||||
newsCategory = newsCategory ?: "",
|
||||
newsLanguage = newsLanguage ?: "",
|
||||
newsContent = newsContent ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +527,7 @@ interface RaveNowAPI {
|
||||
@Query("trend") trend: String? = null,
|
||||
@Query("favouriteUserId") favouriteUserId: Int? = null,
|
||||
@Query("explore") explore: String? = null,
|
||||
@Query("newsFilter") newsFilter: String? = null,
|
||||
): Response<ListContainer<Moment>>
|
||||
|
||||
@Multipart
|
||||
|
||||
@@ -299,12 +299,21 @@ data class MomentEntity(
|
||||
// 关联动态
|
||||
var relMoment: MomentEntity? = null,
|
||||
// 是否收藏
|
||||
var isFavorite: Boolean = false
|
||||
var isFavorite: Boolean = false,
|
||||
// 新闻相关字段
|
||||
val isNews: Boolean = false,
|
||||
val newsTitle: String = "",
|
||||
val newsUrl: String = "",
|
||||
val newsSource: String = "",
|
||||
val newsCategory: String = "",
|
||||
val newsLanguage: String = "",
|
||||
val newsContent: String = ""
|
||||
)
|
||||
class MomentLoaderExtraArgs(
|
||||
val explore: Boolean? = false,
|
||||
val timelineId: Int? = null,
|
||||
val authorId : Int? = null
|
||||
val authorId : Int? = null,
|
||||
val newsOnly: Boolean? = null
|
||||
)
|
||||
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||
override suspend fun fetchData(
|
||||
@@ -317,7 +326,8 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||
pageSize = pageSize,
|
||||
explore = if (extra.explore == true) "true" else "",
|
||||
timelineId = extra.timelineId,
|
||||
authorId = extra.authorId
|
||||
authorId = extra.authorId,
|
||||
newsFilter = if (extra.newsOnly == true) "news_only" else ""
|
||||
)
|
||||
val data = result.body()?.let {
|
||||
ListContainer(
|
||||
@@ -355,6 +365,18 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||
onListChanged?.invoke(this.list)
|
||||
}
|
||||
|
||||
fun updateCommentCount(id: Int, delta: Int) {
|
||||
this.list = this.list.map { momentItem ->
|
||||
if (momentItem.id == id) {
|
||||
val newCount = (momentItem.commentCount + delta).coerceAtLeast(0)
|
||||
momentItem.copy(commentCount = newCount)
|
||||
} else {
|
||||
momentItem
|
||||
}
|
||||
}.toMutableList()
|
||||
onListChanged?.invoke(this.list)
|
||||
}
|
||||
|
||||
fun removeMoment(id: Int) {
|
||||
this.list = this.list.filter { it.id != id }.toMutableList()
|
||||
onListChanged?.invoke(this.list)
|
||||
|
||||
@@ -51,7 +51,8 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
@Composable
|
||||
fun EditCommentBottomModal(
|
||||
replyComment: CommentEntity? = null,
|
||||
onSend: (String) -> Unit = {}
|
||||
autoFocus: Boolean = false,
|
||||
onSend: (String) -> Unit = {},
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
var text by remember { mutableStateOf("") }
|
||||
@@ -59,8 +60,10 @@ fun EditCommentBottomModal(
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
LaunchedEffect(autoFocus) {
|
||||
if (autoFocus) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
|
||||
@@ -72,9 +72,12 @@ open class BaseMomentModel :ViewModel(){
|
||||
}
|
||||
|
||||
|
||||
suspend fun onAddComment(id: Int) {
|
||||
// val currentPagingData = _momentsFlow.value
|
||||
// updateCommentCount(id)
|
||||
fun onAddComment(id: Int) {
|
||||
momentLoader.updateCommentCount(id, +1)
|
||||
}
|
||||
|
||||
fun onDeleteComment(id: Int) {
|
||||
momentLoader.updateCommentCount(id, -1)
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +86,7 @@ open class BaseMomentModel :ViewModel(){
|
||||
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
|
||||
momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
|
||||
}
|
||||
|
||||
suspend fun favoriteMoment(id: Int) {
|
||||
momentService.favoriteMoment(id)
|
||||
momentLoader.updateFavoriteCount(id, true)
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.data.CommentService
|
||||
import com.aiosman.ravenow.data.CommentServiceImpl
|
||||
import com.aiosman.ravenow.entity.CommentEntity
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
||||
import com.aiosman.ravenow.ui.composables.debouncedClickable
|
||||
import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.post.CommentContent
|
||||
import com.aiosman.ravenow.ui.post.CommentMenuModal
|
||||
import com.aiosman.ravenow.ui.post.CommentsViewModel
|
||||
import com.aiosman.ravenow.ui.post.OrderSelectionComponent
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class NewsCommentModalViewModel(
|
||||
val postId: Int?
|
||||
) : ViewModel() {
|
||||
var commentsViewModel: CommentsViewModel = CommentsViewModel(postId.toString())
|
||||
var commentService: CommentService = CommentServiceImpl()
|
||||
|
||||
init {
|
||||
commentsViewModel.preTransit()
|
||||
}
|
||||
|
||||
fun likeComment(commentId: Int) {
|
||||
viewModelScope.launch {
|
||||
commentsViewModel.likeComment(commentId)
|
||||
}
|
||||
}
|
||||
|
||||
fun unlikeComment(commentId: Int) {
|
||||
viewModelScope.launch {
|
||||
commentsViewModel.unlikeComment(commentId)
|
||||
}
|
||||
}
|
||||
|
||||
fun createComment(
|
||||
content: String,
|
||||
parentCommentId: Int? = null,
|
||||
replyUserId: Int? = null,
|
||||
replyCommentId: Int? = null
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
commentsViewModel.createComment(
|
||||
content = content,
|
||||
parentCommentId = parentCommentId,
|
||||
replyUserId = replyUserId,
|
||||
replyCommentId = replyCommentId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteComment(commentId: Int) {
|
||||
commentsViewModel.deleteComment(commentId)
|
||||
}
|
||||
}
|
||||
|
||||
// 新闻评论弹窗
|
||||
// @param postId 新闻帖子ID
|
||||
// @param commentCount 评论数量
|
||||
// @param onDismiss 关闭回调
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewsCommentModal(
|
||||
postId: Int? = null,
|
||||
commentCount: Int = 0,
|
||||
onDismiss: () -> Unit = {},
|
||||
onCommentAdded: () -> Unit = {},
|
||||
onCommentDeleted: () -> Unit = {}
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val navController = LocalNavController.current
|
||||
val debouncedNavigation = rememberDebouncedNavigation()
|
||||
|
||||
// 实时评论数状态
|
||||
var currentCommentCount by remember { mutableStateOf(commentCount) }
|
||||
|
||||
val model = viewModel<NewsCommentModalViewModel>(
|
||||
key = "NewsCommentModalViewModel_$postId",
|
||||
factory = object : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return NewsCommentModalViewModel(postId) as T
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val commentViewModel = model.commentsViewModel
|
||||
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
|
||||
var showCommentMenu by remember { mutableStateOf(false) }
|
||||
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||
|
||||
// 菜单弹窗
|
||||
if (showCommentMenu) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
showCommentMenu = false
|
||||
},
|
||||
containerColor = AppColors.background,
|
||||
sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
),
|
||||
dragHandle = {},
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
windowInsets = WindowInsets(0)
|
||||
) {
|
||||
CommentMenuModal(
|
||||
onDeleteClick = {
|
||||
showCommentMenu = false
|
||||
contextComment?.let {
|
||||
model.deleteComment(it.id)
|
||||
onCommentDeleted()
|
||||
currentCommentCount = (currentCommentCount - 1).coerceAtLeast(0)
|
||||
}
|
||||
},
|
||||
commentEntity = contextComment,
|
||||
onCloseClick = {
|
||||
showCommentMenu = false
|
||||
},
|
||||
isSelf = AppState.UserId?.toLong() == contextComment?.author,
|
||||
onLikeClick = {
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
} else {
|
||||
showCommentMenu = false
|
||||
contextComment?.let {
|
||||
if (it.liked) {
|
||||
model.unlikeComment(it.id)
|
||||
} else {
|
||||
model.likeComment(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onReplyClick = {
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
} else {
|
||||
showCommentMenu = false
|
||||
replyComment = contextComment
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.background(AppColors.background)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "${currentCommentCount}条评论",
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text
|
||||
)
|
||||
|
||||
// 排序选择
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
OrderSelectionComponent {
|
||||
commentViewModel.order = it
|
||||
commentViewModel.reloadComment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 评论列表
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
LazyColumn {
|
||||
item {
|
||||
CommentContent(
|
||||
viewModel = commentViewModel,
|
||||
onLongClick = { comment ->
|
||||
showCommentMenu = true
|
||||
contextComment = comment
|
||||
},
|
||||
onReply = { parentComment, _, _, _ ->
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
} else {
|
||||
replyComment = parentComment
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部输入栏
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(AppColors.background)
|
||||
) {
|
||||
HorizontalDivider(color = AppColors.inputBackground)
|
||||
|
||||
EditCommentBottomModal(
|
||||
replyComment = replyComment,
|
||||
autoFocus = false
|
||||
) {
|
||||
if (replyComment != null) {
|
||||
if (replyComment?.parentCommentId != null) {
|
||||
// 第三级评论
|
||||
model.createComment(
|
||||
content = it,
|
||||
parentCommentId = replyComment?.parentCommentId,
|
||||
replyUserId = replyComment?.author?.toInt(),
|
||||
replyCommentId = replyComment?.id
|
||||
)
|
||||
} else {
|
||||
// 子级评论
|
||||
model.createComment(
|
||||
content = it,
|
||||
parentCommentId = replyComment?.id,
|
||||
replyCommentId = replyComment?.id
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 顶级评论
|
||||
model.createComment(content = it)
|
||||
}
|
||||
replyComment = null
|
||||
onCommentAdded()
|
||||
currentCommentCount++
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(navBarHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -10,24 +11,27 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -39,55 +43,41 @@ 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 androidx.compose.ui.platform.LocalConfiguration
|
||||
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.exp.timeAgo
|
||||
import com.aiosman.ravenow.exp.formatPostTime2
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.dynamic.DynamicViewModel
|
||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.news.NewsViewModel
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NewsScreen() {
|
||||
val model = DynamicViewModel
|
||||
val model = NewsViewModel
|
||||
val moments = model.moments
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// 下拉刷新状态
|
||||
val state = rememberPullRefreshState(model.refreshing, onRefresh = {
|
||||
model.refreshPager(pullRefresh = true)
|
||||
})
|
||||
// 评论弹窗状态
|
||||
var showCommentModal by remember { mutableStateOf(false) }
|
||||
var selectedMoment by remember { mutableStateOf<MomentEntity?>(null) }
|
||||
// 垂直翻页状态
|
||||
val pagerState = rememberPagerState(pageCount = { moments.size })
|
||||
|
||||
// 列表状态
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
// 用于跟踪是否已经触发过加载更多
|
||||
var hasTriggeredLoadMore by remember { mutableStateOf(false) }
|
||||
|
||||
// 监听滚动到底部
|
||||
val reachedBottom by remember {
|
||||
derivedStateOf {
|
||||
val layoutInfo = listState.layoutInfo
|
||||
val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
|
||||
val totalItems = layoutInfo.totalItemsCount
|
||||
|
||||
if (lastVisibleItem == null || totalItems == 0) {
|
||||
false
|
||||
} else {
|
||||
val isLastItemVisible = lastVisibleItem.index >= totalItems - 2
|
||||
isLastItemVisible && !hasTriggeredLoadMore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多数据
|
||||
LaunchedEffect(reachedBottom) {
|
||||
if (reachedBottom) {
|
||||
hasTriggeredLoadMore = true
|
||||
model.loadMore()
|
||||
}
|
||||
}
|
||||
// 防抖器
|
||||
val likeDebouncer = rememberDebouncer()
|
||||
val favoriteDebouncer = rememberDebouncer()
|
||||
|
||||
// 初始化加载数据
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -96,9 +86,13 @@ fun NewsScreen() {
|
||||
|
||||
// 监听数据变化,重置加载状态
|
||||
LaunchedEffect(moments.size) {
|
||||
if (moments.size > 0) {
|
||||
kotlinx.coroutines.delay(500)
|
||||
hasTriggeredLoadMore = false
|
||||
// 当数据增加时,如果接近列表末尾,Pager会自动更新页数
|
||||
}
|
||||
|
||||
// 当翻页接近末尾时加载更多
|
||||
LaunchedEffect(pagerState.currentPage, moments.size) {
|
||||
if (moments.isNotEmpty() && pagerState.currentPage >= moments.size - 2) {
|
||||
model.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,26 +101,96 @@ fun NewsScreen() {
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background)
|
||||
) {
|
||||
Box(Modifier.pullRefresh(state)) {
|
||||
LazyColumn(
|
||||
if (moments.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
items(
|
||||
moments.size,
|
||||
key = { idx -> idx }
|
||||
) { idx ->
|
||||
// 处理下标越界
|
||||
if (idx < 0 || idx >= moments.size) return@items
|
||||
val momentItem = moments[idx] ?: return@items
|
||||
|
||||
NewsItem(
|
||||
moment = momentItem,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Text(text = "暂无新闻内容", color = AppColors.text, fontSize = 16.sp)
|
||||
}
|
||||
} else {
|
||||
VerticalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) { page ->
|
||||
val momentItem = moments.getOrNull(page) ?: return@VerticalPager
|
||||
NewsItem(
|
||||
moment = momentItem,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onCommentClick = {
|
||||
selectedMoment = momentItem
|
||||
showCommentModal = true
|
||||
},
|
||||
onLikeClick = {
|
||||
likeDebouncer {
|
||||
// 检查游客模式
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
} else {
|
||||
scope.launch {
|
||||
if (momentItem.liked) {
|
||||
model.dislikeMoment(momentItem.id)
|
||||
} else {
|
||||
model.likeMoment(momentItem.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onFavoriteClick = {
|
||||
favoriteDebouncer {
|
||||
// 检查游客模式
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
} else {
|
||||
scope.launch {
|
||||
if (momentItem.isFavorite) {
|
||||
model.unfavoriteMoment(momentItem.id)
|
||||
} else {
|
||||
model.favoriteMoment(momentItem.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 评论弹窗
|
||||
if (showCommentModal && selectedMoment != null) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp.dp
|
||||
val sheetHeight = screenHeight * 0.67f // 三分之二高度
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
showCommentModal = false
|
||||
},
|
||||
sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(sheetHeight),
|
||||
containerColor = AppColors.background,
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
windowInsets = androidx.compose.foundation.layout.WindowInsets(0)
|
||||
) {
|
||||
NewsCommentModal(
|
||||
postId = selectedMoment?.id,
|
||||
commentCount = selectedMoment?.commentCount ?: 0,
|
||||
onDismiss = {
|
||||
showCommentModal = false
|
||||
},
|
||||
onCommentAdded = {
|
||||
selectedMoment?.id?.let { model.onAddComment(it) }
|
||||
},
|
||||
onCommentDeleted = {
|
||||
selectedMoment?.id?.let { model.onDeleteComment(it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,20 +199,26 @@ fun NewsScreen() {
|
||||
@Composable
|
||||
fun NewsItem(
|
||||
moment: MomentEntity,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
onCommentClick: () -> Unit = {},
|
||||
onLikeClick: () -> Unit = {},
|
||||
onFavoriteClick: () -> Unit = {}
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(700.dp)
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(bottom = 30.dp)
|
||||
) {
|
||||
// 新闻图片
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -183,7 +253,7 @@ fun NewsItem(
|
||||
|
||||
// 新闻标题
|
||||
Text(
|
||||
text = moment.nickname, // 暂时使用用户名作为标题
|
||||
text = if (moment.newsTitle.isNotEmpty()) moment.newsTitle else moment.nickname,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
@@ -196,15 +266,17 @@ fun NewsItem(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 新闻内容
|
||||
// 新闻内容(超出使用省略号)
|
||||
Text(
|
||||
text = moment.momentTextContent, // 使用动态内容作为新闻内容
|
||||
text = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.text,
|
||||
lineHeight = 20.sp
|
||||
lineHeight = 20.sp,
|
||||
maxLines = 6,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -217,11 +289,16 @@ fun NewsItem(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 来源和时间
|
||||
// 来源和时间(显示月份与具体时间)
|
||||
Text(
|
||||
text = "${moment.nickname} • ${moment.time.timeAgo(context)}",
|
||||
text = if (moment.newsSource.isNotEmpty()) "${moment.newsSource} • ${moment.time.formatPostTime2()}" else "${moment.nickname} • ${moment.time.formatPostTime2()}",
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.secondaryText
|
||||
color = AppColors.secondaryText,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
// 查看全文
|
||||
@@ -249,28 +326,31 @@ fun NewsItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 50.dp),
|
||||
.padding(bottom = 25.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
// 点赞
|
||||
NewsActionButton(
|
||||
icon = R.drawable.rider_pro_moment_like,
|
||||
icon = if (moment.liked) R.drawable.rider_pro_moment_liked else R.drawable.rider_pro_moment_like,
|
||||
count = moment.likeCount.toString(),
|
||||
isActive = moment.liked
|
||||
isActive = moment.liked,
|
||||
modifier = Modifier.noRippleClickable { onLikeClick() }
|
||||
)
|
||||
|
||||
// 评论
|
||||
NewsActionButton(
|
||||
icon = R.mipmap.icon_comment,
|
||||
count = moment.commentCount.toString(),
|
||||
isActive = false
|
||||
isActive = false,
|
||||
modifier = Modifier.noRippleClickable { onCommentClick() }
|
||||
)
|
||||
|
||||
// 收藏
|
||||
NewsActionButton(
|
||||
icon = R.mipmap.icon_collect,
|
||||
icon = if (moment.isFavorite) R.mipmap.icon_variant_2 else R.mipmap.icon_collect,
|
||||
count = moment.favoriteCount.toString(),
|
||||
isActive = moment.isFavorite
|
||||
isActive = moment.isFavorite,
|
||||
modifier = Modifier.noRippleClickable { onFavoriteClick() }
|
||||
)
|
||||
|
||||
// 分享
|
||||
@@ -311,10 +391,7 @@ fun NewsActionButton(
|
||||
Image(
|
||||
painter = androidx.compose.ui.res.painterResource(id = icon),
|
||||
contentDescription = "操作图标",
|
||||
modifier = Modifier.size(16.dp),
|
||||
colorFilter = ColorFilter.tint(
|
||||
if (isActive) AppColors.background else AppColors.text
|
||||
)
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
|
||||
if (count.isNotEmpty()) {
|
||||
@@ -322,7 +399,7 @@ fun NewsActionButton(
|
||||
Text(
|
||||
text = count,
|
||||
fontSize = 12.sp,
|
||||
color = if (isActive) AppColors.background else AppColors.text
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
if (text != null) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
|
||||
|
||||
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
|
||||
|
||||
object NewsViewModel : BaseMomentModel() {
|
||||
override fun extraArgs(): MomentLoaderExtraArgs {
|
||||
// 只拉取新闻
|
||||
return MomentLoaderExtraArgs(
|
||||
explore = false,
|
||||
timelineId = null,
|
||||
authorId = null,
|
||||
newsOnly = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -47,66 +48,72 @@ fun SelfProfileAction(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(AppColors.nonActive)
|
||||
.padding(horizontal = 5.dp, vertical = 12.dp)
|
||||
.width(60.dp).height(25.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(androidx.compose.ui.graphics.Color(0x229284BD))
|
||||
.noRippleClickable {
|
||||
editProfileDebouncer {
|
||||
onEditProfile()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.fill_and_sign),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(12.dp),
|
||||
colorFilter = ColorFilter.tint(androidx.compose.ui.graphics.Color(0xFF9284BD))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.edit_profile),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W900,
|
||||
color = AppColors.text,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = androidx.compose.ui.graphics.Color(0xFF9284BD),
|
||||
)
|
||||
}
|
||||
|
||||
// 预留按钮位置
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.noRippleClickable {
|
||||
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W900,
|
||||
color = AppColors.text,
|
||||
)
|
||||
}
|
||||
|
||||
// 分享按钮
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(AppColors.nonActive)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.noRippleClickable {
|
||||
shareDebouncer {
|
||||
// TODO: 添加分享逻辑
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.share),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W900,
|
||||
color = AppColors.text,
|
||||
)
|
||||
}
|
||||
// // 预留按钮位置
|
||||
// Row(
|
||||
// verticalAlignment = Alignment.CenterVertically,
|
||||
// horizontalArrangement = Arrangement.Center,
|
||||
// modifier = Modifier
|
||||
// .weight(1f)
|
||||
// .clip(RoundedCornerShape(10.dp))
|
||||
// .padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
// .noRippleClickable {
|
||||
//
|
||||
// }
|
||||
// ) {
|
||||
// Text(
|
||||
// text = "",
|
||||
// fontSize = 14.sp,
|
||||
// fontWeight = FontWeight.W900,
|
||||
// color = AppColors.text,
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // 分享按钮
|
||||
// Row(
|
||||
// verticalAlignment = Alignment.CenterVertically,
|
||||
// horizontalArrangement = Arrangement.Center,
|
||||
// modifier = Modifier
|
||||
// .weight(1f)
|
||||
// .clip(RoundedCornerShape(10.dp))
|
||||
// .background(AppColors.nonActive)
|
||||
// .padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
// .noRippleClickable {
|
||||
// shareDebouncer {
|
||||
// // TODO: 添加分享逻辑
|
||||
// }
|
||||
// }
|
||||
// ) {
|
||||
// Text(
|
||||
// text = stringResource(R.string.share),
|
||||
// fontSize = 14.sp,
|
||||
// fontWeight = FontWeight.W900,
|
||||
// color = AppColors.text,
|
||||
// )
|
||||
// }
|
||||
|
||||
// // Rave Premium 按钮(右侧)
|
||||
// Row(
|
||||
|
||||
@@ -359,30 +359,31 @@ fun LoginPage() {
|
||||
NavigationRoute.EmailSignUp.route,
|
||||
)
|
||||
}
|
||||
//苹果登录tab
|
||||
//谷歌登录tab
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ActionButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
width = 1.5.dp,
|
||||
color = if (AppState.darkMode) Color.White else Color.Black,
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
),
|
||||
text = stringResource(R.string.sign_in_with_apple),
|
||||
text = stringResource(R.string.sign_in_with_google),
|
||||
color = if (AppState.darkMode) Color.White else Color.Black,
|
||||
backgroundColor = if (AppState.darkMode) Color.Black else Color.White,
|
||||
leading = {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.apple_logo_medium),
|
||||
contentDescription = "Apple",
|
||||
modifier = Modifier.size(36.dp),
|
||||
colorFilter = ColorFilter.tint(if (AppState.darkMode) Color.White else Color.Black)
|
||||
painter = painterResource(id = R.mipmap.rider_pro_signup_google),
|
||||
contentDescription = "Google",
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
},
|
||||
expandText = true,
|
||||
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp)
|
||||
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 10.dp)
|
||||
) {
|
||||
googleLogin()
|
||||
}
|
||||
|
||||
//登录tab
|
||||
|
||||
@@ -211,7 +211,7 @@ fun SignupScreen() {
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
},
|
||||
text = stringResource(R.string.sign_in_with_apple),
|
||||
text = stringResource(R.string.sign_in_with_google),
|
||||
) {
|
||||
googleLogin()
|
||||
|
||||
|
||||
BIN
app/src/main/res/mipmap-hdpi/fill_and_sign.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/fill_and_sign.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 335 B |
BIN
app/src/main/res/mipmap-mdpi/fill_and_sign.png
Normal file
BIN
app/src/main/res/mipmap-mdpi/fill_and_sign.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 277 B |
BIN
app/src/main/res/mipmap-xhdpi/fill_and_sign.png
Normal file
BIN
app/src/main/res/mipmap-xhdpi/fill_and_sign.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 377 B |
BIN
app/src/main/res/mipmap-xxhdpi/fill_and_sign.png
Normal file
BIN
app/src/main/res/mipmap-xxhdpi/fill_and_sign.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 521 B |
BIN
app/src/main/res/mipmap-xxxhdpi/fill_and_sign.png
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/fill_and_sign.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 614 B |
@@ -33,7 +33,7 @@
|
||||
<string name="text_hint_password">パスワードを入力してください</string>
|
||||
<string name="sign_up_upper">サインアップ</string>
|
||||
<string name="sign_in_with_email">メールで接続</string>
|
||||
<string name="sign_in_with_apple">Appleで接続</string>
|
||||
<string name="sign_in_with_google">Googleで接続</string>
|
||||
<string name="back_upper">戻る</string>
|
||||
<string name="text_hint_confirm_password">パスワードを再入力してください</string>
|
||||
<string name="login_confirm_password_label">パスワードの確認</string>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<string name="text_hint_password">输入密码</string>
|
||||
<string name="sign_up_upper">注册</string>
|
||||
<string name="sign_in_with_email">使用邮箱注册</string>
|
||||
<string name="sign_in_with_apple">使用Apple登录</string>
|
||||
<string name="sign_in_with_google">使用Google账号登录</string>
|
||||
<string name="back_upper">返回</string>
|
||||
<string name="text_hint_confirm_password">再次输入密码</string>
|
||||
<string name="login_confirm_password_label">再次输入密码</string>
|
||||
@@ -48,7 +48,7 @@
|
||||
<string name="error_not_accept_term">"为了提供更好的服务,请您在注册前仔细阅读并同意《用户协议》。 "</string>
|
||||
<string name="empty_my_post_title">还没有发布任何动态</string>
|
||||
<string name="empty_my_post_content">发布一个动态吧</string>
|
||||
<string name="edit_profile">编辑资料</string>
|
||||
<string name="edit_profile">编辑</string>
|
||||
<string name="share">分享</string>
|
||||
<string name="logout">登出</string>
|
||||
<string name="change_password">修改密码</string>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<string name="text_hint_password">Enter your password</string>
|
||||
<string name="sign_up_upper">Sign Up</string>
|
||||
<string name="sign_in_with_email">Connect with Email</string>
|
||||
<string name="sign_in_with_apple">Continue with Apple</string>
|
||||
<string name="sign_in_with_google">Continue with Google</string>
|
||||
<string name="back_upper">BACK</string>
|
||||
<string name="text_hint_confirm_password">Enter your password again</string>
|
||||
<string name="login_confirm_password_label">Confirm password</string>
|
||||
|
||||
Reference in New Issue
Block a user