diff --git a/app/src/main/java/com/aiosman/ravenow/ui/composables/DebounceUtils.kt b/app/src/main/java/com/aiosman/ravenow/ui/composables/DebounceUtils.kt new file mode 100644 index 0000000..4199840 --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/composables/DebounceUtils.kt @@ -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 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 + } + } + } + } + } +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/post/CommentsViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/post/CommentsViewModel.kt index 6f717fc..bc7037a 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/post/CommentsViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/post/CommentsViewModel.kt @@ -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.empty()) - val commentsFlow = _commentsFlow.asStateFlow() + var commentsList by mutableStateOf>(emptyList()) var order: String by mutableStateOf("like") var addedCommentList by mutableStateOf>(emptyList()) var subCommentLoadingMap by mutableStateOf(mutableMapOf()) var highlightCommentId by mutableStateOf(null) var highlightComment by mutableStateOf(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 } } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/post/Post.kt b/app/src/main/java/com/aiosman/ravenow/ui/post/Post.kt index 23f86c6..13a1d2c 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/post/Post.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/post/Post.kt @@ -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(null) } var replyComment by remember { mutableStateOf(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, @@ -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, 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), diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index b3aa84f..4a5ab7e 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -189,5 +189,6 @@ 加入房间失败 创建中... 发现 + 密码不能超过 %1$d 个字符 \ No newline at end of file