@@ -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" />
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 创建者用户ID(ChatAIID),用于过滤特定创建者的房间
|
* @param ownerSessionId 创建者用户ID(ChatAIID),用于过滤特定创建者的房间
|
||||||
*/
|
*/
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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, // 创建者用户ID(ChatAIID),用于过滤特定创建者的房间。如果为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(
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, // 创建者用户ID(ChatAIID),用于过滤特定创建者的房间。如果为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 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user