修复多个bug、更改评论筛选

-修复搜索/动态/关注界面可以同时点赞/收藏同一个动态,使总点赞/收藏数增加
-修复个人主页上滑时动态/智能体/群聊图标会被遮挡
-评论显示不全,只显示50条内容(现在滑动到第50条评论后会出现加载更多按键)
-评论筛选改为全部和最新和热门
-修复评论筛选图标自动发生改变
This commit is contained in:
2025-11-27 18:38:18 +08:00
parent 05615ce5dc
commit 8937ccbf56
12 changed files with 360 additions and 80 deletions

View File

@@ -410,7 +410,16 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
fun updateMomentLike(id: Int,isLike:Boolean) { fun updateMomentLike(id: Int,isLike:Boolean) {
this.list = this.list.map { momentItem -> this.list = this.list.map { momentItem ->
if (momentItem.id == id) { if (momentItem.id == id) {
momentItem.copy(likeCount = momentItem.likeCount + if (isLike) 1 else -1, liked = isLike) // 只有当状态发生变化时才更新计数,避免重复更新
val countDelta = if (momentItem.liked != isLike) {
if (isLike) 1 else -1
} else {
0
}
momentItem.copy(
likeCount = (momentItem.likeCount + countDelta).coerceAtLeast(0),
liked = isLike
)
} else { } else {
momentItem momentItem
} }
@@ -421,7 +430,16 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
fun updateFavoriteCount(id: Int,isFavorite:Boolean) { fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
this.list = this.list.map { momentItem -> this.list = this.list.map { momentItem ->
if (momentItem.id == id) { if (momentItem.id == id) {
momentItem.copy(favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1, isFavorite = isFavorite) // 只有当状态发生变化时才更新计数,避免重复更新
val countDelta = if (momentItem.isFavorite != isFavorite) {
if (isFavorite) 1 else -1
} else {
0
}
momentItem.copy(
favoriteCount = (momentItem.favoriteCount + countDelta).coerceAtLeast(0),
isFavorite = isFavorite
)
} else { } else {
momentItem momentItem
} }

View File

@@ -208,7 +208,9 @@ fun CommentModalContent(
fontSize = 14.sp, fontSize = 14.sp,
color = AppColors.secondaryText color = AppColors.secondaryText
) )
OrderSelectionComponent { OrderSelectionComponent(
selectedOrder = commentViewModel.order
) {
commentViewModel.order = it commentViewModel.order = it
commentViewModel.reloadComment() commentViewModel.reloadComment()
} }

View File

@@ -64,12 +64,36 @@ open class BaseMomentModel :ViewModel(){
momentLoader.updateMomentLike(event.postId, event.isLike) momentLoader.updateMomentLike(event.postId, event.isLike)
} }
suspend fun likeMoment(id: Int) { suspend fun likeMoment(id: Int) {
// 获取当前动态信息,用于计算新的点赞数
val currentMoment = momentLoader.list.find { it.id == id }
val newLikeCount = (currentMoment?.likeCount ?: 0) + 1
momentService.likeMoment(id) momentService.likeMoment(id)
momentLoader.updateMomentLike(id, true)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = newLikeCount,
isLike = true
)
)
} }
suspend fun dislikeMoment(id: Int) { suspend fun dislikeMoment(id: Int) {
// 获取当前动态信息,用于计算新的点赞数
val currentMoment = momentLoader.list.find { it.id == id }
val newLikeCount = ((currentMoment?.likeCount ?: 0) - 1).coerceAtLeast(0)
momentService.dislikeMoment(id) momentService.dislikeMoment(id)
momentLoader.updateMomentLike(id, false)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = newLikeCount,
isLike = false
)
)
} }
@@ -90,14 +114,27 @@ open class BaseMomentModel :ViewModel(){
suspend fun favoriteMoment(id: Int) { suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id) momentService.favoriteMoment(id)
momentLoader.updateFavoriteCount(id, true)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = true
)
)
} }
suspend fun unfavoriteMoment(id: Int) { suspend fun unfavoriteMoment(id: Int) {
momentService.unfavoriteMoment(id) momentService.unfavoriteMoment(id)
momentLoader.updateFavoriteCount(id, false)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = false
)
)
} }
@Subscribe @Subscribe

View File

