Merge pull request #92 from Kevinlinpr/nagisa

调整界面以及修复多个bug
This commit is contained in:
2025-11-28 15:01:41 +08:00
committed by GitHub
19 changed files with 558 additions and 189 deletions

View File

@@ -54,7 +54,7 @@
android:theme="@style/Theme.App.Starting" android:theme="@style/Theme.App.Starting"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:configChanges="fontScale|orientation|screenSize|keyboardHidden"> android:configChanges="fontScale|orientation|screenSize|keyboardHidden|uiMode">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

View File

@@ -73,6 +73,10 @@ class MainActivity : ComponentActivity() {
val config = Configuration(newConfig) val config = Configuration(newConfig)
config.fontScale = 1.0f config.fontScale = 1.0f
super.onConfigurationChanged(config) super.onConfigurationChanged(config)
val isNightMode = (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
if (AppState.darkMode != isNightMode) {
syncDarkModeWithSystem(isNightMode)
}
} }
// 请求通知权限 // 请求通知权限
@@ -129,9 +133,7 @@ class MainActivity : ComponentActivity() {
JPushInterface.init(this) JPushInterface.init(this)
if (AppState.darkMode) { updateWindowBackground(AppState.darkMode)
window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
}
enableEdgeToEdge() enableEdgeToEdge()
scope.launch { scope.launch {
@@ -269,8 +271,22 @@ class MainActivity : ComponentActivity() {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
} }
private fun syncDarkModeWithSystem(isNightMode: Boolean) {
AppState.darkMode = isNightMode
AppState.appTheme = if (isNightMode) DarkThemeColors() else LightThemeColors()
AppStore.saveDarkMode(isNightMode)
updateWindowBackground(isNightMode)
}
private fun updateWindowBackground(isDarkMode: Boolean) {
window.decorView.setBackgroundColor(
if (isDarkMode) android.graphics.Color.BLACK else android.graphics.Color.WHITE
)
}
} }
val LocalNavController = compositionLocalOf<NavHostController> { val LocalNavController = compositionLocalOf<NavHostController> {
error("NavController not provided") error("NavController not provided")
} }

View File

