登录界面UI调整;新增新闻评论

This commit is contained in:
2025-10-27 18:51:42 +08:00
parent f6a760371a
commit 7095832722
7 changed files with 356 additions and 16 deletions

View File

@@ -0,0 +1,293 @@
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 = {}
) {
val AppColors = LocalAppTheme.current
val navController = LocalNavController.current
val debouncedNavigation = rememberDebouncedNavigation()
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)
}
},
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 = "${commentCount}条评论",
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) {
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
}
Spacer(modifier = Modifier.height(navBarHeight))
}
}
}

View File

@@ -10,6 +10,7 @@ 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
@@ -20,7 +21,10 @@ 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
@@ -39,14 +43,16 @@ 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.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.exp.timeAgo
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.dynamic.DynamicViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@OptIn(ExperimentalMaterialApi::class)
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
fun NewsScreen() {
val model = DynamicViewModel
@@ -54,6 +60,10 @@ fun NewsScreen() {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
// 评论弹窗状态
var showCommentModal by remember { mutableStateOf(false) }
var selectedMoment by remember { mutableStateOf<MomentEntity?>(null) }
// 下拉刷新状态
val state = rememberPullRefreshState(model.refreshing, onRefresh = {
model.refreshPager(pullRefresh = true)
@@ -122,12 +132,46 @@ fun NewsScreen() {
NewsItem(
moment = momentItem,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
onCommentClick = {
selectedMoment = momentItem
showCommentModal = true
}
)
}
}
PullRefreshIndicator(model.refreshing, state, Modifier.align(Alignment.TopCenter))
}
// 评论弹窗
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
}
)
}
}
}
}
@@ -135,7 +179,8 @@ fun NewsScreen() {
@Composable
fun NewsItem(
moment: MomentEntity,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
onCommentClick: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
@@ -263,7 +308,8 @@ fun NewsItem(
NewsActionButton(
icon = R.mipmap.icon_comment,
count = moment.commentCount.toString(),
isActive = false
isActive = false,
modifier = Modifier.noRippleClickable { onCommentClick() }
)
// 收藏

View File

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

View File

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

View File

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

View File

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

View File

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