@@ -226,7 +226,9 @@ fun NewsCommentModal(
.fillMaxWidth(), .fillMaxWidth(),
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
OrderSelectionComponent { OrderSelectionComponent(
selectedOrder = commentViewModel.order
) {
commentViewModel.order = it commentViewModel.order = it
commentViewModel.reloadComment() commentViewModel.reloadComment()
} }

View File

@@ -52,6 +52,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -59,6 +60,8 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -85,6 +88,8 @@ import com.aiosman.ravenow.ui.index.IndexViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
import com.aiosman.ravenow.ui.index.tabs.profile.composable.SegmentedControl
import com.aiosman.ravenow.ui.index.tabs.profile.composable.AgentSegmentedControl
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
@@ -168,6 +173,33 @@ fun ProfileV3(
initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset
) )
val scrollState = rememberScrollState(model.profileScrollOffset) val scrollState = rememberScrollState(model.profileScrollOffset)
var tabIndicatorContentOffset by remember { mutableStateOf<Float?>(null) }
var tabIndicatorHeightPx by remember { mutableStateOf(0) }
val topNavigationBarHeightPx = with(density) { (statusBarPaddingValues.calculateTopPadding() + 56.dp).toPx() }
val stickyTopPadding = statusBarPaddingValues.calculateTopPadding() + 56.dp
var agentSegmentOffset by remember { mutableStateOf<Float?>(null) }
var agentSegmentHeightPx by remember { mutableStateOf(0) }
var groupSegmentOffset by remember { mutableStateOf<Float?>(null) }
var groupSegmentHeightPx by remember { mutableStateOf(0) }
var agentSegmentSelected by remember { mutableStateOf(0) }
var groupSegmentSelected by remember { mutableStateOf(0) }
val tabIndicatorHeightDp = with(density) { tabIndicatorHeightPx.toDp() }
val tabBarBottomPx = topNavigationBarHeightPx + tabIndicatorHeightPx
val tabBarBottomPadding = stickyTopPadding + tabIndicatorHeightDp
val tabStickyThreshold = remember(tabIndicatorContentOffset, topNavigationBarHeightPx) {
tabIndicatorContentOffset?.minus(topNavigationBarHeightPx)
}
val agentSegmentThreshold = remember(agentSegmentOffset, tabBarBottomPx) {
agentSegmentOffset?.minus(tabBarBottomPx)
}
val groupSegmentThreshold = remember(groupSegmentOffset, tabBarBottomPx) {
groupSegmentOffset?.minus(tabBarBottomPx)
}
val agentTabIndex = if (isAiAccount) -1 else 1
val groupTabIndex = if (isAiAccount) 1 else 2
val shouldStickTabBar = tabStickyThreshold?.let { scrollState.value >= it } ?: false
val shouldStickAgentSegments = isSelf && !isAiAccount && agentSegmentThreshold?.let { scrollState.value >= it } == true && pagerState.currentPage == agentTabIndex
val shouldStickGroupSegments = isSelf && groupSegmentThreshold?.let { scrollState.value >= it } == true && pagerState.currentPage == groupTabIndex
val externalOwnerSessionId = remember(isSelf, profile?.chatAIId, profile?.trtcUserId) { val externalOwnerSessionId = remember(isSelf, profile?.chatAIId, profile?.trtcUserId) {
if (isSelf) { if (isSelf) {
null null
@@ -499,11 +531,20 @@ fun ProfileV3(
.fillMaxWidth() .fillMaxWidth()
.background(AppColors.profileBackground) .background(AppColors.profileBackground)
.padding(top = 8.dp) .padding(top = 8.dp)
) {
Box(
modifier = Modifier
.onGloballyPositioned { coordinates ->
tabIndicatorHeightPx = coordinates.size.height
tabIndicatorContentOffset = coordinates.positionInRoot().y + scrollState.value
}
.alpha(if (shouldStickTabBar) 0f else 1f)
) { ) {
UserContentPageIndicator( UserContentPageIndicator(
pagerState = pagerState, pagerState = pagerState,
showAgentTab = !isAiAccount showAgentTab = !isAiAccount
) )
}
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.height(650.dp) // 固定滚动高度 modifier = Modifier.height(650.dp) // 固定滚动高度
@@ -542,7 +583,15 @@ fun ProfileV3(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState, state = listState,
nestedScrollConnection = nestedScrollConnection, nestedScrollConnection = nestedScrollConnection,
showSegments = isSelf // 只有查看自己的主页时才显示分段控制器 showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
segmentSelectedIndex = agentSegmentSelected,
onSegmentSelected = { agentSegmentSelected = it },
onSegmentMeasured = { offset, height ->
agentSegmentOffset = offset
agentSegmentHeightPx = height
},
isSegmentSticky = shouldStickAgentSegments,
parentScrollProvider = { scrollState.value }
) )
} else { } else {
// 查看其他用户的主页时传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null // 查看其他用户的主页时传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
@@ -551,7 +600,15 @@ fun ProfileV3(
listState = groupChatListState, listState = groupChatListState,
nestedScrollConnection = nestedScrollConnection, nestedScrollConnection = nestedScrollConnection,
ownerSessionId = externalOwnerSessionId, ownerSessionId = externalOwnerSessionId,
showSegments = isSelf // 只有查看自己的主页时才显示分段控制器 showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
selectedSegmentIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
onSegmentMeasured = { offset, height ->
groupSegmentOffset = offset
groupSegmentHeightPx = height
},
isSegmentSticky = shouldStickGroupSegments,
parentScrollProvider = { scrollState.value }
) )
} }
} }
@@ -563,7 +620,15 @@ fun ProfileV3(
listState = groupChatListState, listState = groupChatListState,
nestedScrollConnection = nestedScrollConnection, nestedScrollConnection = nestedScrollConnection,
ownerSessionId = externalOwnerSessionId, ownerSessionId = externalOwnerSessionId,
showSegments = isSelf // 只有查看自己的主页时才显示分段控制器 showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
selectedSegmentIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
onSegmentMeasured = { offset, height ->
groupSegmentOffset = offset
groupSegmentHeightPx = height
},
isSegmentSticky = shouldStickGroupSegments,
parentScrollProvider = { scrollState.value }
) )
} }
} }
@@ -575,6 +640,55 @@ fun ProfileV3(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
if (shouldStickTabBar && tabIndicatorHeightPx > 0) {
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.padding(top = stickyTopPadding)
.background(AppColors.profileBackground)
) {
UserContentPageIndicator(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
}
}
if (shouldStickAgentSegments && agentSegmentHeightPx > 0) {
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.padding(top = tabBarBottomPadding)
.background(AppColors.profileBackground)
.padding(horizontal = 16.dp)
) {
AgentSegmentedControl(
selectedIndex = agentSegmentSelected,
onSegmentSelected = { agentSegmentSelected = it },
modifier = Modifier.fillMaxWidth()
)
}
}
if (shouldStickGroupSegments && groupSegmentHeightPx > 0) {
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.padding(top = tabBarBottomPadding)
.background(AppColors.profileBackground)
.padding(horizontal = 16.dp)
) {
SegmentedControl(
selectedIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
modifier = Modifier.fillMaxWidth()
)
}
}
// 顶部导航栏 // 顶部导航栏
TopNavigationBar( TopNavigationBar(
isMain = isMain, isMain = isMain,
@@ -673,14 +787,24 @@ private fun GroupChatPlaceholder(
listState: androidx.compose.foundation.lazy.LazyListState, listState: androidx.compose.foundation.lazy.LazyListState,
nestedScrollConnection: NestedScrollConnection? = null, nestedScrollConnection: NestedScrollConnection? = null,
ownerSessionId: String? = null, // 创建者用户IDChatAIID用于过滤特定创建者的房间。如果为null则显示当前用户创建或加入的房间 ownerSessionId: String? = null, // 创建者用户IDChatAIID用于过滤特定创建者的房间。如果为null则显示当前用户创建或加入的房间
showSegments: Boolean = true // 是否显示分段控制器(全部、公开、私有) showSegments: Boolean = true,
selectedSegmentIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) { ) {
GroupChatEmptyContent( GroupChatEmptyContent(
modifier = modifier, modifier = modifier,
listState = listState, listState = listState,
nestedScrollConnection = nestedScrollConnection, nestedScrollConnection = nestedScrollConnection,
ownerSessionId = ownerSessionId, ownerSessionId = ownerSessionId,
showSegments = showSegments showSegments = showSegments,
selectedSegmentIndex = selectedSegmentIndex,
onSegmentSelected = onSegmentSelected,
onSegmentMeasured = onSegmentMeasured,
isSegmentSticky = isSegmentSticky,
parentScrollProvider = parentScrollProvider
) )
} }

View File

@@ -41,6 +41,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -74,9 +76,13 @@ fun GroupChatEmptyContent(
listState: LazyListState, listState: LazyListState,
nestedScrollConnection: NestedScrollConnection? = null, nestedScrollConnection: NestedScrollConnection? = null,
ownerSessionId: String? = null, // 创建者用户IDChatAIID用于过滤特定创建者的房间。如果为null则显示当前用户创建或加入的房间 ownerSessionId: String? = null, // 创建者用户IDChatAIID用于过滤特定创建者的房间。如果为null则显示当前用户创建或加入的房间
showSegments: Boolean = true // 是否显示分段控制器(全部、公开、私有) showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
selectedSegmentIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) { ) {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
@@ -86,7 +92,7 @@ fun GroupChatEmptyContent(
val networkAvailable = isNetworkAvailable(context) val networkAvailable = isNetworkAvailable(context)
// 如果查看其他用户的房间固定使用全部类型filterType = 0 // 如果查看其他用户的房间固定使用全部类型filterType = 0
val filterType = if (showSegments) selectedSegment else 0 val filterType = if (showSegments) selectedSegmentIndex else 0
val state = rememberPullRefreshState( val state = rememberPullRefreshState(
refreshing = if (canLoadRooms) viewModel.roomsRefreshing else false, refreshing = if (canLoadRooms) viewModel.roomsRefreshing else false,
@@ -98,7 +104,7 @@ fun GroupChatEmptyContent(
) )
// 当分段或用户ID改变时重新加载数据 // 当分段或用户ID改变时重新加载数据
LaunchedEffect(selectedSegment, normalizedOwnerSessionId, showSegments) { LaunchedEffect(selectedSegmentIndex, normalizedOwnerSessionId, showSegments) {
if (canLoadRooms) { if (canLoadRooms) {
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId) viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
} }
@@ -120,12 +126,17 @@ fun GroupChatEmptyContent(
// 只在查看自己的房间时显示分段控制器 // 只在查看自己的房间时显示分段控制器
if (showSegments) { if (showSegments) {
SegmentedControl( SegmentedControl(
selectedIndex = selectedSegment, selectedIndex = selectedSegmentIndex,
onSegmentSelected = { onSegmentSelected = onSegmentSelected,
selectedSegment = it modifier = Modifier
// LaunchedEffect 会监听 selectedSegment 的变化并自动刷新 .fillMaxWidth()
}, .onGloballyPositioned { coordinates ->
modifier = Modifier.fillMaxWidth() onSegmentMeasured?.invoke(
coordinates.positionInRoot().y + parentScrollProvider(),
coordinates.size.height
)
}
.alpha(if (isSegmentSticky) 0f else 1f)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -445,7 +456,7 @@ fun RoomItem(
} }
@Composable @Composable
private fun SegmentedControl( fun SegmentedControl(
selectedIndex: Int, selectedIndex: Int,
onSegmentSelected: (Int) -> Unit, onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier

View File

@@ -4,7 +4,6 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -30,9 +29,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -53,6 +55,7 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.network.ReloadButton import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.DebounceUtils import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
@@ -68,7 +71,12 @@ fun UserAgentsList(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
state: LazyListState, state: LazyListState,
nestedScrollConnection: NestedScrollConnection? = null, nestedScrollConnection: NestedScrollConnection? = null,
showSegments: Boolean = true // 是否显示分段控制器(全部、公开、私有) showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
segmentSelectedIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val listModifier = if (nestedScrollConnection != null) { val listModifier = if (nestedScrollConnection != null) {
@@ -82,7 +90,14 @@ fun UserAgentsList(
Box( Box(
modifier = listModifier.fillMaxSize() modifier = listModifier.fillMaxSize()
) { ) {
AgentEmptyContentWithSegments(showSegments = showSegments) AgentEmptyContentWithSegments(
showSegments = showSegments,
segmentSelectedIndex = segmentSelectedIndex,
onSegmentSelected = onSegmentSelected,
onSegmentMeasured = onSegmentMeasured,
isSegmentSticky = isSegmentSticky,
parentScrollProvider = parentScrollProvider
)
} }
} else { } else {
LazyColumn( LazyColumn(
@@ -251,9 +266,13 @@ fun UserAgentCard(
@Composable @Composable
fun AgentEmptyContentWithSegments( fun AgentEmptyContentWithSegments(
showSegments: Boolean = true // 是否显示分段控制器(全部、公开、私有) showSegments: Boolean = true,
segmentSelectedIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) { ) {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
@@ -267,9 +286,17 @@ fun AgentEmptyContentWithSegments(
// 只在查看自己的智能体时显示分段控制器 // 只在查看自己的智能体时显示分段控制器
if (showSegments) { if (showSegments) {
AgentSegmentedControl( AgentSegmentedControl(
selectedIndex = selectedSegment, selectedIndex = segmentSelectedIndex,
onSegmentSelected = { selectedSegment = it }, onSegmentSelected = onSegmentSelected,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
onSegmentMeasured?.invoke(
coordinates.positionInRoot().y + parentScrollProvider(),
coordinates.size.height
)
}
.alpha(if (isSegmentSticky) 0f else 1f)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -321,7 +348,7 @@ fun AgentEmptyContentWithSegments(
} }
@Composable @Composable
private fun AgentSegmentedControl( fun AgentSegmentedControl(
selectedIndex: Int, selectedIndex: Int,
onSegmentSelected: (Int) -> Unit, onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier

View File

@@ -13,37 +13,30 @@ import kotlinx.coroutines.launch
class CommentsViewModel( class CommentsViewModel(
var postId: String = 0.toString(), var postId: String = 0.toString(),
) : ViewModel() { ) : ViewModel() {
companion object {
private const val ORDER_ALL = "all"
private const val COMMENTS_PAGE_SIZE = 50
}
var commentService: CommentService = CommentServiceImpl() var commentService: CommentService = CommentServiceImpl()
var commentsList by mutableStateOf<List<CommentEntity>>(emptyList()) var commentsList by mutableStateOf<List<CommentEntity>>(emptyList())
var order: String by mutableStateOf("like") var order: String by mutableStateOf(ORDER_ALL)
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList()) var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>()) var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
var highlightCommentId by mutableStateOf<Int?>(null) var highlightCommentId by mutableStateOf<Int?>(null)
var highlightComment by mutableStateOf<CommentEntity?>(null) var highlightComment by mutableStateOf<CommentEntity?>(null)
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
var hasError by mutableStateOf(false) var hasError by mutableStateOf(false)
var isLoadingMore by mutableStateOf(false)
var hasMore by mutableStateOf(false)
private var currentPage by mutableStateOf(0)
private var totalComments by mutableStateOf(0)
/** /**
* 预加载,在跳转到 PostScreen 之前设置好内容 * 预加载,在跳转到 PostScreen 之前设置好内容
*/ */
fun preTransit() { fun preTransit() {
viewModelScope.launch { reloadComment()
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
}
}
} }
/** /**
@@ -51,25 +44,61 @@ class CommentsViewModel(
*/ */
fun reloadComment() { fun reloadComment() {
viewModelScope.launch { viewModelScope.launch {
loadComments(page = 1, reset = true)
}
}
fun loadMoreComments() {
if (isLoading || isLoadingMore || !hasMore) {
return
}
viewModelScope.launch {
loadComments(page = currentPage + 1, reset = false)
}
}
private suspend fun loadComments(page: Int, reset: Boolean) {
try { try {
if (reset) {
isLoading = true isLoading = true
val response = commentService.getComments(
pageNumber = 1,
postId = postId.toInt(),
order = order,
pageSize = 50
)
commentsList = response.list
hasError = false hasError = false
} else {
isLoadingMore = true
}
val response = commentService.getComments(
pageNumber = page,
postId = postId.toInt(),
order = normalizeOrder(order),
pageSize = COMMENTS_PAGE_SIZE
)
val total = response.total.coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
totalComments = total
currentPage = response.page
commentsList = if (reset) {
response.list
} else {
commentsList + response.list
}
hasMore = commentsList.size < totalComments
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
if (reset) {
hasError = true hasError = true
commentsList = emptyList()
}
} finally { } finally {
if (reset) {
isLoading = false isLoading = false
} else {
isLoadingMore = false
} }
} }
} }
private fun normalizeOrder(currentOrder: String): String? {
return currentOrder.takeUnless { it.equals(ORDER_ALL, ignoreCase = true) }
}
suspend fun highlightComment(commentId: Int) { suspend fun highlightComment(commentId: Int) {
highlightCommentId = commentId highlightCommentId = commentId

View File

@@ -495,7 +495,9 @@ fun PostScreen(
color = AppColors.nonActiveText color = AppColors.nonActiveText
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
OrderSelectionComponent() { OrderSelectionComponent(
selectedOrder = commentsViewModel.order
) {
commentsViewModel.order = it commentsViewModel.order = it
viewModel.reloadComment() viewModel.reloadComment()
} }
@@ -742,6 +744,33 @@ fun CommentContent(
} }
} }
if (viewModel.isLoadingMore || viewModel.hasMore) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
if (viewModel.isLoadingMore) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = AppColors.main,
strokeWidth = 2.dp
)
} else {
Text(
text = stringResource(id = R.string.load_more),
color = AppColors.main,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.noRippleClickable {
viewModel.loadMoreComments()
}
)
}
}
}
// 加载状态处理 // 加载状态处理
if (viewModel.isLoading) { if (viewModel.isLoading) {
Box( Box(
@@ -1909,15 +1938,15 @@ fun CommentMenuModal(
@Composable @Composable
fun OrderSelectionComponent( fun OrderSelectionComponent(
selectedOrder: String,
onSelected: (String) -> Unit = {} onSelected: (String) -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
var selectedOrder by remember { mutableStateOf("like") }
val orders = listOf( val orders = listOf(
"like" to stringResource(R.string.order_comment_default), "all" to stringResource(R.string.order_comment_default),
"earliest" to stringResource(R.string.order_comment_earliest), "latest" to stringResource(R.string.order_comment_latest),
"latest" to stringResource(R.string.order_comment_latest) "like" to stringResource(R.string.order_comment_hot)
) )
Box( Box(
modifier = Modifier modifier = Modifier
@@ -1935,9 +1964,10 @@ fun OrderSelectionComponent(
Box( Box(
modifier = Modifier modifier = Modifier
.noRippleClickable { .noRippleClickable {
selectedOrder = order.first if (selectedOrder != order.first) {
onSelected(order.first) onSelected(order.first)
} }
}
.background( .background(
if ( if (
selectedOrder == order.first selectedOrder == order.first

View File

@@ -75,9 +75,9 @@
<string name="bio">署名</string> <string name="bio">署名</string>
<string name="nickname">名前</string> <string name="nickname">名前</string>
<string name="comment">コメント</string> <string name="comment">コメント</string>
<string name="order_comment_default">デフォルト</string> <string name="order_comment_default">すべて</string>
<string name="order_comment_latest">最新</string> <string name="order_comment_latest">最新</string>
<string name="order_comment_earliest">最も古い</string> <string name="order_comment_hot">人気</string>
<string name="download">ダウンロード</string> <string name="download">ダウンロード</string>
<string name="original">オリジナル</string> <string name="original">オリジナル</string>
<string name="favourites">お気に入り</string> <string name="favourites">お気に入り</string>

View File

@@ -80,9 +80,9 @@
<string name="bio">个性签名</string> <string name="bio">个性签名</string>
<string name="nickname">昵称</string> <string name="nickname">昵称</string>
<string name="comment">评论</string> <string name="comment">评论</string>
<string name="order_comment_default">默认</string> <string name="order_comment_default">全部</string>
<string name="order_comment_latest">最新</string> <string name="order_comment_latest">最新</string>
<string name="order_comment_earliest">最早</string> <string name="order_comment_hot">热门</string>
<string name="download">下载</string> <string name="download">下载</string>
<string name="original">原始图片</string> <string name="original">原始图片</string>
<string name="favourites">收藏</string> <string name="favourites">收藏</string>

View File

@@ -74,9 +74,9 @@
<string name="bio">Signature</string> <string name="bio">Signature</string>
<string name="nickname">Name</string> <string name="nickname">Name</string>
<string name="comment">COMMENTS</string> <string name="comment">COMMENTS</string>
<string name="order_comment_default">Default</string> <string name="order_comment_default">All</string>
<string name="order_comment_latest">Latest</string> <string name="order_comment_latest">Latest</string>
<string name="order_comment_earliest">Earliest</string> <string name="order_comment_hot">Hot</string>
<string name="download">Download</string> <string name="download">Download</string>
<string name="original">Original</string> <string name="original">Original</string>
<string name="favourites">Favourite</string> <string name="favourites">Favourite</string>