Refactor: Add debounce for navigation and optimize comments loading
- Implemented debounced navigation to prevent multiple rapid navigations. - Replaced Pager-based comment loading with a simpler list-based approach for improved performance and reduced complexity. - Added loading and error states for comment fetching. - Introduced `debouncedClickable` modifier for handling click events with debounce. - Updated image viewer to use simple navigation arrows instead of HorizontalPager for better user experience. - Added a new string resource for password length error.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package com.aiosman.ravenow.ui.composables
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 防抖点击修饰符
|
||||
* @param enabled 是否启用点击
|
||||
* @param debounceTime 防抖时间(毫秒),默认500ms
|
||||
* @param onClick 点击回调
|
||||
*/
|
||||
fun Modifier.debouncedClickable(
|
||||
enabled: Boolean = true,
|
||||
debounceTime: Long = 500L,
|
||||
onClick: () -> Unit
|
||||
): Modifier = composed(
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "debouncedClickable"
|
||||
properties["enabled"] = enabled
|
||||
properties["debounceTime"] = debounceTime
|
||||
}
|
||||
) {
|
||||
var isClickable by remember { mutableStateOf(true) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
clickable(
|
||||
enabled = enabled && isClickable,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {
|
||||
if (isClickable) {
|
||||
isClickable = false
|
||||
onClick()
|
||||
scope.launch {
|
||||
delay(debounceTime)
|
||||
isClickable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖点击修饰符(带涟漪效果)
|
||||
* @param enabled 是否启用点击
|
||||
* @param debounceTime 防抖时间(毫秒),默认500ms
|
||||
* @param onClick 点击回调
|
||||
*/
|
||||
fun Modifier.debouncedClickableWithRipple(
|
||||
enabled: Boolean = true,
|
||||
debounceTime: Long = 500L,
|
||||
onClick: () -> Unit
|
||||
): Modifier = composed(
|
||||
inspectorInfo = debugInspectorInfo {
|
||||
name = "debouncedClickableWithRipple"
|
||||
properties["enabled"] = enabled
|
||||
properties["debounceTime"] = debounceTime
|
||||
}
|
||||
) {
|
||||
var isClickable by remember { mutableStateOf(true) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
clickable(
|
||||
enabled = enabled && isClickable,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = androidx.compose.material.ripple.rememberRipple()
|
||||
) {
|
||||
if (isClickable) {
|
||||
isClickable = false
|
||||
onClick()
|
||||
scope.launch {
|
||||
delay(debounceTime)
|
||||
isClickable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用防抖处理器
|
||||
* 可以用于任何需要防抖的场景
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDebouncer(debounceTime: Long = 500L): ((() -> Unit) -> Unit) {
|
||||
var isExecuting by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
return remember {
|
||||
{ action ->
|
||||
if (!isExecuting) {
|
||||
isExecuting = true
|
||||
action()
|
||||
scope.launch {
|
||||
delay(debounceTime)
|
||||
isExecuting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖状态管理器
|
||||
* 可以手动控制防抖状态
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDebouncedState(
|
||||
debounceTime: Long = 500L
|
||||
): Triple<Boolean, () -> Unit, () -> Unit> {
|
||||
var isDebouncing by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val startDebounce: () -> Unit = remember {
|
||||
{
|
||||
isDebouncing = true
|
||||
scope.launch {
|
||||
delay(debounceTime)
|
||||
isDebouncing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val reset = remember {
|
||||
{
|
||||
isDebouncing = false
|
||||
}
|
||||
}
|
||||
|
||||
return Triple(isDebouncing, startDebounce, reset)
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖的导航处理器
|
||||
* 专门用于导航操作的防抖
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDebouncedNavigation(
|
||||
debounceTime: Long = 1000L
|
||||
): ((() -> Unit) -> Unit) {
|
||||
var isNavigating by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
return remember {
|
||||
{ navigation ->
|
||||
if (!isNavigating) {
|
||||
isNavigating = true
|
||||
try {
|
||||
navigation()
|
||||
} finally {
|
||||
scope.launch {
|
||||
delay(debounceTime)
|
||||
isNavigating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,48 +5,43 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.filter
|
||||
import androidx.paging.map
|
||||
import com.aiosman.ravenow.data.CommentRemoteDataSource
|
||||
import com.aiosman.ravenow.data.CommentService
|
||||
import com.aiosman.ravenow.data.CommentServiceImpl
|
||||
import com.aiosman.ravenow.entity.CommentEntity
|
||||
import com.aiosman.ravenow.entity.CommentPagingSource
|
||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CommentsViewModel(
|
||||
var postId: String = 0.toString(),
|
||||
) : ViewModel() {
|
||||
var commentService: CommentService = CommentServiceImpl()
|
||||
private var _commentsFlow = MutableStateFlow<PagingData<CommentEntity>>(PagingData.empty())
|
||||
val commentsFlow = _commentsFlow.asStateFlow()
|
||||
var commentsList by mutableStateOf<List<CommentEntity>>(emptyList())
|
||||
var order: String by mutableStateOf("like")
|
||||
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
|
||||
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
|
||||
var highlightCommentId by mutableStateOf<Int?>(null)
|
||||
var highlightComment by mutableStateOf<CommentEntity?>(null)
|
||||
var isLoading by mutableStateOf(false)
|
||||
var hasError by mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* 预加载,在跳转到 PostScreen 之前设置好内容
|
||||
*/
|
||||
fun preTransit() {
|
||||
viewModelScope.launch {
|
||||
Pager(config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
CommentPagingSource(
|
||||
CommentRemoteDataSource(commentService),
|
||||
postId = postId.toInt()
|
||||
)
|
||||
}).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_commentsFlow.value = it
|
||||
try {
|
||||
isLoading = true
|
||||
val response = commentService.getComments(
|
||||
pageNumber = 1,
|
||||
postId = postId.toInt(),
|
||||
pageSize = 10
|
||||
)
|
||||
commentsList = response.list
|
||||
hasError = false
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
hasError = true
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,18 +52,20 @@ class CommentsViewModel(
|
||||
fun reloadComment() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
Pager(config = PagingConfig(pageSize = 20, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
CommentPagingSource(
|
||||
CommentRemoteDataSource(commentService),
|
||||
postId = postId.toInt(),
|
||||
order = order
|
||||
)
|
||||
}).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_commentsFlow.value = it
|
||||
}
|
||||
isLoading = true
|
||||
val response = commentService.getComments(
|
||||
pageNumber = 1,
|
||||
postId = postId.toInt(),
|
||||
order = order,
|
||||
pageSize = 50
|
||||
)
|
||||
commentsList = response.list
|
||||
hasError = false
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
hasError = true
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,8 +125,7 @@ class CommentsViewModel(
|
||||
* 更新评论点赞状态
|
||||
*/
|
||||
private fun updateCommentLike(commentId: Int, isLike: Boolean) {
|
||||
val currentPagingData = commentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { comment ->
|
||||
commentsList = commentsList.map { comment ->
|
||||
if (comment.id == commentId) {
|
||||
comment.copy(
|
||||
liked = isLike,
|
||||
@@ -149,7 +145,6 @@ class CommentsViewModel(
|
||||
})
|
||||
}
|
||||
}
|
||||
_commentsFlow.value = updatedPagingData
|
||||
}
|
||||
|
||||
// 用于防止重复点赞的状态集合
|
||||
@@ -270,9 +265,7 @@ class CommentsViewModel(
|
||||
if (addedCommentList.any { it.id == commentId }) {
|
||||
addedCommentList = addedCommentList.filter { it.id != commentId }
|
||||
} else {
|
||||
val currentPagingData = commentsFlow.value
|
||||
val updatedPagingData = currentPagingData.filter { it.id != commentId }
|
||||
_commentsFlow.value = updatedPagingData
|
||||
commentsList = commentsList.filter { it.id != commentId }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,8 +290,7 @@ class CommentsViewModel(
|
||||
} else {
|
||||
// 普通评论
|
||||
viewModelScope.launch {
|
||||
val currentPagingData = commentsFlow.value
|
||||
val updatedPagingData = currentPagingData.map { comment ->
|
||||
commentsList = commentsList.map { comment ->
|
||||
if (comment.id == commentId) {
|
||||
try {
|
||||
subCommentLoadingMap[commentId] = true
|
||||
@@ -321,7 +313,6 @@ class CommentsViewModel(
|
||||
}
|
||||
comment
|
||||
}
|
||||
_commentsFlow.value = updatedPagingData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,6 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerDefaults
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -87,8 +84,6 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.ConstVars
|
||||
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||
@@ -119,6 +114,8 @@ import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
||||
import com.aiosman.ravenow.ui.composables.FollowButton
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.composables.debouncedClickable
|
||||
import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation
|
||||
import com.aiosman.ravenow.utils.FileUtil.saveImageToGallery
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -147,6 +144,7 @@ fun PostScreen(
|
||||
val commentsViewModel = viewModel.commentsViewModel
|
||||
val scope = rememberCoroutineScope()
|
||||
val navController = LocalNavController.current
|
||||
val debouncedNavigation = rememberDebouncedNavigation()
|
||||
var showCommentMenu by remember { mutableStateOf(false) }
|
||||
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||
@@ -202,7 +200,13 @@ fun PostScreen(
|
||||
commentModalState.hide()
|
||||
showCommentMenu = false
|
||||
}
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
commentModalState.hide()
|
||||
@@ -228,7 +232,13 @@ fun PostScreen(
|
||||
commentModalState.hide()
|
||||
showCommentMenu = false
|
||||
}
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
commentModalState.hide()
|
||||
@@ -316,7 +326,13 @@ fun PostScreen(
|
||||
onLikeClick = {
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
if (viewModel.moment?.liked == true) {
|
||||
@@ -330,7 +346,13 @@ fun PostScreen(
|
||||
onCreateCommentClick = {
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
replyComment = null
|
||||
showCommentModal = true
|
||||
@@ -339,7 +361,13 @@ fun PostScreen(
|
||||
onFavoriteClick = {
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
if (viewModel.moment?.isFavorite == true) {
|
||||
@@ -394,7 +422,13 @@ fun PostScreen(
|
||||
onFollowClick = {
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.FOLLOW_USER)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
if (viewModel.moment?.followStatus == true) {
|
||||
@@ -407,13 +441,21 @@ fun PostScreen(
|
||||
},
|
||||
onDeleteClick = {
|
||||
viewModel.deleteMoment {
|
||||
navController.navigateUp()
|
||||
debouncedNavigation {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
},
|
||||
onReportClick = {
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.REPORT_CONTENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showReportDialog = true
|
||||
}
|
||||
@@ -478,7 +520,13 @@ fun PostScreen(
|
||||
onReply = { parentComment, _, _, _ ->
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
replyComment = parentComment
|
||||
showCommentModal = true
|
||||
@@ -506,8 +554,9 @@ fun CommentContent(
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
val navController = LocalNavController.current
|
||||
val debouncedNavigation = rememberDebouncedNavigation()
|
||||
|
||||
val commentsPagging = viewModel.commentsFlow.collectAsLazyPagingItems()
|
||||
val commentsList = viewModel.commentsList
|
||||
val addedTopLevelComment = viewModel.addedCommentList.filter {
|
||||
it.parentCommentId == null
|
||||
}
|
||||
@@ -522,7 +571,13 @@ fun CommentContent(
|
||||
onLike = { comment ->
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 使用防抖机制避免重复点击
|
||||
viewModel.viewModelScope.launch {
|
||||
@@ -540,7 +595,13 @@ fun CommentContent(
|
||||
onReply = { parentComment, _, _, _ ->
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onReply(
|
||||
parentComment,
|
||||
@@ -577,7 +638,13 @@ fun CommentContent(
|
||||
onLike = { comment ->
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 防抖机制已在ViewModel中实现
|
||||
viewModel.viewModelScope.launch {
|
||||
@@ -612,8 +679,7 @@ fun CommentContent(
|
||||
}
|
||||
}
|
||||
|
||||
for (idx in 0 until commentsPagging.itemCount) {
|
||||
val item = commentsPagging[idx] ?: return
|
||||
commentsList.forEach { item ->
|
||||
if (
|
||||
item.id != viewModel.highlightCommentId &&
|
||||
viewModel.addedCommentList.none { it.id == item.id }
|
||||
@@ -631,7 +697,13 @@ fun CommentContent(
|
||||
onLike = { comment ->
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 防抖机制已在ViewModel中实现
|
||||
viewModel.viewModelScope.launch {
|
||||
@@ -649,7 +721,13 @@ fun CommentContent(
|
||||
onReply = { parentComment, _, _, _ ->
|
||||
// 检查游客模式,如果是游客则跳转登录
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onReply(
|
||||
parentComment,
|
||||
@@ -669,47 +747,35 @@ fun CommentContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Handle loading and error states as before
|
||||
// if (commentsPagging.loadState.refresh is LoadState.Loading) {
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .fillMaxSize()
|
||||
// .height(120.dp),
|
||||
// contentAlignment = Alignment.Center
|
||||
// ) {
|
||||
// Column(
|
||||
// horizontalAlignment = Alignment.CenterHorizontally
|
||||
// ) {
|
||||
// LinearProgressIndicator(
|
||||
// modifier = Modifier.width(160.dp),
|
||||
// color = AppColors.main
|
||||
// )
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
// Text(
|
||||
// text = "Loading...",
|
||||
// fontSize = 14.sp
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
// if (commentsPagging.loadState.append is LoadState.Loading) {
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .fillMaxSize()
|
||||
// .height(64.dp),
|
||||
// contentAlignment = Alignment.Center
|
||||
// ) {
|
||||
// LinearProgressIndicator(
|
||||
// modifier = Modifier.width(160.dp),
|
||||
// color = AppColors.main
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
if (commentsPagging.loadState.refresh is LoadState.Error) {
|
||||
// 加载状态处理
|
||||
if (viewModel.isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.height(120.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.width(160.dp),
|
||||
color = AppColors.main
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Loading...",
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 错误状态处理
|
||||
if (viewModel.hasError) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -724,25 +790,11 @@ fun CommentContent(
|
||||
}
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (commentsPagging.loadState.append is LoadState.Error) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.height(64.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Failed to load more comments, click to retry",
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.noRippleClickable {
|
||||
commentsPagging.retry()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 评论为空
|
||||
if (commentsPagging.itemCount == 0 && commentsPagging.loadState.refresh is LoadState.NotLoading && addedTopLevelComment.isEmpty()) {
|
||||
if (commentsList.isEmpty() && addedTopLevelComment.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -791,6 +843,7 @@ fun Header(
|
||||
val AppColors = LocalAppTheme.current
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
val debouncedNavigation = rememberDebouncedNavigation()
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
if (expanded) {
|
||||
ModalBottomSheet(
|
||||
@@ -829,8 +882,10 @@ fun Header(
|
||||
painter = painterResource(id = R.drawable.rider_pro_back_icon), // Replace with your image resource
|
||||
contentDescription = "Back",
|
||||
modifier = Modifier
|
||||
.noRippleClickable {
|
||||
navController.navigateUp()
|
||||
.debouncedClickable {
|
||||
debouncedNavigation {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
.size(24.dp),
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
@@ -849,14 +904,16 @@ fun Header(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(CircleShape)
|
||||
.noRippleClickable {
|
||||
.debouncedClickable(debounceTime = 1000L) {
|
||||
userId?.let {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
userId.toString()
|
||||
debouncedNavigation {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
userId.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
@@ -897,7 +954,7 @@ fun Header(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ImageViewerDialog(
|
||||
images: List<MomentImageEntity>,
|
||||
@@ -906,11 +963,12 @@ fun ImageViewerDialog(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val pagerState = rememberPagerState(pageCount = { images.size }, initialPage = initialPage)
|
||||
var currentPage by remember { mutableStateOf(initialPage) }
|
||||
val navigationBarPaddings =
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 16.dp
|
||||
val showRawImageStates = remember { mutableStateListOf(*Array(images.size) { false }) }
|
||||
var isDownloading by remember { mutableStateOf(false) }
|
||||
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = {
|
||||
onDismiss()
|
||||
@@ -921,8 +979,6 @@ fun ImageViewerDialog(
|
||||
dismissOnClickOutside = true,
|
||||
usePlatformDefaultWidth = false,
|
||||
)
|
||||
|
||||
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -930,16 +986,15 @@ fun ImageViewerDialog(
|
||||
.background(Color.Black),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(0.8f),
|
||||
) { page ->
|
||||
.weight(0.8f)
|
||||
) {
|
||||
val zoomState = rememberZoomState()
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
if (showRawImageStates[page]) images[page].url else images[page].thumbnail,
|
||||
if (showRawImageStates[currentPage]) images[currentPage].url else images[currentPage].thumbnail,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -951,7 +1006,49 @@ fun ImageViewerDialog(
|
||||
),
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
|
||||
// Navigation arrows
|
||||
if (images.size > 1) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Left arrow
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.noRippleClickable {
|
||||
if (currentPage > 0) {
|
||||
currentPage--
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (currentPage > 0) {
|
||||
Text("<", color = Color.White, fontSize = 24.sp)
|
||||
}
|
||||
}
|
||||
|
||||
// Right arrow
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.noRippleClickable {
|
||||
if (currentPage < images.size - 1) {
|
||||
currentPage++
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (currentPage < images.size - 1) {
|
||||
Text(">", color = Color.White, fontSize = 24.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.padding(top = 10.dp, bottom = 10.dp)) {
|
||||
if (images.size > 1) {
|
||||
Box(
|
||||
@@ -961,7 +1058,7 @@ fun ImageViewerDialog(
|
||||
.padding(vertical = 4.dp, horizontal = 24.dp)
|
||||
) {
|
||||
androidx.compose.material.Text(
|
||||
text = "${pagerState.currentPage + 1}/${images.size}",
|
||||
text = "${currentPage + 1}/${images.size}",
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
@@ -998,7 +1095,7 @@ fun ImageViewerDialog(
|
||||
}
|
||||
isDownloading = true
|
||||
scope.launch {
|
||||
saveImageToGallery(context, images[pagerState.currentPage].url)
|
||||
saveImageToGallery(context, images[currentPage].url)
|
||||
isDownloading = false
|
||||
}
|
||||
}
|
||||
@@ -1023,15 +1120,15 @@ fun ImageViewerDialog(
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
if (!showRawImageStates[pagerState.currentPage]) {
|
||||
if (!showRawImageStates[currentPage]) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
if (!showRawImageStates[pagerState.currentPage]) {
|
||||
if (!showRawImageStates[currentPage]) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.noRippleClickable {
|
||||
showRawImageStates[pagerState.currentPage] = true
|
||||
showRawImageStates[currentPage] = true
|
||||
}
|
||||
) {
|
||||
androidx.compose.material.Icon(
|
||||
@@ -1053,94 +1150,112 @@ fun ImageViewerDialog(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun PostImageView(
|
||||
images: List<MomentImageEntity>,
|
||||
initialPage: Int? = 0
|
||||
) {
|
||||
val pagerState = rememberPagerState(pageCount = { images.size }, initialPage = initialPage ?: 0)
|
||||
val context = LocalContext.current
|
||||
var isImageViewerDialog by remember { mutableStateOf(false) }
|
||||
var currentImageIndex by remember { mutableStateOf(initialPage ?: 0) }
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
isImageViewerDialog = false
|
||||
}
|
||||
}
|
||||
|
||||
if (isImageViewerDialog) {
|
||||
ImageViewerDialog(
|
||||
images = images,
|
||||
initialPage = pagerState.currentPage
|
||||
initialPage = currentImageIndex
|
||||
) {
|
||||
isImageViewerDialog = false
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
isImageViewerDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
.background(Color.Gray.copy(alpha = 0.1f)),
|
||||
flingBehavior = PagerDefaults.flingBehavior(
|
||||
state = pagerState,
|
||||
snapAnimationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioNoBouncy,
|
||||
stiffness = Spring.StiffnessMedium,
|
||||
)
|
||||
)
|
||||
|
||||
) { page ->
|
||||
val image = images[page]
|
||||
if (images.isNotEmpty()) {
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
image.thumbnail,
|
||||
images[currentImageIndex].thumbnail,
|
||||
contentDescription = "Image",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onTap = {
|
||||
isImageViewerDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
.background(Color.Gray.copy(alpha = 0.1f))
|
||||
)
|
||||
}
|
||||
|
||||
// Indicator container
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
if (images.size > 1) {
|
||||
images.forEachIndexed { index, _ ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (pagerState.currentPage == index) Color.Red else Color.Gray.copy(
|
||||
alpha = 0.5f
|
||||
// Navigation and Indicator container
|
||||
if (images.size > 1) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Previous button
|
||||
Text(
|
||||
text = "Previous",
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.noRippleClickable {
|
||||
if (currentImageIndex > 0) {
|
||||
currentImageIndex--
|
||||
}
|
||||
},
|
||||
color = if (currentImageIndex > 0) Color.Blue else Color.Gray
|
||||
)
|
||||
|
||||
// Indicators
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
images.forEachIndexed { index, _ ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (currentImageIndex == index) Color.Red else Color.Gray.copy(
|
||||
alpha = 0.5f
|
||||
)
|
||||
)
|
||||
)
|
||||
.padding(4.dp)
|
||||
|
||||
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
.padding(4.dp)
|
||||
)
|
||||
if (index < images.size - 1) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next button
|
||||
Text(
|
||||
text = "Next",
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.noRippleClickable {
|
||||
if (currentImageIndex < images.size - 1) {
|
||||
currentImageIndex++
|
||||
}
|
||||
},
|
||||
color = if (currentImageIndex < images.size - 1) Color.Blue else Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1189,6 +1304,7 @@ fun CommentItem(
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val debouncedNavigation = rememberDebouncedNavigation()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -1199,21 +1315,24 @@ fun CommentItem(
|
||||
.size(if (isChild) 24.dp else 32.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Gray.copy(alpha = 0.1f))
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
commentEntity.author.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = commentEntity.avatar,
|
||||
contentDescription = "Comment Profile Picture",
|
||||
modifier = Modifier.size(if (isChild) 24.dp else 32.dp)
|
||||
.clip(CircleShape),
|
||||
contentDescription = "Comment Profile Picture ${commentEntity.name}",
|
||||
modifier = Modifier
|
||||
.size(if (isChild) 24.dp else 32.dp)
|
||||
.clip(CircleShape)
|
||||
.debouncedClickable(debounceTime = 1000L) {
|
||||
debouncedNavigation {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
commentEntity.author.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
@@ -1273,12 +1392,14 @@ fun CommentItem(
|
||||
start = offset,
|
||||
end = offset
|
||||
).firstOrNull()?.let {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
it.item
|
||||
debouncedNavigation {
|
||||
navController.navigate(
|
||||
NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
it.item
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
style = TextStyle(fontSize = 14.sp, color = AppColors.text),
|
||||
|
||||
Reference in New Issue
Block a user