diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fa31758..6c02a56 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,7 +54,7 @@ android:theme="@style/Theme.App.Starting" android:screenOrientation="portrait" android:windowSoftInputMode="adjustResize" - android:configChanges="fontScale|orientation|screenSize|keyboardHidden"> + android:configChanges="fontScale|orientation|screenSize|keyboardHidden|uiMode"> diff --git a/app/src/main/java/com/aiosman/ravenow/MainActivity.kt b/app/src/main/java/com/aiosman/ravenow/MainActivity.kt index c566ccf..1d2652b 100644 --- a/app/src/main/java/com/aiosman/ravenow/MainActivity.kt +++ b/app/src/main/java/com/aiosman/ravenow/MainActivity.kt @@ -73,6 +73,10 @@ class MainActivity : ComponentActivity() { val config = Configuration(newConfig) config.fontScale = 1.0f 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) - if (AppState.darkMode) { - window.decorView.setBackgroundColor(android.graphics.Color.BLACK) - } + updateWindowBackground(AppState.darkMode) enableEdgeToEdge() scope.launch { @@ -269,8 +271,22 @@ class MainActivity : ComponentActivity() { 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 { error("NavController not provided") } diff --git a/app/src/main/java/com/aiosman/ravenow/data/AccountService.kt b/app/src/main/java/com/aiosman/ravenow/data/AccountService.kt index ab245d5..b83f0d5 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/AccountService.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/AccountService.kt @@ -545,7 +545,13 @@ class AccountServiceImpl : AccountService { val bannerField: MultipartBody.Part? = banner?.let { 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) { diff --git a/app/src/main/java/com/aiosman/ravenow/entity/Moment.kt b/app/src/main/java/com/aiosman/ravenow/entity/Moment.kt index 9d301ce..e757ed9 100644 --- a/app/src/main/java/com/aiosman/ravenow/entity/Moment.kt +++ b/app/src/main/java/com/aiosman/ravenow/entity/Moment.kt @@ -410,7 +410,16 @@ class MomentLoader : DataLoader() { fun updateMomentLike(id: Int,isLike:Boolean) { this.list = this.list.map { momentItem -> 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 { momentItem } @@ -421,7 +430,16 @@ class MomentLoader : DataLoader() { fun updateFavoriteCount(id: Int,isFavorite:Boolean) { this.list = this.list.map { momentItem -> 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 { momentItem } diff --git a/app/src/main/java/com/aiosman/ravenow/store.kt b/app/src/main/java/com/aiosman/ravenow/store.kt index 868ce02..ff380b1 100644 --- a/app/src/main/java/com/aiosman/ravenow/store.kt +++ b/app/src/main/java/com/aiosman/ravenow/store.kt @@ -2,6 +2,7 @@ package com.aiosman.ravenow import android.content.Context import android.content.SharedPreferences +import android.content.res.Configuration import com.google.android.gms.auth.api.signin.GoogleSignInOptions /** @@ -24,11 +25,16 @@ object AppStore { .requestEmail() .build() googleSignInOptions = gso - // apply dark mode - if (sharedPreferences.getBoolean("darkMode", false)) { - AppState.darkMode = true - AppState.appTheme = DarkThemeColors() + // apply dark mode - 如果用户未手动设置,优先跟随系统 + val hasUserPreference = sharedPreferences.contains("darkMode") + val resolvedDarkMode = if (hasUserPreference) { + 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 val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null) diff --git a/app/src/main/java/com/aiosman/ravenow/ui/account/MbtiSelectScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/account/MbtiSelectScreen.kt index 4e173e7..c7b324e 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/account/MbtiSelectScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/account/MbtiSelectScreen.kt @@ -20,9 +20,8 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet @@ -219,53 +218,94 @@ fun MbtiSelectBottomSheet( val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - // 不消费任何事件,让 LazyVerticalGrid 先处理 + // 不消费任何事件,让 LazyColumn 先处理 return Offset.Zero } override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { - // 消费 LazyVerticalGrid 处理后的剩余滚动事件,防止传递到 ModalBottomSheet + // 消费 LazyColumn 处理后的剩余滚动事件,防止传递到 ModalBottomSheet return available } override suspend fun onPreFling(available: Velocity): Velocity { - // 不消费惯性滚动,让 LazyVerticalGrid 先处理 + // 不消费惯性滚动,让 LazyColumn 先处理 return Velocity.Zero } override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - // 消费 LazyVerticalGrid 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet + // 消费 LazyColumn 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet return available } } } - // 网格列表 - 2列 - LazyVerticalGrid( - columns = GridCells.Fixed(2), + // MBTI解释文字背景色 + val descriptionBackgroundColor = if (isDarkMode) { + Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色 + } else { + Color(0xFFFAF9FB) + } + + // 使用LazyColumn包裹解释文字和MBTI类型网格,使它们一起滚动 + LazyColumn( modifier = Modifier .fillMaxWidth() .weight(1f) .nestedScroll(nestedScrollConnection), contentPadding = PaddingValues( start = 8.dp, - top = 8.dp, + top = 0.dp, end = 8.dp, bottom = 8.dp ), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalArrangement = Arrangement.spacedBy(10.dp) + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - itemsIndexed(MBTI_TYPES) { index, mbti -> - MbtiItem( - mbti = mbti, - isSelected = mbti == currentMbti, - onClick = { - // 保存MBTI类型 - model.mbti = mbti - onClose() + // MBTI解释文字 - 作为第一个item + item { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(descriptionBackgroundColor) + .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)) + } + } } } } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt b/app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt index 913725d..6d99c70 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt @@ -396,7 +396,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,) ProfileInfoCard( label = stringResource(R.string.nickname), value = model.name, - placeholder = "Value", + placeholder = stringResource(R.string.nickname_placeholder), onValueChange = { onNicknameChange(it) }, isMultiline = false ) @@ -407,7 +407,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,) ProfileInfoCard( label = stringResource(R.string.personal_intro), 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) }, isMultiline = true ) @@ -502,12 +502,24 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,) // 验证通过,执行保存 model.viewModelScope.launch { model.isUpdating = true - model.updateUserProfile(context) - model.viewModelScope.launch(Dispatchers.Main) { - debouncedNavigation { - navController.navigateUp() + try { + model.updateUserProfile(context) + model.viewModelScope.launch(Dispatchers.Main) { + 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 ) { // 标签 - Text( - text = label, - fontSize = 17.sp, - fontWeight = FontWeight.Normal, - color = appColors.text, - modifier = Modifier.width(100.dp) - ) + Box( + modifier = Modifier + .width(100.dp) + .height(if (isMultiline) 44.dp else 56.dp), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = label, + fontSize = 17.sp, + fontWeight = FontWeight.Normal, + color = appColors.text + ) + } Spacer(modifier = Modifier.width(16.dp)) diff --git a/app/src/main/java/com/aiosman/ravenow/ui/comment/CommentModal.kt b/app/src/main/java/com/aiosman/ravenow/ui/comment/CommentModal.kt index 253d650..4f69723 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/comment/CommentModal.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/comment/CommentModal.kt @@ -208,7 +208,9 @@ fun CommentModalContent( fontSize = 14.sp, color = AppColors.secondaryText ) - OrderSelectionComponent { + OrderSelectionComponent( + selectedOrder = commentViewModel.order + ) { commentViewModel.order = it commentViewModel.reloadComment() } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/BaseMomentModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/BaseMomentModel.kt index a8a4d7b..2ec21cb 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/BaseMomentModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/BaseMomentModel.kt @@ -64,12 +64,36 @@ open class BaseMomentModel :ViewModel(){ momentLoader.updateMomentLike(event.postId, event.isLike) } suspend fun likeMoment(id: Int) { + // 获取当前动态信息,用于计算新的点赞数 + val currentMoment = momentLoader.list.find { it.id == id } + val newLikeCount = (currentMoment?.likeCount ?: 0) + 1 + momentService.likeMoment(id) - momentLoader.updateMomentLike(id, true) + + // 只发送事件,让事件订阅者统一处理更新,避免重复更新 + EventBus.getDefault().post( + MomentLikeChangeEvent( + postId = id, + likeCount = newLikeCount, + isLike = true + ) + ) } suspend fun dislikeMoment(id: Int) { + // 获取当前动态信息,用于计算新的点赞数 + val currentMoment = momentLoader.list.find { it.id == id } + val newLikeCount = ((currentMoment?.likeCount ?: 0) - 1).coerceAtLeast(0) + 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) { momentService.favoriteMoment(id) - momentLoader.updateFavoriteCount(id, true) + + // 只发送事件,让事件订阅者统一处理更新,避免重复更新 + EventBus.getDefault().post( + MomentFavouriteChangeEvent( + postId = id, + isFavourite = true + ) + ) } suspend fun unfavoriteMoment(id: Int) { momentService.unfavoriteMoment(id) - momentLoader.updateFavoriteCount(id, false) - + + // 只发送事件,让事件订阅者统一处理更新,避免重复更新 + EventBus.getDefault().post( + MomentFavouriteChangeEvent( + postId = id, + isFavourite = false + ) + ) } @Subscribe diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsCommentModal.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsCommentModal.kt index caa0c46..b6b79e2 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsCommentModal.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/news/NewsCommentModal.kt @@ -226,7 +226,9 @@ fun NewsCommentModal( .fillMaxWidth(), horizontalArrangement = Arrangement.End ) { - OrderSelectionComponent { + OrderSelectionComponent( + selectedOrder = commentViewModel.order + ) { commentViewModel.order = it commentViewModel.reloadComment() } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/MyProfileViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/MyProfileViewModel.kt index 1466d60..3f125c4 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/MyProfileViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/MyProfileViewModel.kt @@ -319,6 +319,7 @@ object MyProfileViewModel : ViewModel() { return } + val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId) if (roomsLoading && !pullRefresh) return viewModelScope.launch { @@ -340,16 +341,16 @@ object MyProfileViewModel : ViewModel() { 2 -> "private" else -> null } + val effectiveRoomType = if (normalizedOwnerId != null) "public" else roomType // 构建API调用参数 - if (ownerSessionId != null) { - // 查看其他用户的房间:显示该用户创建和加入的房间 - // 1. 先快速获取该用户创建的房间(不需要 includeUsers,减少数据量) + if (normalizedOwnerId != null) { + // 查看其他用户的房间:仅显示该用户创建的公开房间 val createdResponse = apiClient.getRooms( page = currentPage, pageSize = roomsPageSize, - roomType = roomType, - ownerSessionId = ownerSessionId, + roomType = effectiveRoomType, + ownerSessionId = normalizedOwnerId, includeUsers = false ) @@ -359,68 +360,23 @@ object MyProfileViewModel : ViewModel() { emptyList() } - // 先快速显示创建的房间,提升用户体验 if (pullRefresh || currentPage == 1) { rooms = createdRooms } else { rooms = rooms + createdRooms } - // 处理分页(基于创建的房间) val total = createdResponse.body()?.total ?: 0L roomsHasMore = rooms.size < total if (roomsHasMore && !pullRefresh) { 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 { // 查看自己的房间:显示创建或加入的房间 val response = apiClient.getRooms( page = currentPage, pageSize = roomsPageSize, - roomType = roomType, + roomType = effectiveRoomType, showCreated = true, showJoined = if (filterType == 2) null else true // 私有房间不显示加入的 ) @@ -458,8 +414,9 @@ object MyProfileViewModel : ViewModel() { * @param ownerSessionId 创建者用户ID(ChatAIID),用于过滤特定创建者的房间 */ fun loadMoreRooms(filterType: Int = 0, ownerSessionId: String? = null) { + val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId) 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() roomsCurrentPage = 1 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 } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt index 77623ba..f6449b2 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt @@ -52,6 +52,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush 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.NestedScrollSource 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.LocalDensity 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.GroupChatEmptyContent 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.UserAgentsRow import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator @@ -168,6 +173,41 @@ fun ProfileV3( initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset ) val scrollState = rememberScrollState(model.profileScrollOffset) + var tabIndicatorContentOffset by remember { mutableStateOf(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(null) } + var agentSegmentHeightPx by remember { mutableStateOf(0) } + var groupSegmentOffset by remember { mutableStateOf(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) { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { @@ -492,10 +532,19 @@ fun ProfileV3( .background(AppColors.profileBackground) .padding(top = 8.dp) ) { - UserContentPageIndicator( - pagerState = pagerState, - showAgentTab = !isAiAccount - ) + Box( + modifier = Modifier + .onGloballyPositioned { coordinates -> + tabIndicatorHeightPx = coordinates.size.height + tabIndicatorContentOffset = coordinates.positionInRoot().y + scrollState.value + } + .alpha(if (shouldStickTabBar) 0f else 1f) + ) { + UserContentPageIndicator( + pagerState = pagerState, + showAgentTab = !isAiAccount + ) + } HorizontalPager( state = pagerState, modifier = Modifier.height(650.dp) // 固定滚动高度 @@ -534,28 +583,52 @@ fun ProfileV3( modifier = Modifier.fillMaxSize(), state = listState, nestedScrollConnection = nestedScrollConnection, - showSegments = isSelf // 只有查看自己的主页时才显示分段控制器 + showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器 + segmentSelectedIndex = agentSegmentSelected, + onSegmentSelected = { agentSegmentSelected = it }, + onSegmentMeasured = { offset, height -> + agentSegmentOffset = offset + agentSegmentHeightPx = height + }, + isSegmentSticky = shouldStickAgentSegments, + parentScrollProvider = { scrollState.value } ) } else { - // 查看其他用户的主页时,传递该用户的chatAIId以显示其创建的群聊;查看自己的主页时传递null + // 查看其他用户的主页时,传递该用户的会话ID以显示其创建的群聊;查看自己的主页时传递null GroupChatPlaceholder( modifier = Modifier.fillMaxSize(), listState = groupChatListState, nestedScrollConnection = nestedScrollConnection, - ownerSessionId = if (!isSelf) profile?.chatAIId else null, - showSegments = isSelf // 只有查看自己的主页时才显示分段控制器 + ownerSessionId = externalOwnerSessionId, + showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器 + selectedSegmentIndex = groupSegmentSelected, + onSegmentSelected = { groupSegmentSelected = it }, + onSegmentMeasured = { offset, height -> + groupSegmentOffset = offset + groupSegmentHeightPx = height + }, + isSegmentSticky = shouldStickGroupSegments, + parentScrollProvider = { scrollState.value } ) } } 2 -> { if (!isAiAccount) { - // 查看其他用户的主页时,传递该用户的chatAIId以显示其创建的群聊;查看自己的主页时传递null + // 查看其他用户的主页时,传递该用户的会话ID以显示其创建的群聊;查看自己的主页时传递null GroupChatPlaceholder( modifier = Modifier.fillMaxSize(), listState = groupChatListState, nestedScrollConnection = nestedScrollConnection, - ownerSessionId = if (!isSelf) profile?.chatAIId else null, - showSegments = isSelf // 只有查看自己的主页时才显示分段控制器 + ownerSessionId = externalOwnerSessionId, + 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)) } + 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( isMain = isMain, @@ -665,14 +787,24 @@ private fun GroupChatPlaceholder( listState: androidx.compose.foundation.lazy.LazyListState, nestedScrollConnection: NestedScrollConnection? = null, ownerSessionId: String? = null, // 创建者用户ID(ChatAIID),用于过滤特定创建者的房间。如果为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( modifier = modifier, listState = listState, nestedScrollConnection = nestedScrollConnection, ownerSessionId = ownerSessionId, - showSegments = showSegments + showSegments = showSegments, + selectedSegmentIndex = selectedSegmentIndex, + onSegmentSelected = onSegmentSelected, + onSegmentMeasured = onSegmentMeasured, + isSegmentSticky = isSegmentSticky, + parentScrollProvider = parentScrollProvider ) } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/GroupChatEmptyContent.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/GroupChatEmptyContent.kt index 1124eb2..ee4d190 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/GroupChatEmptyContent.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/GroupChatEmptyContent.kt @@ -41,6 +41,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color 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.res.painterResource import androidx.compose.ui.res.stringResource @@ -74,28 +76,38 @@ fun GroupChatEmptyContent( listState: LazyListState, nestedScrollConnection: NestedScrollConnection? = null, ownerSessionId: String? = null, // 创建者用户ID(ChatAIID),用于过滤特定创建者的房间。如果为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 context = LocalContext.current val navController = LocalNavController.current val viewModel = MyProfileViewModel + val normalizedOwnerSessionId = ownerSessionId?.takeIf { it.isNotBlank() } + val canLoadRooms = showSegments || normalizedOwnerSessionId != null val networkAvailable = isNetworkAvailable(context) // 如果查看其他用户的房间,固定使用全部类型(filterType = 0) - val filterType = if (showSegments) selectedSegment else 0 + val filterType = if (showSegments) selectedSegmentIndex else 0 val state = rememberPullRefreshState( - refreshing = viewModel.roomsRefreshing, + refreshing = if (canLoadRooms) viewModel.roomsRefreshing else false, onRefresh = { - viewModel.refreshRooms(filterType = filterType, ownerSessionId = ownerSessionId) + if (canLoadRooms) { + viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId) + } } ) // 当分段或用户ID改变时,重新加载数据 - LaunchedEffect(selectedSegment, ownerSessionId, showSegments) { - viewModel.refreshRooms(filterType = filterType, ownerSessionId = ownerSessionId) + LaunchedEffect(selectedSegmentIndex, normalizedOwnerSessionId, showSegments) { + if (canLoadRooms) { + viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId) + } } val nestedScrollModifier = if (nestedScrollConnection != null) { @@ -114,12 +126,17 @@ fun GroupChatEmptyContent( // 只在查看自己的房间时显示分段控制器 if (showSegments) { SegmentedControl( - selectedIndex = selectedSegment, - onSegmentSelected = { - selectedSegment = it - // LaunchedEffect 会监听 selectedSegment 的变化并自动刷新 - }, - modifier = Modifier.fillMaxWidth() + selectedIndex = selectedSegmentIndex, + onSegmentSelected = onSegmentSelected, + 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)) @@ -130,7 +147,14 @@ fun GroupChatEmptyContent( .fillMaxSize() .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( modifier = nestedScrollModifier.fillMaxWidth(), @@ -208,18 +232,23 @@ fun GroupChatEmptyContent( if (viewModel.roomsHasMore && !viewModel.roomsLoading) { item { LaunchedEffect(Unit) { - viewModel.loadMoreRooms(filterType = filterType, ownerSessionId = ownerSessionId) + viewModel.loadMoreRooms( + filterType = filterType, + ownerSessionId = normalizedOwnerSessionId + ) } } } } } - PullRefreshIndicator( - refreshing = viewModel.roomsRefreshing, - state = state, - modifier = Modifier.align(Alignment.TopCenter) - ) + if (canLoadRooms) { + PullRefreshIndicator( + refreshing = viewModel.roomsRefreshing, + state = state, + modifier = Modifier.align(Alignment.TopCenter) + ) + } } } } @@ -427,7 +456,7 @@ fun RoomItem( } @Composable -private fun SegmentedControl( +fun SegmentedControl( selectedIndex: Int, onSegmentSelected: (Int) -> Unit, modifier: Modifier = Modifier diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/UserAgentsList.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/UserAgentsList.kt index 457e1df..841778c 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/UserAgentsList.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/UserAgentsList.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import com.aiosman.ravenow.ui.modifiers.noRippleClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -30,9 +29,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter 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.res.painterResource 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.ui.composables.CustomAsyncImage 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.utils.DebounceUtils import com.aiosman.ravenow.utils.NetworkUtils @@ -68,7 +71,12 @@ fun UserAgentsList( modifier: Modifier = Modifier, state: LazyListState, 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 listModifier = if (nestedScrollConnection != null) { @@ -82,7 +90,14 @@ fun UserAgentsList( Box( modifier = listModifier.fillMaxSize() ) { - AgentEmptyContentWithSegments(showSegments = showSegments) + AgentEmptyContentWithSegments( + showSegments = showSegments, + segmentSelectedIndex = segmentSelectedIndex, + onSegmentSelected = onSegmentSelected, + onSegmentMeasured = onSegmentMeasured, + isSegmentSticky = isSegmentSticky, + parentScrollProvider = parentScrollProvider + ) } } else { LazyColumn( @@ -251,9 +266,13 @@ fun UserAgentCard( @Composable 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 isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) @@ -267,9 +286,17 @@ fun AgentEmptyContentWithSegments( // 只在查看自己的智能体时显示分段控制器 if (showSegments) { AgentSegmentedControl( - selectedIndex = selectedSegment, - onSegmentSelected = { selectedSegment = it }, - modifier = Modifier.fillMaxWidth() + selectedIndex = segmentSelectedIndex, + onSegmentSelected = onSegmentSelected, + 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)) @@ -321,7 +348,7 @@ fun AgentEmptyContentWithSegments( } @Composable -private fun AgentSegmentedControl( +fun AgentSegmentedControl( selectedIndex: Int, onSegmentSelected: (Int) -> Unit, modifier: Modifier = Modifier diff --git a/app/src/main/java/com/aiosman/ravenow/ui/post/CommentsViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/post/CommentsViewModel.kt index bc7037a..2939221 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/post/CommentsViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/post/CommentsViewModel.kt @@ -13,37 +13,30 @@ import kotlinx.coroutines.launch class CommentsViewModel( var postId: String = 0.toString(), ) : ViewModel() { + companion object { + private const val ORDER_ALL = "all" + private const val COMMENTS_PAGE_SIZE = 50 + } + var commentService: CommentService = CommentServiceImpl() var commentsList by mutableStateOf>(emptyList()) - var order: String by mutableStateOf("like") + var order: String by mutableStateOf(ORDER_ALL) var addedCommentList by mutableStateOf>(emptyList()) var subCommentLoadingMap by mutableStateOf(mutableMapOf()) var highlightCommentId by mutableStateOf(null) var highlightComment by mutableStateOf(null) var isLoading by mutableStateOf(false) var hasError by mutableStateOf(false) + var isLoadingMore by mutableStateOf(false) + var hasMore by mutableStateOf(false) + private var currentPage by mutableStateOf(0) + private var totalComments by mutableStateOf(0) /** * 预加载,在跳转到 PostScreen 之前设置好内容 */ fun preTransit() { - viewModelScope.launch { - 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 - } - } + reloadComment() } /** @@ -51,25 +44,61 @@ class CommentsViewModel( */ fun reloadComment() { 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 - val response = commentService.getComments( - pageNumber = 1, - postId = postId.toInt(), - order = order, - pageSize = 50 - ) - commentsList = response.list hasError = false - } catch (e: Exception) { - e.printStackTrace() + } 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) { + e.printStackTrace() + if (reset) { hasError = true - } finally { + commentsList = emptyList() + } + } finally { + if (reset) { 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) { highlightCommentId = commentId diff --git a/app/src/main/java/com/aiosman/ravenow/ui/post/Post.kt b/app/src/main/java/com/aiosman/ravenow/ui/post/Post.kt index 2ee9145..d8951f0 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/post/Post.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/post/Post.kt @@ -495,7 +495,9 @@ fun PostScreen( color = AppColors.nonActiveText ) Spacer(modifier = Modifier.weight(1f)) - OrderSelectionComponent() { + OrderSelectionComponent( + selectedOrder = commentsViewModel.order + ) { commentsViewModel.order = it 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) { Box( @@ -1909,15 +1938,15 @@ fun CommentMenuModal( @Composable fun OrderSelectionComponent( + selectedOrder: String, onSelected: (String) -> Unit = {} ) { val AppColors = LocalAppTheme.current - var selectedOrder by remember { mutableStateOf("like") } val orders = listOf( - "like" to stringResource(R.string.order_comment_default), - "earliest" to stringResource(R.string.order_comment_earliest), - "latest" to stringResource(R.string.order_comment_latest) + "all" to stringResource(R.string.order_comment_default), + "latest" to stringResource(R.string.order_comment_latest), + "like" to stringResource(R.string.order_comment_hot) ) Box( modifier = Modifier @@ -1935,8 +1964,9 @@ fun OrderSelectionComponent( Box( modifier = Modifier .noRippleClickable { - selectedOrder = order.first - onSelected(order.first) + if (selectedOrder != order.first) { + onSelected(order.first) + } } .background( if ( diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5287f98..abeb4b8 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -75,10 +75,11 @@ 署名 名前 コメント + すべて コメント: デフォルト 最新 - 最も古い + 人気 ダウンロード オリジナル お気に入り @@ -333,8 +334,11 @@ ニックネームの長さは20文字を超えることはできません 自己紹介の長さは100文字を超えることはできません ユーザープロフィールの読み込みに失敗しました。もう一度お試しください + ニックネームを入力 + 私の世界へようこそ。魔法について何かお見せします 保存 MBTIを選択 + MBTIは心理学理論に基づく人格評価ツールです。人々がエネルギーを獲得する方法(内向-外向)、情報を収集する方法(感覚-直感)、意思決定を行う方法(思考-感情)、生活様式(判断-知覚)の好みを理解することで、人格タイプを16種類に分類します。 星座を選択 牡羊座 牡牛座 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 1e0f616..3aacf7e 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -80,10 +80,11 @@ 个性签名 昵称 评论 + 全部 评论: 默认 最新 - 最早 + 热门 下载 原始图片 收藏 @@ -324,8 +325,12 @@ 昵称长度不能大于20 个人简介长度不能大于100 加载用户资料失败,请重试 + 网络错误,请检查网络 + 请输入昵称 + 欢迎来到我的世界,我会向你展示一些关于魔法的内容 保存 选择 MBTI + MBTI是基于心理学理论的人格测评工具。了解人们获取能量方式(内向-外向)、收集信息方式(感觉-直觉)、做决策方式(思维-情感)、生活方式(判断-知觉)的偏好,将人格类型分为16种。 选择星座 白羊座 金牛座 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80bb713..761e91a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,10 +74,11 @@ Signature Name COMMENTS + All commented: Default Latest - Earliest + Hot Download Original Favourite @@ -332,8 +333,12 @@ Nickname length cannot be greater than 20 Bio length cannot be greater than 100 Failed to load user profile, please try again + Network error, please check your network + Value + Welcome to my fantiac word i will show you something about magic Save Choose MBTI + 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. Choose Zodiac Aries Taurus