Merge pull request #45 from Kevinlinpr/zhong_1

登录界面UI调整;新增新闻评论
This commit is contained in:
2025-10-28 18:45:22 +08:00
committed by GitHub
19 changed files with 616 additions and 154 deletions

View File

@@ -32,6 +32,21 @@ data class Moment(
val time: String, val time: String,
@SerializedName("isFollowed") @SerializedName("isFollowed")
val isFollowed: Boolean, 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 { fun toMomentItem(): MomentEntity {
return MomentEntity( return MomentEntity(
@@ -60,6 +75,14 @@ data class Moment(
authorId = user.id.toInt(), authorId = user.id.toInt(),
liked = isLiked, liked = isLiked,
isFavorite = isFavorite, isFavorite = isFavorite,
// 新闻相关字段
isNews = isNews,
newsTitle = newsTitle ?: "",
newsUrl = newsUrl ?: "",
newsSource = newsSource ?: "",
newsCategory = newsCategory ?: "",
newsLanguage = newsLanguage ?: "",
newsContent = newsContent ?: ""
) )
} }
} }

View File

@@ -527,6 +527,7 @@ interface RaveNowAPI {
@Query("trend") trend: String? = null, @Query("trend") trend: String? = null,
@Query("favouriteUserId") favouriteUserId: Int? = null, @Query("favouriteUserId") favouriteUserId: Int? = null,
@Query("explore") explore: String? = null, @Query("explore") explore: String? = null,
@Query("newsFilter") newsFilter: String? = null,
): Response<ListContainer<Moment>> ): Response<ListContainer<Moment>>
@Multipart @Multipart

View File

