diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsCommentModal.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsCommentModal.kt new file mode 100644 index 0000000..f9c05cd --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsCommentModal.kt @@ -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( + key = "NewsCommentModalViewModel_$postId", + factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): 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(null) } + var replyComment by remember { mutableStateOf(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)) + } + } +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsScreen.kt index 08eb59b..83aad3a 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsScreen.kt @@ -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(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() } ) // 收藏 diff --git a/app/src/main/java/com/aiosman/ravenow/ui/login/login.kt b/app/src/main/java/com/aiosman/ravenow/ui/login/login.kt index 1ded727..bf3868c 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/login/login.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/login/login.kt @@ -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 diff --git a/app/src/main/java/com/aiosman/ravenow/ui/login/signup.kt b/app/src/main/java/com/aiosman/ravenow/ui/login/signup.kt index 00c29a0..63e3852 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/login/signup.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/login/signup.kt @@ -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() diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 6c8e612..0d5200f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -33,7 +33,7 @@ パスワードを入力してください サインアップ メールで接続 - Appleで接続 + Googleで接続 戻る パスワードを再入力してください パスワードの確認 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 1aa274d..a884fe0 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -32,7 +32,7 @@ 输入密码 注册 使用邮箱注册 - 使用Apple登录 + 使用Google账号登录 返回 再次输入密码 再次输入密码 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b44081..bb229c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,7 +32,7 @@ Enter your password Sign Up Connect with Email - Continue with Apple + Continue with Google BACK Enter your password again Confirm password