@@ -545,7 +545,13 @@ class AccountServiceImpl : AccountService {
val bannerField: MultipartBody.Part? = banner?.let { val bannerField: MultipartBody.Part? = banner?.let {
createMultipartBody(it.file, it.filename, "banner") createMultipartBody(it.file, it.filename, "banner")
} }
ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField) val resp = ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to update profile")
}
} }
override suspend fun registerUserWithPassword(loginName: String, password: String) { override suspend fun registerUserWithPassword(loginName: String, password: String) {

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

@@ -2,6 +2,7 @@ package com.aiosman.ravenow
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration
import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.auth.api.signin.GoogleSignInOptions
/** /**
@@ -24,11 +25,16 @@ object AppStore {
.requestEmail() .requestEmail()
.build() .build()
googleSignInOptions = gso googleSignInOptions = gso
// apply dark mode // apply dark mode - 如果用户未手动设置,优先跟随系统
if (sharedPreferences.getBoolean("darkMode", false)) { val hasUserPreference = sharedPreferences.contains("darkMode")
AppState.darkMode = true val resolvedDarkMode = if (hasUserPreference) {
AppState.appTheme = DarkThemeColors() sharedPreferences.getBoolean("darkMode", false)
} else {
val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
currentNightMode == Configuration.UI_MODE_NIGHT_YES
} }
AppState.darkMode = resolvedDarkMode
AppState.appTheme = if (resolvedDarkMode) DarkThemeColors() else LightThemeColors()
// load chat background // load chat background
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null) val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)

View File

@@ -20,9 +20,8 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
@@ -219,53 +218,94 @@ fun MbtiSelectBottomSheet(
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// 不消费任何事件,让 LazyVerticalGrid 先处理 // 不消费任何事件,让 LazyColumn 先处理
return Offset.Zero return Offset.Zero
} }
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
// 消费 LazyVerticalGrid 处理后的剩余滚动事件,防止传递到 ModalBottomSheet // 消费 LazyColumn 处理后的剩余滚动事件,防止传递到 ModalBottomSheet
return available return available
} }
override suspend fun onPreFling(available: Velocity): Velocity { override suspend fun onPreFling(available: Velocity): Velocity {
// 不消费惯性滚动,让 LazyVerticalGrid 先处理 // 不消费惯性滚动,让 LazyColumn 先处理
return Velocity.Zero return Velocity.Zero
} }
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
// 消费 LazyVerticalGrid 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet // 消费 LazyColumn 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet
return available return available
} }
} }
} }
// 网格列表 - 2列 // MBTI解释文字背景色
LazyVerticalGrid( val descriptionBackgroundColor = if (isDarkMode) {
columns = GridCells.Fixed(2), Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
} else {
Color(0xFFFAF9FB)
}
// 使用LazyColumn包裹解释文字和MBTI类型网格使它们一起滚动
LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
.nestedScroll(nestedScrollConnection), .nestedScroll(nestedScrollConnection),
contentPadding = PaddingValues( contentPadding = PaddingValues(
start = 8.dp, start = 8.dp,
top = 8.dp, top = 0.dp,
end = 8.dp, end = 8.dp,
bottom = 8.dp bottom = 8.dp
), ),
horizontalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(16.dp)
verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
itemsIndexed(MBTI_TYPES) { index, mbti -> // MBTI解释文字 - 作为第一个item
MbtiItem( item {
mbti = mbti, Box(
isSelected = mbti == currentMbti, modifier = Modifier
onClick = { .fillMaxWidth()
// 保存MBTI类型 .clip(RoundedCornerShape(16.dp))
model.mbti = mbti .background(descriptionBackgroundColor)
onClose() .padding(horizontal = 16.dp, vertical = 12.dp)
) {
Text(
text = stringResource(R.string.mbti_description),
color = appColors.text,
fontSize = 14.sp,
lineHeight = 20.sp
)
}
}
// MBTI类型网格 - 手动创建2列网格布局
itemsIndexed(MBTI_TYPES.chunked(2)) { rowIndex, rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = if (rowIndex < MBTI_TYPES.chunked(2).size - 1) 10.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
rowItems.forEachIndexed { colIndex, mbti ->
Box(
modifier = Modifier.weight(1f)
) {
MbtiItem(
mbti = mbti,
isSelected = mbti == currentMbti,
onClick = {
// 保存MBTI类型
model.mbti = mbti
onClose()
}
)
}
} }
) // 如果这一行只有1个item添加一个空的Spacer来保持布局
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
} }
} }
} }

View File

@@ -396,7 +396,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
ProfileInfoCard( ProfileInfoCard(
label = stringResource(R.string.nickname), label = stringResource(R.string.nickname),
value = model.name, value = model.name,
placeholder = "Value", placeholder = stringResource(R.string.nickname_placeholder),
onValueChange = { onNicknameChange(it) }, onValueChange = { onNicknameChange(it) },
isMultiline = false isMultiline = false
) )
@@ -407,7 +407,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
ProfileInfoCard( ProfileInfoCard(
label = stringResource(R.string.personal_intro), label = stringResource(R.string.personal_intro),
value = model.bio, value = model.bio,
placeholder = "Welcome to my fantiac word i will show you something about magic", placeholder = stringResource(R.string.bio_placeholder),
onValueChange = { onBioChange(it) }, onValueChange = { onBioChange(it) },
isMultiline = true isMultiline = true
) )
@@ -502,12 +502,24 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 验证通过,执行保存 // 验证通过,执行保存
model.viewModelScope.launch { model.viewModelScope.launch {
model.isUpdating = true model.isUpdating = true
model.updateUserProfile(context) try {
model.viewModelScope.launch(Dispatchers.Main) { model.updateUserProfile(context)
debouncedNavigation { model.viewModelScope.launch(Dispatchers.Main) {
navController.navigateUp() debouncedNavigation {
navController.navigateUp()
}
model.isUpdating = false
}
} catch (e: Exception) {
// 捕获所有异常,包括网络异常
model.viewModelScope.launch(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.network_error_check_network),
Toast.LENGTH_SHORT
).show()
model.isUpdating = false
} }
model.isUpdating = false
} }
} }
}, },
@@ -568,13 +580,19 @@ fun ProfileInfoCard(
verticalAlignment = if (isMultiline) Alignment.Top else Alignment.CenterVertically verticalAlignment = if (isMultiline) Alignment.Top else Alignment.CenterVertically
) { ) {
// 标签 // 标签
Text( Box(
text = label, modifier = Modifier
fontSize = 17.sp, .width(100.dp)
fontWeight = FontWeight.Normal, .height(if (isMultiline) 44.dp else 56.dp),
color = appColors.text, contentAlignment = Alignment.CenterStart
modifier = Modifier.width(100.dp) ) {
) Text(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.text
)
}
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))

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

@@ -319,6 +319,7 @@ object MyProfileViewModel : ViewModel() {
return return
} }
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
if (roomsLoading && !pullRefresh) return if (roomsLoading && !pullRefresh) return
viewModelScope.launch { viewModelScope.launch {
@@ -340,16 +341,16 @@ object MyProfileViewModel : ViewModel() {
2 -> "private" 2 -> "private"
else -> null else -> null
} }
val effectiveRoomType = if (normalizedOwnerId != null) "public" else roomType
// 构建API调用参数 // 构建API调用参数
if (ownerSessionId != null) { if (normalizedOwnerId != null) {
// 查看其他用户的房间:显示该用户创建和加入的房间 // 查看其他用户的房间:显示该用户创建的公开房间
// 1. 先快速获取该用户创建的房间(不需要 includeUsers减少数据量
val createdResponse = apiClient.getRooms( val createdResponse = apiClient.getRooms(
page = currentPage, page = currentPage,
pageSize = roomsPageSize, pageSize = roomsPageSize,
roomType = roomType, roomType = effectiveRoomType,
ownerSessionId = ownerSessionId, ownerSessionId = normalizedOwnerId,
includeUsers = false includeUsers = false
) )
@@ -359,68 +360,23 @@ object MyProfileViewModel : ViewModel() {
emptyList() emptyList()
} }
// 先快速显示创建的房间,提升用户体验
if (pullRefresh || currentPage == 1) { if (pullRefresh || currentPage == 1) {
rooms = createdRooms rooms = createdRooms
} else { } else {
rooms = rooms + createdRooms rooms = rooms + createdRooms
} }
// 处理分页(基于创建的房间)
val total = createdResponse.body()?.total ?: 0L val total = createdResponse.body()?.total ?: 0L
roomsHasMore = rooms.size < total roomsHasMore = rooms.size < total
if (roomsHasMore && !pullRefresh) { if (roomsHasMore && !pullRefresh) {
roomsCurrentPage++ roomsCurrentPage++
} }
// 2. 后台异步获取该用户加入的房间(仅在非私有房间时获取,且只在第一页时获取以提升性能)
if (filterType != 2 && currentPage == 1) {
launch {
try {
// 获取公开房间,但限制数量以减少数据量
val joinedResponse = apiClient.getRooms(
page = 1,
pageSize = roomsPageSize,
roomType = if (filterType == 1) "public" else null,
includeUsers = true // 需要成员信息来判断
)
if (joinedResponse.isSuccessful) {
val joinedRooms = joinedResponse.body()?.list?.mapNotNull { room ->
try {
val entity = room.toRoomtEntity()
// 检查房间的创建者是否是该用户
val isCreatedByUser = entity.creator.profile.chatAIId == ownerSessionId
// 检查房间的成员是否包含该用户
val isMember = entity.users.any { it.profile.chatAIId == ownerSessionId }
// 如果用户是成员但不是创建者,则认为是加入的房间
if (isMember && !isCreatedByUser) {
entity
} else {
null
}
} catch (e: Exception) {
Log.e("MyProfileViewModel", "处理房间失败: ${e.message}")
null
}
} ?: emptyList()
// 合并并去重基于房间ID然后更新列表
val allRooms = (rooms + joinedRooms).distinctBy { it.id }
rooms = allRooms
}
} catch (e: Exception) {
Log.e("MyProfileViewModel", "获取加入的房间失败: ${e.message}")
}
}
}
} else { } else {
// 查看自己的房间:显示创建或加入的房间 // 查看自己的房间:显示创建或加入的房间
val response = apiClient.getRooms( val response = apiClient.getRooms(
page = currentPage, page = currentPage,
pageSize = roomsPageSize, pageSize = roomsPageSize,
roomType = roomType, roomType = effectiveRoomType,
showCreated = true, showCreated = true,
showJoined = if (filterType == 2) null else true // 私有房间不显示加入的 showJoined = if (filterType == 2) null else true // 私有房间不显示加入的
) )
@@ -458,8 +414,9 @@ object MyProfileViewModel : ViewModel() {
* @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间 * @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间
*/ */
fun loadMoreRooms(filterType: Int = 0, ownerSessionId: String? = null) { fun loadMoreRooms(filterType: Int = 0, ownerSessionId: String? = null) {
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
if (roomsLoading || !roomsHasMore) return if (roomsLoading || !roomsHasMore) return
loadRooms(filterType = filterType, pullRefresh = false, ownerSessionId = ownerSessionId) loadRooms(filterType = filterType, pullRefresh = false, ownerSessionId = normalizedOwnerId)
} }
/** /**
@@ -471,6 +428,12 @@ object MyProfileViewModel : ViewModel() {
rooms = emptyList() rooms = emptyList()
roomsCurrentPage = 1 roomsCurrentPage = 1
roomsHasMore = true roomsHasMore = true
loadRooms(filterType = filterType, pullRefresh = true, ownerSessionId = ownerSessionId) val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
loadRooms(filterType = filterType, pullRefresh = true, ownerSessionId = normalizedOwnerId)
}
private fun normalizeOwnerSessionId(ownerSessionId: String?): String? {
val trimmed = ownerSessionId?.trim()
return if (trimmed.isNullOrEmpty()) null else trimmed
} }
} }

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,41 @@ 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) {
if (isSelf) {
null
} else {
profile?.chatAIId?.takeIf { it.isNotBlank() }
?: profile?.trtcUserId?.takeIf { it.isNotBlank() }
}
}
val nestedScrollConnection = remember(scrollState, pagerState, gridState, listState, groupChatListState, isAiAccount) { val nestedScrollConnection = remember(scrollState, pagerState, gridState, listState, groupChatListState, isAiAccount) {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
@@ -492,10 +532,19 @@ fun ProfileV3(
.background(AppColors.profileBackground) .background(AppColors.profileBackground)
.padding(top = 8.dp) .padding(top = 8.dp)
) { ) {
UserContentPageIndicator( Box(
pagerState = pagerState, modifier = Modifier
showAgentTab = !isAiAccount .onGloballyPositioned { coordinates ->
) tabIndicatorHeightPx = coordinates.size.height
tabIndicatorContentOffset = coordinates.positionInRoot().y + scrollState.value
}
.alpha(if (shouldStickTabBar) 0f else 1f)
) {
UserContentPageIndicator(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
}
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.height(650.dp) // 固定滚动高度 modifier = Modifier.height(650.dp) // 固定滚动高度
@@ -534,28 +583,52 @@ 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 {
// 查看其他用户的主页时,传递该用户的chatAIId以显示其创建的群聊查看自己的主页时传递null // 查看其他用户的主页时,传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
GroupChatPlaceholder( GroupChatPlaceholder(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
listState = groupChatListState, listState = groupChatListState,
nestedScrollConnection = nestedScrollConnection, nestedScrollConnection = nestedScrollConnection,
ownerSessionId = if (!isSelf) profile?.chatAIId else null, ownerSessionId = externalOwnerSessionId,
showSegments = isSelf // 只有查看自己的主页时才显示分段控制器 showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
selectedSegmentIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
onSegmentMeasured = { offset, height ->
groupSegmentOffset = offset
groupSegmentHeightPx = height
},
isSegmentSticky = shouldStickGroupSegments,
parentScrollProvider = { scrollState.value }
) )
} }
} }
2 -> { 2 -> {
if (!isAiAccount) { if (!isAiAccount) {
// 查看其他用户的主页时,传递该用户的chatAIId以显示其创建的群聊查看自己的主页时传递null // 查看其他用户的主页时,传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
GroupChatPlaceholder( GroupChatPlaceholder(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
listState = groupChatListState, listState = groupChatListState,
nestedScrollConnection = nestedScrollConnection, nestedScrollConnection = nestedScrollConnection,
ownerSessionId = if (!isSelf) profile?.chatAIId else null, ownerSessionId = externalOwnerSessionId,
showSegments = isSelf // 只有查看自己的主页时才显示分段控制器 showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
selectedSegmentIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
onSegmentMeasured = { offset, height ->
groupSegmentOffset = offset
groupSegmentHeightPx = height
},
isSegmentSticky = shouldStickGroupSegments,
parentScrollProvider = { scrollState.value }
) )
} }
} }
@@ -567,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,
@@ -665,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,28 +76,38 @@ 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
val viewModel = MyProfileViewModel val viewModel = MyProfileViewModel
val normalizedOwnerSessionId = ownerSessionId?.takeIf { it.isNotBlank() }
val canLoadRooms = showSegments || normalizedOwnerSessionId != null
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 = viewModel.roomsRefreshing, refreshing = if (canLoadRooms) viewModel.roomsRefreshing else false,
onRefresh = { onRefresh = {
viewModel.refreshRooms(filterType = filterType, ownerSessionId = ownerSessionId) if (canLoadRooms) {
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
}
} }
) )
// 当分段或用户ID改变时重新加载数据 // 当分段或用户ID改变时重新加载数据
LaunchedEffect(selectedSegment, ownerSessionId, showSegments) { LaunchedEffect(selectedSegmentIndex, normalizedOwnerSessionId, showSegments) {
viewModel.refreshRooms(filterType = filterType, ownerSessionId = ownerSessionId) if (canLoadRooms) {
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
}
} }
val nestedScrollModifier = if (nestedScrollConnection != null) { val nestedScrollModifier = if (nestedScrollConnection != null) {
@@ -114,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))
@@ -130,7 +147,14 @@ fun GroupChatEmptyContent(
.fillMaxSize() .fillMaxSize()
.pullRefresh(state) .pullRefresh(state)
) { ) {
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) { if (!canLoadRooms) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
// 空状态内容(居中) // 空状态内容(居中)
Column( Column(
modifier = nestedScrollModifier.fillMaxWidth(), modifier = nestedScrollModifier.fillMaxWidth(),
@@ -208,18 +232,23 @@ fun GroupChatEmptyContent(
if (viewModel.roomsHasMore && !viewModel.roomsLoading) { if (viewModel.roomsHasMore && !viewModel.roomsLoading) {
item { item {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.loadMoreRooms(filterType = filterType, ownerSessionId = ownerSessionId) viewModel.loadMoreRooms(
filterType = filterType,
ownerSessionId = normalizedOwnerSessionId
)
} }
} }
} }
} }
} }
PullRefreshIndicator( if (canLoadRooms) {
refreshing = viewModel.roomsRefreshing, PullRefreshIndicator(
state = state, refreshing = viewModel.roomsRefreshing,
modifier = Modifier.align(Alignment.TopCenter) state = state,
) modifier = Modifier.align(Alignment.TopCenter)
)
}
} }
} }
} }
@@ -427,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 {
try { 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 {
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
} catch (e: Exception) { } else {
e.printStackTrace() 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) {
e.printStackTrace()
if (reset) {
hasError = true hasError = true
} finally { commentsList = emptyList()
}
} 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,8 +1964,9 @@ 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 (

View File

@@ -75,10 +75,11 @@
<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="comment_notice">コメント:</string> <string name="comment_notice">コメント:</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>
@@ -333,8 +334,11 @@
<string name="error_nickname_too_long">ニックネームの長さは20文字を超えることはできません</string> <string name="error_nickname_too_long">ニックネームの長さは20文字を超えることはできません</string>
<string name="error_bio_too_long">自己紹介の長さは100文字を超えることはできません</string> <string name="error_bio_too_long">自己紹介の長さは100文字を超えることはできません</string>
<string name="error_load_profile_failed">ユーザープロフィールの読み込みに失敗しました。もう一度お試しください</string> <string name="error_load_profile_failed">ユーザープロフィールの読み込みに失敗しました。もう一度お試しください</string>
<string name="nickname_placeholder">ニックネームを入力</string>
<string name="bio_placeholder">私の世界へようこそ。魔法について何かお見せします</string>
<string name="save">保存</string> <string name="save">保存</string>
<string name="choose_mbti">MBTIを選択</string> <string name="choose_mbti">MBTIを選択</string>
<string name="mbti_description">MBTIは心理学理論に基づく人格評価ツールです。人々がエネルギーを獲得する方法内向-外向)、情報を収集する方法(感覚-直感)、意思決定を行う方法(思考-感情)、生活様式(判断-知覚の好みを理解することで、人格タイプを16種類に分類します。</string>
<string name="choose_zodiac">星座を選択</string> <string name="choose_zodiac">星座を選択</string>
<string name="zodiac_aries">牡羊座</string> <string name="zodiac_aries">牡羊座</string>
<string name="zodiac_taurus">牡牛座</string> <string name="zodiac_taurus">牡牛座</string>

View File

@@ -80,10 +80,11 @@
<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="comment_notice">评论:</string> <string name="comment_notice">评论:</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>
@@ -324,8 +325,12 @@
<string name="error_nickname_too_long">昵称长度不能大于20</string> <string name="error_nickname_too_long">昵称长度不能大于20</string>
<string name="error_bio_too_long">个人简介长度不能大于100</string> <string name="error_bio_too_long">个人简介长度不能大于100</string>
<string name="error_load_profile_failed">加载用户资料失败,请重试</string> <string name="error_load_profile_failed">加载用户资料失败,请重试</string>
<string name="network_error_check_network">网络错误,请检查网络</string>
<string name="nickname_placeholder">请输入昵称</string>
<string name="bio_placeholder">欢迎来到我的世界,我会向你展示一些关于魔法的内容</string>
<string name="save">保存</string> <string name="save">保存</string>
<string name="choose_mbti">选择 MBTI</string> <string name="choose_mbti">选择 MBTI</string>
<string name="mbti_description">MBTI是基于心理学理论的人格测评工具。了解人们获取能量方式(内向-外向)、收集信息方式(感觉-直觉)、做决策方式(思维-情感)、生活方式(判断-知觉)的偏好,将人格类型分为16种。</string>
<string name="choose_zodiac">选择星座</string> <string name="choose_zodiac">选择星座</string>
<string name="zodiac_aries">白羊座</string> <string name="zodiac_aries">白羊座</string>
<string name="zodiac_taurus">金牛座</string> <string name="zodiac_taurus">金牛座</string>

View File

@@ -74,10 +74,11 @@
<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">All</string>
<string name="comment_notice">commented:</string> <string name="comment_notice">commented:</string>
<string name="order_comment_default">Default</string> <string name="order_comment_default">Default</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>
@@ -332,8 +333,12 @@
<string name="error_nickname_too_long">Nickname length cannot be greater than 20</string> <string name="error_nickname_too_long">Nickname length cannot be greater than 20</string>
<string name="error_bio_too_long">Bio length cannot be greater than 100</string> <string name="error_bio_too_long">Bio length cannot be greater than 100</string>
<string name="error_load_profile_failed">Failed to load user profile, please try again</string> <string name="error_load_profile_failed">Failed to load user profile, please try again</string>
<string name="network_error_check_network">Network error, please check your network</string>
<string name="nickname_placeholder">Value</string>
<string name="bio_placeholder">Welcome to my fantiac word i will show you something about magic</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="choose_mbti">Choose MBTI</string> <string name="choose_mbti">Choose MBTI</string>
<string name="mbti_description">MBTI is a personality assessment tool based on psychological theory. By understanding people\'s preferences in how they acquire energy (introversion-extraversion), collect information (sensing-intuition), make decisions (thinking-feeling), and live their lives (judging-perceiving), personality types are divided into 16 kinds.</string>
<string name="choose_zodiac">Choose Zodiac</string> <string name="choose_zodiac">Choose Zodiac</string>
<string name="zodiac_aries">Aries</string> <string name="zodiac_aries">Aries</string>
<string name="zodiac_taurus">Taurus</string> <string name="zodiac_taurus">Taurus</string>