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:
2025-09-03 16:03:57 +08:00
parent 824be5fad8
commit d703b5ae05
4 changed files with 486 additions and 210 deletions

View File

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

View File

@@ -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()
try {
isLoading = true
val response = commentService.getComments(
pageNumber = 1,
postId = postId.toInt(),
pageSize = 10
)
}).flow.cachedIn(viewModelScope).collectLatest {
_commentsFlow.value = it
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),
isLoading = true
val response = commentService.getComments(
pageNumber = 1,
postId = postId.toInt(),
order = order
order = order,
pageSize = 50
)
}).flow.cachedIn(viewModelScope).collectLatest {
_commentsFlow.value = it
}
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
}
}

View File

@@ -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
}
debouncedNavigation {
debouncedNavigation {
debouncedNavigation {
navController.navigate(NavigationRoute.Login.route)
}
}
}
} else {
scope.launch {
commentModalState.hide()
@@ -228,7 +232,13 @@ fun PostScreen(
commentModalState.hide()
showCommentMenu = false
}
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)) {
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)) {
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)) {
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)) {
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 {
debouncedNavigation {
navController.navigateUp()
}
}
},
onReportClick = {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.REPORT_CONTENT)) {
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)) {
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)) {
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)) {
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)) {
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)) {
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)) {
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,9 +882,11 @@ fun Header(
painter = painterResource(id = R.drawable.rider_pro_back_icon), // Replace with your image resource
contentDescription = "Back",
modifier = Modifier
.noRippleClickable {
.debouncedClickable {
debouncedNavigation {
navController.navigateUp()
}
}
.size(24.dp),
colorFilter = ColorFilter.tint(AppColors.text)
)
@@ -849,8 +904,9 @@ fun Header(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.noRippleClickable {
.debouncedClickable(debounceTime = 1000L) {
userId?.let {
debouncedNavigation {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
@@ -858,6 +914,7 @@ fun Header(
)
)
}
}
},
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,33 +1150,39 @@ 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,
if (images.isNotEmpty()) {
CustomAsyncImage(
context,
images[currentImageIndex].thumbnail,
contentDescription = "Image",
contentScale = ContentScale.Crop,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
@@ -1090,57 +1193,69 @@ fun PostImageView(
}
)
}
.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]
CustomAsyncImage(
context,
image.thumbnail,
contentDescription = "Image",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.background(Color.Gray.copy(alpha = 0.1f))
)
}
// Indicator container
// 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
) {
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(
if (currentImageIndex == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
)
)
.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,7 +1315,16 @@ fun CommentItem(
.size(if (isChild) 24.dp else 32.dp)
.clip(CircleShape)
.background(Color.Gray.copy(alpha = 0.1f))
.noRippleClickable {
) {
CustomAsyncImage(
context = context,
imageUrl = commentEntity.avatar,
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}",
@@ -1207,13 +1332,7 @@ fun CommentItem(
)
)
}
) {
CustomAsyncImage(
context = context,
imageUrl = commentEntity.avatar,
contentDescription = "Comment Profile Picture",
modifier = Modifier.size(if (isChild) 24.dp else 32.dp)
.clip(CircleShape),
},
contentScale = ContentScale.Crop
)
}
@@ -1273,6 +1392,7 @@ fun CommentItem(
start = offset,
end = offset
).firstOrNull()?.let {
debouncedNavigation {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
@@ -1280,6 +1400,7 @@ fun CommentItem(
)
)
}
}
},
style = TextStyle(fontSize = 14.sp, color = AppColors.text),
onLongPress = {

View File

@@ -189,5 +189,6 @@
<string name="group_room_enter_fail">加入房间失败</string>
<string name="agent_createing">创建中...</string>
<string name="agent_find">发现</string>
<string name="text_error_password_too_long">密码不能超过 %1$d 个字符</string>
</resources>