@@ -299,12 +299,21 @@ data class MomentEntity(
// 关联动态 // 关联动态
var relMoment: MomentEntity? = null, 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( class MomentLoaderExtraArgs(
val explore: Boolean? = false, val explore: Boolean? = false,
val timelineId: Int? = null, val timelineId: Int? = null,
val authorId : Int? = null val authorId : Int? = null,
val newsOnly: Boolean? = null
) )
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() { class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
override suspend fun fetchData( override suspend fun fetchData(
@@ -317,7 +326,8 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
pageSize = pageSize, pageSize = pageSize,
explore = if (extra.explore == true) "true" else "", explore = if (extra.explore == true) "true" else "",
timelineId = extra.timelineId, timelineId = extra.timelineId,
authorId = extra.authorId authorId = extra.authorId,
newsFilter = if (extra.newsOnly == true) "news_only" else ""
) )
val data = result.body()?.let { val data = result.body()?.let {
ListContainer( ListContainer(
@@ -355,6 +365,18 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
onListChanged?.invoke(this.list) 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) { fun removeMoment(id: Int) {
this.list = this.list.filter { it.id != id }.toMutableList() this.list = this.list.filter { it.id != id }.toMutableList()
onListChanged?.invoke(this.list) onListChanged?.invoke(this.list)

View File

@@ -51,7 +51,8 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable @Composable
fun EditCommentBottomModal( fun EditCommentBottomModal(
replyComment: CommentEntity? = null, replyComment: CommentEntity? = null,
onSend: (String) -> Unit = {} autoFocus: Boolean = false,
onSend: (String) -> Unit = {},
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
var text by remember { mutableStateOf("") } var text by remember { mutableStateOf("") }
@@ -59,8 +60,10 @@ fun EditCommentBottomModal(
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(autoFocus) {
focusRequester.requestFocus() if (autoFocus) {
focusRequester.requestFocus()
}
} }
Column( Column(

View File

@@ -72,9 +72,12 @@ open class BaseMomentModel :ViewModel(){
} }
suspend fun onAddComment(id: Int) { fun onAddComment(id: Int) {
// val currentPagingData = _momentsFlow.value momentLoader.updateCommentCount(id, +1)
// updateCommentCount(id) }
fun onDeleteComment(id: Int) {
momentLoader.updateCommentCount(id, -1)
} }
@@ -83,6 +86,7 @@ open class BaseMomentModel :ViewModel(){
fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) { fun onMomentFavoriteChangeEvent(event: MomentFavouriteChangeEvent) {
momentLoader.updateFavoriteCount(event.postId, event.isFavourite) momentLoader.updateFavoriteCount(event.postId, event.isFavourite)
} }
suspend fun favoriteMoment(id: Int) { suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id) momentService.favoriteMoment(id)
momentLoader.updateFavoriteCount(id, true) momentLoader.updateFavoriteCount(id, true)

View File

@@ -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))
}
}
}

View File

@@ -1,5 +1,6 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news package com.aiosman.ravenow.ui.index.tabs.moment.tabs.news
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -39,55 +43,41 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalConfiguration
import com.aiosman.ravenow.GuestLoginCheckOut
import com.aiosman.ravenow.GuestLoginCheckOutScene
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.exp.timeAgo 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.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 @Composable
fun NewsScreen() { fun NewsScreen() {
val model = DynamicViewModel val model = NewsViewModel
val moments = model.moments val moments = model.moments
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
// 下拉刷新状态 // 评论弹窗状态
val state = rememberPullRefreshState(model.refreshing, onRefresh = { var showCommentModal by remember { mutableStateOf(false) }
model.refreshPager(pullRefresh = true) var selectedMoment by remember { mutableStateOf<MomentEntity?>(null) }
}) // 垂直翻页状态
val pagerState = rememberPagerState(pageCount = { moments.size })
// 列表状态 // 防抖器
val listState = rememberLazyListState() val likeDebouncer = rememberDebouncer()
val favoriteDebouncer = rememberDebouncer()
// 用于跟踪是否已经触发过加载更多
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()
}
}
// 初始化加载数据 // 初始化加载数据
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -96,9 +86,13 @@ fun NewsScreen() {
// 监听数据变化,重置加载状态 // 监听数据变化,重置加载状态
LaunchedEffect(moments.size) { LaunchedEffect(moments.size) {
if (moments.size > 0) { // 当数据增加时如果接近列表末尾Pager会自动更新页数
kotlinx.coroutines.delay(500) }
hasTriggeredLoadMore = false
// 当翻页接近末尾时加载更多
LaunchedEffect(pagerState.currentPage, moments.size) {
if (moments.isNotEmpty() && pagerState.currentPage >= moments.size - 2) {
model.loadMore()
} }
} }
@@ -107,26 +101,96 @@ fun NewsScreen() {
.fillMaxSize() .fillMaxSize()
.background(AppColors.background) .background(AppColors.background)
) { ) {
Box(Modifier.pullRefresh(state)) { if (moments.isEmpty()) {
LazyColumn( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState contentAlignment = Alignment.Center
) { ) {
items( Text(text = "暂无新闻内容", color = AppColors.text, fontSize = 16.sp)
moments.size, }
key = { idx -> idx } } else {
) { idx -> VerticalPager(
// 处理下标越界 state = pagerState,
if (idx < 0 || idx >= moments.size) return@items modifier = Modifier.fillMaxSize()
val momentItem = moments[idx] ?: return@items ) { page ->
val momentItem = moments.getOrNull(page) ?: return@VerticalPager
NewsItem( NewsItem(
moment = momentItem, moment = momentItem,
modifier = Modifier.fillMaxWidth() 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 @Composable
fun NewsItem( fun NewsItem(
moment: MomentEntity, moment: MomentEntity,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
onCommentClick: () -> Unit = {},
onLikeClick: () -> Unit = {},
onFavoriteClick: () -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxSize()
.height(700.dp)
.background(AppColors.background) .background(AppColors.background)
.padding(vertical = 8.dp), .padding(vertical = 8.dp),
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceBetween
) { ) {
Column { Column(
modifier = Modifier
.weight(1f)
.padding(bottom = 30.dp)
) {
// 新闻图片 // 新闻图片
Box( Box(
modifier = Modifier modifier = Modifier
@@ -183,7 +253,7 @@ fun NewsItem(
// 新闻标题 // 新闻标题
Text( Text(
text = moment.nickname, // 暂时使用用户名作为标题 text = if (moment.newsTitle.isNotEmpty()) moment.newsTitle else moment.nickname,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
@@ -196,15 +266,17 @@ fun NewsItem(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 新闻内容 // 新闻内容(超出使用省略号)
Text( Text(
text = moment.momentTextContent, // 使用动态内容作为新闻内容 text = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
fontSize = 14.sp, fontSize = 14.sp,
color = AppColors.text, color = AppColors.text,
lineHeight = 20.sp lineHeight = 20.sp,
maxLines = 6,
overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -217,11 +289,16 @@ fun NewsItem(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// 来源和时间 // 来源和时间(显示月份与具体时间)
Text( 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, 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 modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(bottom = 50.dp), .padding(bottom = 25.dp),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
// 点赞 // 点赞
NewsActionButton( 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(), count = moment.likeCount.toString(),
isActive = moment.liked isActive = moment.liked,
modifier = Modifier.noRippleClickable { onLikeClick() }
) )
// 评论 // 评论
NewsActionButton( NewsActionButton(
icon = R.mipmap.icon_comment, icon = R.mipmap.icon_comment,
count = moment.commentCount.toString(), count = moment.commentCount.toString(),
isActive = false isActive = false,
modifier = Modifier.noRippleClickable { onCommentClick() }
) )
// 收藏 // 收藏
NewsActionButton( NewsActionButton(
icon = R.mipmap.icon_collect, icon = if (moment.isFavorite) R.mipmap.icon_variant_2 else R.mipmap.icon_collect,
count = moment.favoriteCount.toString(), count = moment.favoriteCount.toString(),
isActive = moment.isFavorite isActive = moment.isFavorite,
modifier = Modifier.noRippleClickable { onFavoriteClick() }
) )
// 分享 // 分享
@@ -311,10 +391,7 @@ fun NewsActionButton(
Image( Image(
painter = androidx.compose.ui.res.painterResource(id = icon), painter = androidx.compose.ui.res.painterResource(id = icon),
contentDescription = "操作图标", contentDescription = "操作图标",
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp)
colorFilter = ColorFilter.tint(
if (isActive) AppColors.background else AppColors.text
)
) )
if (count.isNotEmpty()) { if (count.isNotEmpty()) {
@@ -322,7 +399,7 @@ fun NewsActionButton(
Text( Text(
text = count, text = count,
fontSize = 12.sp, fontSize = 12.sp,
color = if (isActive) AppColors.background else AppColors.text color = AppColors.text
) )
} }
if (text != null) { if (text != null) {

View File

@@ -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
)
}
}

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -47,66 +48,72 @@ fun SelfProfileAction(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.weight(1f) .width(60.dp).height(25.dp)
.clip(RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(12.dp))
.background(AppColors.nonActive) .background(androidx.compose.ui.graphics.Color(0x229284BD))
.padding(horizontal = 5.dp, vertical = 12.dp)
.noRippleClickable { .noRippleClickable {
editProfileDebouncer { editProfileDebouncer {
onEditProfile() 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(
text = stringResource(R.string.edit_profile), text = stringResource(R.string.edit_profile),
fontSize = 14.sp, fontSize = 12.sp,
fontWeight = FontWeight.W900, fontWeight = FontWeight.W600,
color = AppColors.text, color = androidx.compose.ui.graphics.Color(0xFF9284BD),
) )
} }
// 预留按钮位置 // // 预留按钮位置
Row( // Row(
verticalAlignment = Alignment.CenterVertically, // verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, // horizontalArrangement = Arrangement.Center,
modifier = Modifier // modifier = Modifier
.weight(1f) // .weight(1f)
.clip(RoundedCornerShape(10.dp)) // .clip(RoundedCornerShape(10.dp))
.padding(horizontal = 16.dp, vertical = 12.dp) // .padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable { // .noRippleClickable {
//
} // }
) { // ) {
Text( // Text(
text = "", // text = "",
fontSize = 14.sp, // fontSize = 14.sp,
fontWeight = FontWeight.W900, // fontWeight = FontWeight.W900,
color = AppColors.text, // color = AppColors.text,
) // )
} // }
//
// 分享按钮 // // 分享按钮
Row( // Row(
verticalAlignment = Alignment.CenterVertically, // verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, // horizontalArrangement = Arrangement.Center,
modifier = Modifier // modifier = Modifier
.weight(1f) // .weight(1f)
.clip(RoundedCornerShape(10.dp)) // .clip(RoundedCornerShape(10.dp))
.background(AppColors.nonActive) // .background(AppColors.nonActive)
.padding(horizontal = 16.dp, vertical = 12.dp) // .padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable { // .noRippleClickable {
shareDebouncer { // shareDebouncer {
// TODO: 添加分享逻辑 // // TODO: 添加分享逻辑
} // }
} // }
) { // ) {
Text( // Text(
text = stringResource(R.string.share), // text = stringResource(R.string.share),
fontSize = 14.sp, // fontSize = 14.sp,
fontWeight = FontWeight.W900, // fontWeight = FontWeight.W900,
color = AppColors.text, // color = AppColors.text,
) // )
} // }
// // Rave Premium 按钮(右侧) // // Rave Premium 按钮(右侧)
// Row( // Row(

View File

@@ -359,30 +359,31 @@ fun LoginPage() {
NavigationRoute.EmailSignUp.route, NavigationRoute.EmailSignUp.route,
) )
} }
//苹果登录tab //谷歌登录tab
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
ActionButton( ActionButton(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(52.dp)
.border( .border(
width = 1.dp, width = 1.5.dp,
color = if (AppState.darkMode) Color.White else Color.Black, color = if (AppState.darkMode) Color.White else Color.Black,
shape = RoundedCornerShape(24.dp) 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, color = if (AppState.darkMode) Color.White else Color.Black,
backgroundColor = if (AppState.darkMode) Color.Black else Color.White, backgroundColor = if (AppState.darkMode) Color.Black else Color.White,
leading = { leading = {
Image( Image(
painter = painterResource(id = R.mipmap.apple_logo_medium), painter = painterResource(id = R.mipmap.rider_pro_signup_google),
contentDescription = "Apple", contentDescription = "Google",
modifier = Modifier.size(36.dp), modifier = Modifier.size(18.dp),
colorFilter = ColorFilter.tint(if (AppState.darkMode) Color.White else Color.Black)
) )
}, },
expandText = true, expandText = true,
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 8.dp) contentPadding = PaddingValues(vertical = 8.dp, horizontal = 10.dp)
) { ) {
googleLogin()
} }
//登录tab //登录tab

View File

@@ -211,7 +211,7 @@ fun SignupScreen() {
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
}, },
text = stringResource(R.string.sign_in_with_apple), text = stringResource(R.string.sign_in_with_google),
) { ) {
googleLogin() googleLogin()

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

View File

@@ -33,7 +33,7 @@
<string name="text_hint_password">パスワードを入力してください</string> <string name="text_hint_password">パスワードを入力してください</string>
<string name="sign_up_upper">サインアップ</string> <string name="sign_up_upper">サインアップ</string>
<string name="sign_in_with_email">メールで接続</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="back_upper">戻る</string>
<string name="text_hint_confirm_password">パスワードを再入力してください</string> <string name="text_hint_confirm_password">パスワードを再入力してください</string>
<string name="login_confirm_password_label">パスワードの確認</string> <string name="login_confirm_password_label">パスワードの確認</string>

View File

@@ -32,7 +32,7 @@
<string name="text_hint_password">输入密码</string> <string name="text_hint_password">输入密码</string>
<string name="sign_up_upper">注册</string> <string name="sign_up_upper">注册</string>
<string name="sign_in_with_email">使用邮箱注册</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="back_upper">返回</string>
<string name="text_hint_confirm_password">再次输入密码</string> <string name="text_hint_confirm_password">再次输入密码</string>
<string name="login_confirm_password_label">再次输入密码</string> <string name="login_confirm_password_label">再次输入密码</string>
@@ -48,7 +48,7 @@
<string name="error_not_accept_term">"为了提供更好的服务,请您在注册前仔细阅读并同意《用户协议》。 "</string> <string name="error_not_accept_term">"为了提供更好的服务,请您在注册前仔细阅读并同意《用户协议》。 "</string>
<string name="empty_my_post_title">还没有发布任何动态</string> <string name="empty_my_post_title">还没有发布任何动态</string>
<string name="empty_my_post_content">发布一个动态吧</string> <string name="empty_my_post_content">发布一个动态吧</string>
<string name="edit_profile">编辑资料</string> <string name="edit_profile">编辑</string>
<string name="share">分享</string> <string name="share">分享</string>
<string name="logout">登出</string> <string name="logout">登出</string>
<string name="change_password">修改密码</string> <string name="change_password">修改密码</string>

View File

@@ -32,7 +32,7 @@
<string name="text_hint_password">Enter your password</string> <string name="text_hint_password">Enter your password</string>
<string name="sign_up_upper">Sign Up</string> <string name="sign_up_upper">Sign Up</string>
<string name="sign_in_with_email">Connect with Email</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="back_upper">BACK</string>
<string name="text_hint_confirm_password">Enter your password again</string> <string name="text_hint_confirm_password">Enter your password again</string>
<string name="login_confirm_password_label">Confirm password</string> <string name="login_confirm_password_label">Confirm password</string>