Compare commits
3 Commits
add_news
...
update_tik
| Author | SHA1 | Date | |
|---|---|---|---|
| b06f2d6c4a | |||
| a215d79ce8 | |||
| 0e0d622864 |
@@ -4,11 +4,12 @@ object ConstVars {
|
|||||||
// api 地址 - 根据构建类型自动选择
|
// api 地址 - 根据构建类型自动选择
|
||||||
// Debug: http://192.168.0.201:8088
|
// Debug: http://192.168.0.201:8088
|
||||||
// Release: https://rider-pro.aiosman.com/beta_api
|
// Release: https://rider-pro.aiosman.com/beta_api
|
||||||
val BASE_SERVER = if (BuildConfig.DEBUG) {
|
// val BASE_SERVER = if (!BuildConfig.DEBUG) {
|
||||||
"http://47.109.137.67:6363" // Debug环境
|
// "http://47.109.137.67:6363" // Debug环境
|
||||||
} else {
|
// } else {
|
||||||
"https://rider-pro.aiosman.com/beta_api" // Release环境
|
// "https://rider-pro.aiosman.com/beta_api" // Release环境
|
||||||
}
|
// }
|
||||||
|
val BASE_SERVER = "https://rider-pro.aiosman.com/beta_api"
|
||||||
|
|
||||||
const val MOMENT_LIKE_CHANNEL_ID = "moment_like"
|
const val MOMENT_LIKE_CHANNEL_ID = "moment_like"
|
||||||
const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like"
|
const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like"
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.aiosman.ravenow.data
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类翻译数据模型
|
||||||
|
*/
|
||||||
|
data class CategoryTranslation(
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类翻译集合
|
||||||
|
*/
|
||||||
|
data class CategoryTranslations(
|
||||||
|
@SerializedName("ja")
|
||||||
|
val ja: CategoryTranslation? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据模型
|
||||||
|
*/
|
||||||
|
data class Category(
|
||||||
|
@SerializedName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("description")
|
||||||
|
val description: String,
|
||||||
|
@SerializedName("avatar")
|
||||||
|
val avatar: String,
|
||||||
|
@SerializedName("parentId")
|
||||||
|
val parentId: Int? = null,
|
||||||
|
@SerializedName("sort")
|
||||||
|
val sort: Int,
|
||||||
|
@SerializedName("isActive")
|
||||||
|
val isActive: Boolean,
|
||||||
|
@SerializedName("promptCount")
|
||||||
|
val promptCount: Int,
|
||||||
|
@SerializedName("createdAt")
|
||||||
|
val createdAt: String,
|
||||||
|
@SerializedName("updatedAt")
|
||||||
|
val updatedAt: String,
|
||||||
|
@SerializedName("translations")
|
||||||
|
val translations: CategoryTranslations? = null
|
||||||
|
)
|
||||||
@@ -6,6 +6,7 @@ import com.aiosman.ravenow.data.AccountLike
|
|||||||
import com.aiosman.ravenow.data.AccountNotice
|
import com.aiosman.ravenow.data.AccountNotice
|
||||||
import com.aiosman.ravenow.data.AccountProfile
|
import com.aiosman.ravenow.data.AccountProfile
|
||||||
import com.aiosman.ravenow.data.Agent
|
import com.aiosman.ravenow.data.Agent
|
||||||
|
import com.aiosman.ravenow.data.Category
|
||||||
import com.aiosman.ravenow.data.Comment
|
import com.aiosman.ravenow.data.Comment
|
||||||
import com.aiosman.ravenow.data.DataContainer
|
import com.aiosman.ravenow.data.DataContainer
|
||||||
import com.aiosman.ravenow.data.ListContainer
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
@@ -570,19 +571,39 @@ interface RaveNowAPI {
|
|||||||
@Body body: RemoveAccountRequestBody
|
@Body body: RemoveAccountRequestBody
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GET("outside/prompts")
|
@GET("outside/prompts")
|
||||||
suspend fun getAgent(
|
suspend fun getAgent(
|
||||||
@Query("page") page: Int = 1,
|
@Query("page") page: Int = 1,
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
@Query("withWorkflow") withWorkflow: Int = 1,
|
@Query("order") order: String? = null,
|
||||||
|
@Query("orderKey") orderKey: String? = null,
|
||||||
|
@Query("createdAt") createdAt: String? = null,
|
||||||
|
@Query("updatedAt") updatedAt: String? = null,
|
||||||
|
@Query("createdStart") createdStart: String? = null,
|
||||||
|
@Query("createdEnd") createdEnd: String? = null,
|
||||||
|
@Query("updatedStart") updatedStart: String? = null,
|
||||||
|
@Query("updatedEnd") updatedEnd: String? = null,
|
||||||
|
@Query("title") title: String? = null,
|
||||||
@Query("authorId") authorId: Int? = null,
|
@Query("authorId") authorId: Int? = null,
|
||||||
|
@Query("authorOpenId") authorOpenId: String? = null,
|
||||||
|
@Query("showPrivate") showPrivate: String? = null,
|
||||||
|
@Query("explore") explore: String? = null,
|
||||||
|
@Query("desc") desc: String? = null,
|
||||||
|
@Query("withWorkflow") withWorkflow: String? = null,
|
||||||
|
@Query("hasAvatar") hasAvatar: String? = null,
|
||||||
|
@Query("random") random: String? = null,
|
||||||
|
@Query("categoryName") categoryName: String? = null,
|
||||||
|
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||||
|
@Query("uncategorized") uncategorized: String? = null,
|
||||||
): Response<DataContainer<ListContainer<Agent>>>
|
): Response<DataContainer<ListContainer<Agent>>>
|
||||||
|
|
||||||
@GET("outside/my/prompts")
|
@GET("outside/my/prompts")
|
||||||
suspend fun getMyAgent(
|
suspend fun getMyAgent(
|
||||||
@Query("page") page: Int = 1,
|
@Query("page") page: Int = 1,
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
@Query("withWorkflow") withWorkflow: Int = 1,
|
@Query("withWorkflow") withWorkflow: String = "1",
|
||||||
): Response<ListContainer<Agent>>
|
): Response<ListContainer<Agent>>
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@@ -619,6 +640,7 @@ interface RaveNowAPI {
|
|||||||
suspend fun getRooms(@Query("page") page: Int = 1,
|
suspend fun getRooms(@Query("page") page: Int = 1,
|
||||||
@Query("pageSize") pageSize: Int = 20,
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
@Query("isRecommended") isRecommended: Int = 1,
|
@Query("isRecommended") isRecommended: Int = 1,
|
||||||
|
@Query("random") random: Int? = null
|
||||||
): Response<ListContainer<Room>>
|
): Response<ListContainer<Room>>
|
||||||
|
|
||||||
@GET("outside/rooms/detail")
|
@GET("outside/rooms/detail")
|
||||||
@@ -632,5 +654,12 @@ interface RaveNowAPI {
|
|||||||
@POST("outside/generate/agent-info")
|
@POST("outside/generate/agent-info")
|
||||||
suspend fun generateAgentInfo(@Body body: GenerateAgentInfoRequestBody): Response<DataContainer<GenerateAgentInfoResponseBody>>
|
suspend fun generateAgentInfo(@Body body: GenerateAgentInfoRequestBody): Response<DataContainer<GenerateAgentInfoResponseBody>>
|
||||||
|
|
||||||
|
@GET("outside/categories")
|
||||||
|
suspend fun getCategories(
|
||||||
|
@Query("page") page: Int = 1,
|
||||||
|
@Query("pageSize") pageSize: Int = 20,
|
||||||
|
@Query("lang") lang: String? = null
|
||||||
|
): Response<ListContainer<Category>>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
160
app/src/main/java/com/aiosman/ravenow/entity/Category.kt
Normal file
160
app/src/main/java/com/aiosman/ravenow/entity/Category.kt
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package com.aiosman.ravenow.entity
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.data.Category
|
||||||
|
import com.aiosman.ravenow.data.CategoryTranslation
|
||||||
|
import com.aiosman.ravenow.data.CategoryTranslations
|
||||||
|
import com.aiosman.ravenow.data.ListContainer
|
||||||
|
import com.aiosman.ravenow.data.ServiceException
|
||||||
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
|
import com.aiosman.ravenow.utils.Utils
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类实体类
|
||||||
|
*/
|
||||||
|
data class CategoryEntity(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val avatar: String,
|
||||||
|
val parentId: Int? = null,
|
||||||
|
val sort: Int,
|
||||||
|
val isActive: Boolean,
|
||||||
|
val promptCount: Int,
|
||||||
|
val createdAt: String,
|
||||||
|
val updatedAt: String,
|
||||||
|
val translations: CategoryTranslations? = null
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* 获取本地化名称
|
||||||
|
*/
|
||||||
|
fun getLocalizedName(): String {
|
||||||
|
return when (Utils.getCurrentLanguage()) {
|
||||||
|
"ja" -> translations?.ja?.name ?: name
|
||||||
|
else -> name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本地化描述
|
||||||
|
*/
|
||||||
|
fun getLocalizedDescription(): String {
|
||||||
|
return when (Utils.getCurrentLanguage()) {
|
||||||
|
"ja" -> translations?.ja?.description ?: description
|
||||||
|
else -> description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category 数据模型扩展函数
|
||||||
|
*/
|
||||||
|
fun Category.toCategoryEntity(): CategoryEntity {
|
||||||
|
return CategoryEntity(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
description = description,
|
||||||
|
avatar = if (avatar.isNotEmpty()) "${ApiClient.BASE_API_URL}/outside$avatar?token=${AppStore.token}" else "",
|
||||||
|
parentId = parentId,
|
||||||
|
sort = sort,
|
||||||
|
isActive = isActive,
|
||||||
|
promptCount = promptCount,
|
||||||
|
createdAt = createdAt,
|
||||||
|
updatedAt = updatedAt,
|
||||||
|
translations = translations
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据后端服务
|
||||||
|
*/
|
||||||
|
class CategoryBackend {
|
||||||
|
private val DataBatchSize = 20
|
||||||
|
|
||||||
|
suspend fun getCategories(pageNumber: Int): ListContainer<CategoryEntity>? {
|
||||||
|
try {
|
||||||
|
val resp = ApiClient.api.getCategories(
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
lang = Utils.getCurrentLanguageTag()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!resp.isSuccessful) {
|
||||||
|
throw ServiceException("获取分类失败: ${resp.code()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = resp.body() ?: return null
|
||||||
|
|
||||||
|
return ListContainer(
|
||||||
|
total = body.total,
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
list = body.list.map { it.toCategoryEntity() }
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ServiceException("网络请求失败: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据加载器参数
|
||||||
|
*/
|
||||||
|
class CategoryLoaderExtraArgs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类数据加载器
|
||||||
|
*/
|
||||||
|
class CategoryLoader : DataLoader<CategoryEntity, CategoryLoaderExtraArgs>() {
|
||||||
|
override suspend fun fetchData(
|
||||||
|
page: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
extra: CategoryLoaderExtraArgs
|
||||||
|
): ListContainer<CategoryEntity> {
|
||||||
|
val backend = CategoryBackend()
|
||||||
|
return backend.getCategories(page) ?: ListContainer(
|
||||||
|
total = 0,
|
||||||
|
page = page,
|
||||||
|
pageSize = pageSize,
|
||||||
|
list = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类分页数据源
|
||||||
|
*/
|
||||||
|
class CategoryPagingSource(
|
||||||
|
private val backend: CategoryBackend
|
||||||
|
) : PagingSource<Int, CategoryEntity>() {
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, CategoryEntity>): Int? {
|
||||||
|
return state.anchorPosition?.let { anchorPosition ->
|
||||||
|
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
|
||||||
|
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CategoryEntity> {
|
||||||
|
return try {
|
||||||
|
val page = params.key ?: 1
|
||||||
|
val response = backend.getCategories(page)
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
LoadResult.Error(IOException("获取分类数据失败"))
|
||||||
|
} else {
|
||||||
|
val hasMore = response.list.size == response.pageSize
|
||||||
|
LoadResult.Page(
|
||||||
|
data = response.list,
|
||||||
|
prevKey = if (page == 1) null else page - 1,
|
||||||
|
nextKey = if (hasMore) page + 1 else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -327,7 +327,8 @@ class MomentLoaderExtraArgs(
|
|||||||
val explore: Boolean? = false,
|
val explore: Boolean? = false,
|
||||||
val timelineId: Int? = null,
|
val timelineId: Int? = null,
|
||||||
val authorId : Int? = null,
|
val authorId : Int? = null,
|
||||||
val newsOnly: Boolean? = false
|
val newsOnly: Boolean? = false,
|
||||||
|
val trend: Boolean? = false
|
||||||
)
|
)
|
||||||
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
||||||
override suspend fun fetchData(
|
override suspend fun fetchData(
|
||||||
@@ -341,7 +342,9 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
|
|||||||
explore = if (extra.explore == true) "true" else "",
|
explore = if (extra.explore == true) "true" else "",
|
||||||
timelineId = extra.timelineId,
|
timelineId = extra.timelineId,
|
||||||
authorId = extra.authorId,
|
authorId = extra.authorId,
|
||||||
newsFilter = if (extra.newsOnly == true) "news_only" else ""
|
newsFilter = if (extra.newsOnly == true) "news_only" else "",
|
||||||
|
trend = if (extra.trend == true) "1" else ""
|
||||||
|
|
||||||
)
|
)
|
||||||
val data = result.body()?.let {
|
val data = result.body()?.let {
|
||||||
ListContainer(
|
ListContainer(
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
import com.aiosman.ravenow.R
|
import com.aiosman.ravenow.R
|
||||||
import com.aiosman.ravenow.entity.CommentEntity
|
import com.aiosman.ravenow.entity.CommentEntity
|
||||||
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
||||||
@@ -79,6 +80,7 @@ fun CommentModalContent(
|
|||||||
onCommentAdded: () -> Unit = {},
|
onCommentAdded: () -> Unit = {},
|
||||||
onDismiss: () -> Unit = {}
|
onDismiss: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
val model = viewModel<CommentModalViewModel>(
|
val model = viewModel<CommentModalViewModel>(
|
||||||
key = "CommentModalViewModel_$postId",
|
key = "CommentModalViewModel_$postId",
|
||||||
factory = object : ViewModelProvider.Factory {
|
factory = object : ViewModelProvider.Factory {
|
||||||
@@ -113,7 +115,7 @@ fun CommentModalContent(
|
|||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
showCommentMenu = false
|
showCommentMenu = false
|
||||||
},
|
},
|
||||||
containerColor = Color.White,
|
containerColor = AppColors.background,
|
||||||
sheetState = rememberModalBottomSheetState(
|
sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = true
|
skipPartiallyExpanded = true
|
||||||
),
|
),
|
||||||
@@ -142,24 +144,8 @@ fun CommentModalContent(
|
|||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
|
|
||||||
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.comment),
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
color = Color(0xFFF7F7F7)
|
|
||||||
)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -170,7 +156,8 @@ fun CommentModalContent(
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.comment_count, commentCount),
|
text = stringResource(id = R.string.comment_count, commentCount),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = Color(0xff666666)
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = AppColors.nonActiveText
|
||||||
)
|
)
|
||||||
OrderSelectionComponent {
|
OrderSelectionComponent {
|
||||||
commentViewModel.order = it
|
commentViewModel.order = it
|
||||||
@@ -205,7 +192,7 @@ fun CommentModalContent(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color(0xfff7f7f7))
|
.background(AppColors.background)
|
||||||
) {
|
) {
|
||||||
EditCommentBottomModal(replyComment) {
|
EditCommentBottomModal(replyComment) {
|
||||||
commentViewModel.viewModelScope.launch {
|
commentViewModel.viewModelScope.launch {
|
||||||
|
|||||||
@@ -59,9 +59,10 @@ fun EditCommentBottomModal(
|
|||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
// 移除自动聚焦,避免自动弹出键盘
|
||||||
focusRequester.requestFocus()
|
// LaunchedEffect(Unit) {
|
||||||
}
|
// focusRequester.requestFocus()
|
||||||
|
// }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -69,20 +70,6 @@ fun EditCommentBottomModal(
|
|||||||
.background(AppColors.background)
|
.background(AppColors.background)
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
if (replyComment == null) "Comment" else "Reply",
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontStyle = FontStyle.Italic,
|
|
||||||
color = AppColors.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
if (replyComment != null) {
|
if (replyComment != null) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -129,9 +116,9 @@ fun EditCommentBottomModal(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(Color.White)
|
.background(AppColors.inputBackground)
|
||||||
.border(1.dp, Color.Black, RoundedCornerShape(20.dp))
|
.border(1.dp, AppColors.text.copy(alpha = 0.2f), RoundedCornerShape(20.dp))
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.Top
|
verticalAlignment = Alignment.Top
|
||||||
@@ -146,7 +133,7 @@ fun EditCommentBottomModal(
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
color = Color.Black,
|
color = AppColors.text,
|
||||||
fontWeight = FontWeight.Normal
|
fontWeight = FontWeight.Normal
|
||||||
),
|
),
|
||||||
minLines = 1
|
minLines = 1
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -36,6 +37,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
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
|
||||||
@@ -69,13 +71,11 @@ import com.aiosman.ravenow.utils.DebounceUtils
|
|||||||
import com.aiosman.ravenow.utils.ResourceCleanupManager
|
import com.aiosman.ravenow.utils.ResourceCleanupManager
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.ui.zIndex
|
||||||
|
|
||||||
@OptIn( ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Agent() {
|
fun Agent() {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
@@ -96,6 +96,18 @@ fun Agent() {
|
|||||||
viewModel.ensureDataLoaded()
|
viewModel.ensureDataLoaded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听滚动状态,实现自动加载更多
|
||||||
|
LaunchedEffect(scrollState) {
|
||||||
|
snapshotFlow { scrollState.value }
|
||||||
|
.collect { scrollValue ->
|
||||||
|
val maxScroll = scrollState.maxValue
|
||||||
|
if (scrollValue >= maxScroll - 100 && !viewModel.isLoading) {
|
||||||
|
// 滚动到接近底部时加载更多
|
||||||
|
viewModel.loadMoreAgentData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 防抖状态
|
// 防抖状态
|
||||||
var lastClickTime by remember { mutableStateOf(0L) }
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
@@ -107,23 +119,21 @@ fun Agent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// 固定顶部搜索条
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxWidth()
|
||||||
.verticalScroll(scrollState)
|
.background(AppColors.background)
|
||||||
.padding(
|
.zIndex(999.0f)
|
||||||
top = statusBarPaddingValues.calculateTopPadding(),
|
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
|
||||||
bottom = navigationBarPaddings,
|
|
||||||
start = 16.dp,
|
|
||||||
end = 16.dp
|
|
||||||
),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.wrapContentHeight()
|
.fillMaxWidth()
|
||||||
.height(44.dp)
|
.fillMaxHeight().padding(top = 32.dp, start = 16.dp, end = 16.dp),
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -149,7 +159,21 @@ fun Agent() {
|
|||||||
colorFilter = ColorFilter.tint(AppColors.text)
|
colorFilter = ColorFilter.tint(AppColors.text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(15.dp))
|
}
|
||||||
|
|
||||||
|
// 可滚动的内容区域
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(
|
||||||
|
top = 44.dp + statusBarPaddingValues.calculateTopPadding() + 15.dp,
|
||||||
|
bottom = navigationBarPaddings,
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp
|
||||||
|
),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
// // 搜索框
|
// // 搜索框
|
||||||
// Row(
|
// Row(
|
||||||
// modifier = Modifier
|
// modifier = Modifier
|
||||||
@@ -229,9 +253,10 @@ fun Agent() {
|
|||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
|
|
||||||
var selectedTabIndex by remember { mutableStateOf(0) }
|
// 使用 ViewModel 中的选中状态
|
||||||
|
val selectedTabIndex = viewModel.selectedCategoryIndex
|
||||||
|
|
||||||
// 标签页
|
// 动态标签页
|
||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -240,83 +265,35 @@ fun Agent() {
|
|||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
viewModel.categories.forEachIndexed { index, category ->
|
||||||
item {
|
item {
|
||||||
CustomTabItem(
|
CustomTabItem(
|
||||||
text = stringResource(R.string.agent_recommend),
|
text = category.getLocalizedName(),
|
||||||
isSelected = selectedTabIndex == 0,
|
isSelected = selectedTabIndex == index,
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedTabIndex = 0
|
viewModel.selectCategory(index)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (index < viewModel.categories.size - 1) {
|
||||||
item {
|
item {
|
||||||
TabSpacer()
|
TabSpacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
|
||||||
CustomTabItem(
|
|
||||||
text = "scenes",
|
|
||||||
isSelected = selectedTabIndex == 1,
|
|
||||||
onClick = {
|
|
||||||
selectedTabIndex = 1
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
TabSpacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
CustomTabItem(
|
|
||||||
text = "voices",
|
|
||||||
isSelected = selectedTabIndex == 2,
|
|
||||||
onClick = {
|
|
||||||
selectedTabIndex = 2
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
TabSpacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
CustomTabItem(
|
|
||||||
text = "anime",
|
|
||||||
isSelected = selectedTabIndex == 3,
|
|
||||||
onClick = {
|
|
||||||
selectedTabIndex = 3
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
TabSpacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
CustomTabItem(
|
|
||||||
text = "assist",
|
|
||||||
isSelected = selectedTabIndex == 4,
|
|
||||||
onClick = {
|
|
||||||
selectedTabIndex = 4
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (selectedTabIndex) {
|
// 显示当前选中分类的 Agent 数据
|
||||||
0 -> {
|
|
||||||
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
|
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
|
||||||
}
|
}
|
||||||
else -> {
|
|
||||||
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
|
// 推荐聊天房间
|
||||||
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
|
ChatRoomsSection(
|
||||||
}
|
chatRooms = viewModel.chatRooms,
|
||||||
}
|
navController = LocalNavController.current
|
||||||
}
|
)
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -341,33 +318,76 @@ fun Agent() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(50.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
// Agent两列网格布局
|
||||||
.fillMaxWidth()
|
AgentGridLayout(
|
||||||
.weight(1f)
|
agentItems = viewModel.agentItems,
|
||||||
) {
|
|
||||||
val agentItems = viewModel.agentItems.take(15)
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = GridCells.Fixed(2),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(50.dp)
|
|
||||||
) {
|
|
||||||
items(agentItems) { agentItem ->
|
|
||||||
AgentCardSquare(
|
|
||||||
agentItem = agentItem,
|
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
navController = LocalNavController.current
|
navController = LocalNavController.current
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AgentGridLayout(
|
||||||
|
agentItems: List<AgentItem>,
|
||||||
|
viewModel: AgentViewModel,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// 将agentItems按两列分组
|
||||||
|
agentItems.chunked(2).forEachIndexed { rowIndex, rowItems ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
top = if (rowIndex == 0) 30.dp else 20.dp, // 第一行添加更多顶部间距
|
||||||
|
bottom = 20.dp
|
||||||
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// 第一列
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
AgentCardSquare(
|
||||||
|
agentItem = rowItems[0],
|
||||||
|
viewModel = viewModel,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二列(如果存在)
|
||||||
|
if (rowItems.size > 1) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
AgentCardSquare(
|
||||||
|
agentItem = rowItems[1],
|
||||||
|
viewModel = viewModel,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果只有一列,添加空白占位
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SuspiciousIndentation")
|
@SuppressLint("SuspiciousIndentation")
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navController: NavHostController) {
|
fun AgentCardSquare(
|
||||||
|
agentItem: AgentItem,
|
||||||
|
viewModel: AgentViewModel,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val cardHeight = 180.dp
|
val cardHeight = 180.dp
|
||||||
val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一
|
val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一
|
||||||
@@ -379,7 +399,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(cardHeight)
|
.height(cardHeight)
|
||||||
.background(Color(0xFFE0E0E0), RoundedCornerShape(12.dp)) // 灰色背景
|
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp)) // 主题背景
|
||||||
.clickable {
|
.clickable {
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
viewModel.goToProfile(agentItem.openId, navController)
|
viewModel.goToProfile(agentItem.openId, navController)
|
||||||
@@ -394,7 +414,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(y = -avatarSize / 2)
|
.offset(y = -avatarSize / 2)
|
||||||
.size(avatarSize)
|
.size(avatarSize)
|
||||||
.background(Color.White, RoundedCornerShape(avatarSize / 2))
|
.background(AppColors.background, RoundedCornerShape(avatarSize / 2))
|
||||||
.clip(RoundedCornerShape(avatarSize / 2)),
|
.clip(RoundedCornerShape(avatarSize / 2)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
@@ -421,7 +441,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = avatarSize / 2 + 8.dp, start = 8.dp, end = 8.dp, bottom = 8.dp),
|
.padding(top = avatarSize / 2 + 8.dp, start = 8.dp, end = 8.dp, bottom = 48.dp), // 为底部聊天按钮留出空间
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.Text(
|
androidx.compose.material3.Text(
|
||||||
@@ -443,16 +463,17 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
|
|||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f, fill = false)
|
modifier = Modifier.weight(1f, fill = false)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
// 聊天按钮,固定在底部居中,距离底部有一定边距
|
||||||
|
|
||||||
// 聊天按钮,位于底部居中
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 12.dp) // 距离底部的边距
|
||||||
.width(80.dp)
|
.width(80.dp)
|
||||||
.height(32.dp)
|
.height(32.dp)
|
||||||
.background(
|
.background(
|
||||||
color = Color(0X147c7480),
|
color = AppColors.inputBackground,
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
)
|
)
|
||||||
.clickable {
|
.clickable {
|
||||||
@@ -481,11 +502,11 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
|
fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
// 每页显示5个agent
|
// 每页显示5个agent
|
||||||
@@ -558,7 +579,13 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int, modifier: Modifier = Modifier,navController: NavHostController) {
|
fun AgentPage(
|
||||||
|
viewModel: AgentViewModel,
|
||||||
|
agentItems: List<AgentItem>,
|
||||||
|
page: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -566,7 +593,11 @@ fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int,
|
|||||||
) {
|
) {
|
||||||
// 显示3个agent
|
// 显示3个agent
|
||||||
agentItems.forEachIndexed { index, agentItem ->
|
agentItems.forEachIndexed { index, agentItem ->
|
||||||
AgentCard2(agentItem = agentItem, viewModel = viewModel, navController = LocalNavController.current)
|
AgentCard2(
|
||||||
|
agentItem = agentItem,
|
||||||
|
viewModel = viewModel,
|
||||||
|
navController = LocalNavController.current
|
||||||
|
)
|
||||||
if (index < agentItems.size - 1) {
|
if (index < agentItems.size - 1) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
@@ -576,7 +607,7 @@ fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int,
|
|||||||
|
|
||||||
@SuppressLint("SuspiciousIndentation")
|
@SuppressLint("SuspiciousIndentation")
|
||||||
@Composable
|
@Composable
|
||||||
fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: NavHostController) {
|
fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: NavHostController) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
// 防抖状态
|
// 防抖状态
|
||||||
@@ -592,7 +623,7 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.background(Color(0xFFF5F5F5), RoundedCornerShape(24.dp))
|
.background(AppColors.secondaryBackground, RoundedCornerShape(24.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
viewModel.goToProfile(agentItem.openId, navController)
|
viewModel.goToProfile(agentItem.openId, navController)
|
||||||
@@ -656,7 +687,7 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = 60.dp, height = 32.dp)
|
.size(width = 60.dp, height = 32.dp)
|
||||||
.background(
|
.background(
|
||||||
color = Color(0X147c7480),
|
color = AppColors.inputBackground,
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
)
|
)
|
||||||
.clickable {
|
.clickable {
|
||||||
@@ -686,3 +717,135 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatRoomsSection(
|
||||||
|
chatRooms: List<ChatRoom>,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// 标题
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.rider_pro_agent2),
|
||||||
|
contentDescription = "chat room",
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = stringResource(R.string.agent_chat_room),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3行宫格布局
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// 将聊天房间按3个一组分组
|
||||||
|
chatRooms.chunked(3).forEach { rowRooms ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
rowRooms.forEach { chatRoom ->
|
||||||
|
ChatRoomCard(
|
||||||
|
chatRoom = chatRoom,
|
||||||
|
navController = navController,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 如果这一行不足3个,添加空白占位
|
||||||
|
repeat(3 - rowRooms.size) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatRoomCard(
|
||||||
|
chatRoom: ChatRoom,
|
||||||
|
navController: NavHostController,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val cardSize = 100.dp
|
||||||
|
|
||||||
|
// 防抖状态
|
||||||
|
var lastClickTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
|
// 正方形卡片,文字重叠在底部
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(cardSize)
|
||||||
|
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
|
||||||
|
.clickable {
|
||||||
|
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
|
||||||
|
// 这里可以添加进入聊天房间的逻辑
|
||||||
|
// navController.navigate(NavigationRoute.ChatRoom.route.replace("{id}", chatRoom.id))
|
||||||
|
}) {
|
||||||
|
lastClickTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// 优先显示banner,如果没有banner则显示头像
|
||||||
|
val imageUrl = if (chatRoom.banner.isNotEmpty()) chatRoom.banner else chatRoom.avatar
|
||||||
|
|
||||||
|
if (imageUrl.isNotEmpty()) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(cardSize)
|
||||||
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 默认房间图标
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.mipmap.rider_pro_agent),
|
||||||
|
contentDescription = "默认房间图标",
|
||||||
|
modifier = Modifier.size(cardSize * 0.4f),
|
||||||
|
colorFilter = ColorFilter.tint(AppColors.secondaryText)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 房间名称,重叠在底部
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.6f),
|
||||||
|
shape = RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = chatRoom.name,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import com.aiosman.ravenow.AppState
|
||||||
|
import com.aiosman.ravenow.AppStore
|
||||||
|
import com.aiosman.ravenow.ConstVars
|
||||||
|
import com.aiosman.ravenow.data.Room
|
||||||
import com.aiosman.ravenow.data.api.ApiClient
|
import com.aiosman.ravenow.data.api.ApiClient
|
||||||
import com.aiosman.ravenow.data.api.RaveNowAPI
|
import com.aiosman.ravenow.data.api.RaveNowAPI
|
||||||
import com.aiosman.ravenow.data.api.SingleChatRequestBody
|
import com.aiosman.ravenow.data.api.SingleChatRequestBody
|
||||||
@@ -13,8 +17,18 @@ import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createG
|
|||||||
import com.aiosman.ravenow.ui.NavigationRoute
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
|
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
|
||||||
|
import com.aiosman.ravenow.entity.CategoryEntity
|
||||||
|
import com.aiosman.ravenow.entity.CategoryBackend
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class ChatRoom(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val avatar: String = "",
|
||||||
|
val banner: String = "",
|
||||||
|
val memberCount: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
object AgentViewModel: ViewModel() {
|
object AgentViewModel: ViewModel() {
|
||||||
|
|
||||||
private val apiClient: RaveNowAPI = ApiClient.api
|
private val apiClient: RaveNowAPI = ApiClient.api
|
||||||
@@ -22,32 +36,65 @@ object AgentViewModel: ViewModel() {
|
|||||||
var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
|
var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var chatRooms by mutableStateOf<List<ChatRoom>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var rooms by mutableStateOf<List<Room>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var categories by mutableStateOf<List<CategoryEntity>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var selectedCategoryIndex by mutableStateOf(0)
|
||||||
|
private set
|
||||||
|
|
||||||
var errorMessage by mutableStateOf<String?>(null)
|
var errorMessage by mutableStateOf<String?>(null)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
||||||
var isRefreshing by mutableStateOf(false)
|
var isRefreshing by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var isLoading by mutableStateOf(false)
|
var isLoading by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
private var currentPage = 1
|
||||||
|
private var hasMoreData = true
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadAgentData()
|
loadAgentData()
|
||||||
|
loadChatRooms()
|
||||||
|
loadCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAgentData() {
|
private fun loadAgentData(categoryIndex: Int = 0) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
|
currentPage = 1
|
||||||
|
hasMoreData = true
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
|
val selectedCategory = if (categoryIndex < categories.size) categories[categoryIndex] else null
|
||||||
|
|
||||||
|
val response = if (categoryIndex == 0 || selectedCategory == null) {
|
||||||
|
// 推荐分类或无效分类,加载所有 Agent
|
||||||
|
apiClient.getAgent(page = currentPage, pageSize = 20, withWorkflow = "1", random = "true")
|
||||||
|
} else {
|
||||||
|
// 特定分类,使用 categoryName 参数
|
||||||
|
apiClient.getAgent(
|
||||||
|
page = currentPage,
|
||||||
|
pageSize = 20,
|
||||||
|
withWorkflow = "1",
|
||||||
|
categoryName = selectedCategory.name,
|
||||||
|
random = "true"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
agentItems = agents.map { agent ->
|
agentItems = agents.map { agent ->
|
||||||
AgentItem.fromAgent(agent)
|
AgentItem.fromAgent(agent)
|
||||||
}
|
}
|
||||||
|
hasMoreData = agents.size >= 20
|
||||||
} else {
|
} else {
|
||||||
errorMessage = "获取Agent数据失败: ${response.code()}"
|
errorMessage = "获取Agent数据失败: ${response.code()}"
|
||||||
}
|
}
|
||||||
@@ -58,6 +105,129 @@ object AgentViewModel: ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadChatRooms() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val response = apiClient.getRooms(page = 1, pageSize = 21, isRecommended = 1, random = 1) // 请求21个,确保是3的倍数
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val allRooms = response.body()?.list ?: emptyList()
|
||||||
|
// 确保房间数量是3的倍数,如果不足则截取,如果超出则取前几个
|
||||||
|
val targetCount = (allRooms.size / 3) * 3 // 向下取整到最近的3的倍数
|
||||||
|
rooms = allRooms.take(targetCount)
|
||||||
|
|
||||||
|
// 转换为ChatRoom格式用于兼容现有UI
|
||||||
|
chatRooms = rooms.map { room ->
|
||||||
|
ChatRoom(
|
||||||
|
id = room.trtcRoomId,
|
||||||
|
name = room.name,
|
||||||
|
avatar = room.avatar,
|
||||||
|
banner = ConstVars.BASE_SERVER + "/api/v1/outside/" + room.recommendBanner + "?token=${AppStore.token}",
|
||||||
|
memberCount = room.userCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 如果网络请求失败,使用默认数据
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadCategories() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val categoryBackend = CategoryBackend()
|
||||||
|
val response = categoryBackend.getCategories(1)
|
||||||
|
if (response != null) {
|
||||||
|
// 添加一个默认的"推荐"分类在第一位
|
||||||
|
val recommendCategory = CategoryEntity(
|
||||||
|
id = 0,
|
||||||
|
name = "推荐",
|
||||||
|
description = "推荐内容",
|
||||||
|
avatar = "",
|
||||||
|
parentId = null,
|
||||||
|
sort = 0,
|
||||||
|
isActive = true,
|
||||||
|
promptCount = 0,
|
||||||
|
createdAt = "",
|
||||||
|
updatedAt = "",
|
||||||
|
translations = null
|
||||||
|
)
|
||||||
|
categories = listOf(recommendCategory) + response.list
|
||||||
|
|
||||||
|
// 分类加载完成后,重新加载当前选中分类的 Agent 数据
|
||||||
|
if (agentItems.isEmpty()) {
|
||||||
|
loadAgentData(selectedCategoryIndex)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果请求失败,使用默认分类
|
||||||
|
categories = listOf()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 如果网络请求失败,使用默认分类
|
||||||
|
categories = listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载更多Agent数据
|
||||||
|
*/
|
||||||
|
fun loadMoreAgentData() {
|
||||||
|
if (!hasMoreData || isLoading) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
isLoading = true
|
||||||
|
try {
|
||||||
|
val nextPage = currentPage + 1
|
||||||
|
val selectedCategory = if (selectedCategoryIndex < categories.size) categories[selectedCategoryIndex] else null
|
||||||
|
|
||||||
|
val response = if (selectedCategoryIndex == 0 || selectedCategory == null) {
|
||||||
|
// 推荐分类或无效分类,加载所有 Agent
|
||||||
|
apiClient.getAgent(page = nextPage, pageSize = 20, withWorkflow = "1")
|
||||||
|
} else {
|
||||||
|
// 特定分类,使用 categoryName 参数
|
||||||
|
apiClient.getAgent(
|
||||||
|
page = nextPage,
|
||||||
|
pageSize = 20,
|
||||||
|
withWorkflow = "1",
|
||||||
|
categoryName = selectedCategory.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
|
val newAgentItems = agents.map { agent ->
|
||||||
|
AgentItem.fromAgent(agent)
|
||||||
|
}
|
||||||
|
agentItems = agentItems + newAgentItems
|
||||||
|
currentPage = nextPage
|
||||||
|
hasMoreData = agents.size >= 20
|
||||||
|
} else {
|
||||||
|
errorMessage = "加载更多Agent数据失败: ${response.code()}"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = "网络请求失败: ${e.message}"
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择分类并加载对应的 Agent 数据
|
||||||
|
*/
|
||||||
|
fun selectCategory(categoryIndex: Int) {
|
||||||
|
if (categoryIndex != selectedCategoryIndex) {
|
||||||
|
selectedCategoryIndex = categoryIndex
|
||||||
|
loadAgentData(categoryIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createSingleChat(
|
fun createSingleChat(
|
||||||
openId: String,
|
openId: String,
|
||||||
) {
|
) {
|
||||||
@@ -116,9 +286,13 @@ object AgentViewModel: ViewModel() {
|
|||||||
*/
|
*/
|
||||||
fun ResetModel() {
|
fun ResetModel() {
|
||||||
agentItems = emptyList()
|
agentItems = emptyList()
|
||||||
|
categories = emptyList()
|
||||||
|
selectedCategoryIndex = 0
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
currentPage = 1
|
||||||
|
hasMoreData = true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,8 @@ 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.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -45,17 +47,24 @@ import com.aiosman.ravenow.ui.index.tabs.moment.tabs.dynamic.Dynamic
|
|||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.Explore
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.Explore
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.hot.HotMomentsList
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.hot.HotMomentsList
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.news.News
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.news.News
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend.Recommend
|
||||||
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentsList
|
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.timeline.TimelineMomentsList
|
||||||
import com.aiosman.ravenow.ui.index.tabs.search.SearchViewModel
|
import com.aiosman.ravenow.ui.index.tabs.search.SearchViewModel
|
||||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import com.aiosman.ravenow.ui.composables.TabItem
|
import com.aiosman.ravenow.ui.composables.TabItem
|
||||||
import com.aiosman.ravenow.ui.composables.TabSpacer
|
import com.aiosman.ravenow.ui.composables.TabSpacer
|
||||||
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
import com.aiosman.ravenow.ui.composables.rememberDebouncer
|
||||||
|
|
||||||
|
data class TabData(
|
||||||
|
val text: String,
|
||||||
|
val index: Int
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态列表
|
* 动态列表
|
||||||
*/
|
*/
|
||||||
@@ -67,8 +76,8 @@ fun MomentsList() {
|
|||||||
val navigationBarPaddings =
|
val navigationBarPaddings =
|
||||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
|
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
|
||||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
// 游客模式下显示3个tab:Worldwide、Hot、News,非游客模式显示4个tab:Worldwide、Following、Hot、News
|
// 游客模式下显示4个tab:Worldwide、Hot、News、Recommend,非游客模式显示5个tab:Worldwide、Following、Hot、News、Recommend
|
||||||
val tabCount = if (AppStore.isGuest) 3 else 4
|
val tabCount = if (AppStore.isGuest) 4 else 5
|
||||||
var pagerState = rememberPagerState { tabCount }
|
var pagerState = rememberPagerState { tabCount }
|
||||||
var scope = rememberCoroutineScope()
|
var scope = rememberCoroutineScope()
|
||||||
Column(
|
Column(
|
||||||
@@ -144,117 +153,49 @@ fun MomentsList() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(23.dp))
|
// Spacer(modifier = Modifier.height(23.dp))
|
||||||
|
|
||||||
Row(
|
val tabDebouncer = rememberDebouncer()
|
||||||
|
|
||||||
|
// 创建tab数据列表
|
||||||
|
val tabs = if (AppStore.isGuest) {
|
||||||
|
listOf(
|
||||||
|
TabData(stringResource(R.string.index_worldwide), 0),
|
||||||
|
TabData(stringResource(R.string.index_hot), 1),
|
||||||
|
TabData(stringResource(R.string.index_news), 2),
|
||||||
|
TabData(stringResource(R.string.index_recommend), 3)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
TabData(stringResource(R.string.index_worldwide), 0),
|
||||||
|
TabData(stringResource(R.string.index_following), 1),
|
||||||
|
TabData(stringResource(R.string.index_hot), 2),
|
||||||
|
TabData(stringResource(R.string.index_news), 3),
|
||||||
|
TabData(stringResource(R.string.index_recommend), 4)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.padding(start = 16.dp, bottom = 16.dp),
|
.padding(start = 16.dp, bottom = 16.dp),
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.Bottom
|
verticalAlignment = Alignment.Bottom
|
||||||
) {
|
) {
|
||||||
val tabDebouncer = rememberDebouncer()
|
items(tabs) { tab ->
|
||||||
|
UnderlineTabItem(
|
||||||
// 新探索标签
|
text = tab.text,
|
||||||
Box {
|
isSelected = pagerState.currentPage == tab.index,
|
||||||
CustomTabItem(
|
|
||||||
text = stringResource(R.string.index_worldwide),
|
|
||||||
isSelected = pagerState.currentPage == 0,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
tabDebouncer {
|
tabDebouncer {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(0)
|
pagerState.animateScrollToPage(tab.index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TabSpacer()
|
|
||||||
|
|
||||||
// 只有非游客用户才显示"关注"tab
|
|
||||||
if (!AppStore.isGuest) {
|
|
||||||
Box {
|
|
||||||
CustomTabItem(
|
|
||||||
text = stringResource(R.string.index_following),
|
|
||||||
isSelected = pagerState.currentPage == 1,
|
|
||||||
onClick = {
|
|
||||||
tabDebouncer {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TabSpacer()
|
|
||||||
|
|
||||||
// 热门标签
|
|
||||||
Box {
|
|
||||||
CustomTabItem(
|
|
||||||
text = stringResource(R.string.index_hot),
|
|
||||||
isSelected = pagerState.currentPage == 2,
|
|
||||||
onClick = {
|
|
||||||
tabDebouncer {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TabSpacer()
|
|
||||||
|
|
||||||
// 新闻标签
|
|
||||||
Box {
|
|
||||||
CustomTabItem(
|
|
||||||
text = stringResource(R.string.index_news),
|
|
||||||
isSelected = pagerState.currentPage == 3,
|
|
||||||
onClick = {
|
|
||||||
tabDebouncer {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 热门标签 (游客模式)
|
|
||||||
Box {
|
|
||||||
CustomTabItem(
|
|
||||||
text = stringResource(R.string.index_hot),
|
|
||||||
isSelected = pagerState.currentPage == 1,
|
|
||||||
onClick = {
|
|
||||||
tabDebouncer {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TabSpacer()
|
|
||||||
|
|
||||||
// 新闻标签 (游客模式)
|
|
||||||
Box {
|
|
||||||
CustomTabItem(
|
|
||||||
text = stringResource(R.string.index_news),
|
|
||||||
isSelected = pagerState.currentPage == 2,
|
|
||||||
onClick = {
|
|
||||||
tabDebouncer {
|
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
@@ -264,7 +205,7 @@ fun MomentsList() {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
if (AppStore.isGuest) {
|
if (AppStore.isGuest) {
|
||||||
// 游客模式:Worldwide(0), Hot(1), News(2)
|
// 游客模式:Worldwide(0), Hot(1), News(2), Recommend(3)
|
||||||
when (it) {
|
when (it) {
|
||||||
0 -> {
|
0 -> {
|
||||||
Dynamic()
|
Dynamic()
|
||||||
@@ -275,9 +216,12 @@ fun MomentsList() {
|
|||||||
2 -> {
|
2 -> {
|
||||||
News()
|
News()
|
||||||
}
|
}
|
||||||
|
3 -> {
|
||||||
|
Recommend()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 正常用户:Worldwide(0), Following(1), Hot(2), News(3)
|
// 正常用户:Worldwide(0), Following(1), Hot(2), News(3), Recommend(4)
|
||||||
when (it) {
|
when (it) {
|
||||||
0 -> {
|
0 -> {
|
||||||
Dynamic()
|
Dynamic()
|
||||||
@@ -291,11 +235,49 @@ fun MomentsList() {
|
|||||||
3 -> {
|
3 -> {
|
||||||
News()
|
News()
|
||||||
}
|
}
|
||||||
|
4 -> {
|
||||||
|
Recommend()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Composable
|
||||||
|
fun UnderlineTabItem(
|
||||||
|
text: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.noRippleClickable { onClick() },
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = if (isSelected) 18.sp else 16.sp,
|
||||||
|
color = if (isSelected) AppColors.text else AppColors.nonActiveText,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(34.dp)
|
||||||
|
.height(4.dp)
|
||||||
|
.background(
|
||||||
|
color = if (isSelected) AppColors.text else Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(2.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomTabItem(
|
fun CustomTabItem(
|
||||||
text: String,
|
text: String,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class ExploreViewModel : ViewModel() {
|
|||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
|
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = "1")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
agentItems = agents.map { agent ->
|
agentItems = agents.map { agent ->
|
||||||
@@ -114,7 +114,7 @@ class ExploreViewModel : ViewModel() {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
try {
|
try {
|
||||||
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
|
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = "1")
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val agents = response.body()?.data?.list ?: emptyList()
|
val agents = response.body()?.data?.list ?: emptyList()
|
||||||
agentItems = agents.map { agent ->
|
agentItems = agents.map { agent ->
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.compose.AsyncImagePainter
|
||||||
|
import coil.compose.SubcomposeAsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.request.CachePolicy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预加载图片组件 - 专门用于推荐页面的图片预加载
|
||||||
|
* 支持预加载周围页面的图片,提升滑动体验
|
||||||
|
* 增加了loading状态指示器
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AsyncImage(
|
||||||
|
imageUrl: String,
|
||||||
|
contentDescription: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentScale: ContentScale = ContentScale.Crop,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
SubcomposeAsyncImage(
|
||||||
|
model = ImageRequest.Builder(context)
|
||||||
|
.data(imageUrl)
|
||||||
|
.crossfade(true)
|
||||||
|
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||||
|
.diskCachePolicy(CachePolicy.ENABLED)
|
||||||
|
.build(),
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = contentScale,
|
||||||
|
loading = {
|
||||||
|
// Loading 状态
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.8f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
color = Color.White,
|
||||||
|
strokeWidth = 3.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error = {
|
||||||
|
// 错误状态
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = "图片加载失败",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOut
|
||||||
|
import com.aiosman.ravenow.GuestLoginCheckOutScene
|
||||||
|
import com.aiosman.ravenow.LocalAppTheme
|
||||||
|
import com.aiosman.ravenow.LocalNavController
|
||||||
|
import com.aiosman.ravenow.R
|
||||||
|
import com.aiosman.ravenow.ui.NavigationRoute
|
||||||
|
import com.aiosman.ravenow.ui.comment.CommentModalContent
|
||||||
|
import com.aiosman.ravenow.entity.MomentEntity
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推荐动态列表 - 垂直滑动样式
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun Recommend() {
|
||||||
|
val model = RecommendViewModel
|
||||||
|
val moments = model.moments
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
model.refreshPager()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moments.isEmpty()) {
|
||||||
|
// 空状态
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "暂无推荐内容",
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用垂直滑动的Pager
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
RecommendPager(
|
||||||
|
moments = moments,
|
||||||
|
model = model,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
|
||||||
|
// 加载更多状态指示器
|
||||||
|
if (model.refreshing) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.3f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "加载更多中...",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RecommendPager(
|
||||||
|
moments: List<MomentEntity>,
|
||||||
|
model: RecommendViewModel,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
|
val pagerState = remember {
|
||||||
|
RecommendPagerState(
|
||||||
|
currentPage = 0,
|
||||||
|
minPage = 0,
|
||||||
|
maxPage = moments.size - 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var showCommentModal by remember { mutableStateOf(false) }
|
||||||
|
var currentMoment by remember { mutableStateOf<MomentEntity?>(null) }
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
val appColor = LocalAppTheme.current
|
||||||
|
|
||||||
|
// 当moments列表变化时,更新pagerState的maxPage
|
||||||
|
LaunchedEffect(moments.size) {
|
||||||
|
pagerState.maxPage = moments.size - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
RecommendPager(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = pagerState,
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
offscreenLimit = 1,
|
||||||
|
onLoadMore = {
|
||||||
|
if (!model.refreshing) {
|
||||||
|
model.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val momentItem = moments[page]
|
||||||
|
|
||||||
|
SingleRecommendItemContent(
|
||||||
|
momentEntity = momentItem,
|
||||||
|
pagerState = pagerState,
|
||||||
|
page = page,
|
||||||
|
onCommentClick = {
|
||||||
|
currentMoment = momentItem
|
||||||
|
showCommentModal = true
|
||||||
|
},
|
||||||
|
onLikeClick = {
|
||||||
|
scope.launch {
|
||||||
|
if (momentItem.liked) {
|
||||||
|
model.dislikeMoment(momentItem.id)
|
||||||
|
} else {
|
||||||
|
model.likeMoment(momentItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFavoriteClick = {
|
||||||
|
scope.launch {
|
||||||
|
if (momentItem.isFavorite) {
|
||||||
|
model.unfavoriteMoment(momentItem.id)
|
||||||
|
} else {
|
||||||
|
model.favoriteMoment(momentItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFollowClick = {
|
||||||
|
model.followAction(momentItem)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCommentModal && currentMoment != null) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { showCommentModal = false },
|
||||||
|
containerColor = appColor.background,
|
||||||
|
sheetState = sheetState
|
||||||
|
) {
|
||||||
|
CommentModalContent(
|
||||||
|
postId = currentMoment!!.id,
|
||||||
|
|
||||||
|
) {
|
||||||
|
// 评论回调
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SingleRecommendItemContent(
|
||||||
|
momentEntity: MomentEntity,
|
||||||
|
pagerState: RecommendPagerState,
|
||||||
|
page: Int,
|
||||||
|
onCommentClick: () -> Unit,
|
||||||
|
onLikeClick: () -> Unit,
|
||||||
|
onFavoriteClick: () -> Unit,
|
||||||
|
onFollowClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize().background(AppColors.background)) {
|
||||||
|
// 主图片内容
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (momentEntity.images.isNotEmpty()) {
|
||||||
|
AsyncImage(
|
||||||
|
imageUrl = momentEntity.images[0].thumbnail,
|
||||||
|
contentDescription = "推荐内容",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 默认背景
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧操作按钮
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.BottomEnd
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(bottom = 72.dp, end = 12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// 用户头像
|
||||||
|
UserAvatar(momentEntity = momentEntity)
|
||||||
|
|
||||||
|
// 点赞按钮
|
||||||
|
RecommendActionButton(
|
||||||
|
icon = R.drawable.rider_pro_video_like,
|
||||||
|
text = formatCount(momentEntity.likeCount),
|
||||||
|
isActive = momentEntity.liked,
|
||||||
|
onClick = {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
onLikeClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 评论按钮
|
||||||
|
RecommendActionButton(
|
||||||
|
icon = R.drawable.rider_pro_video_comment,
|
||||||
|
text = formatCount(momentEntity.commentCount),
|
||||||
|
onClick = {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
onCommentClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 收藏按钮
|
||||||
|
RecommendActionButton(
|
||||||
|
icon = R.drawable.rider_pro_video_favor,
|
||||||
|
text = formatCount(momentEntity.favoriteCount),
|
||||||
|
isActive = momentEntity.isFavorite,
|
||||||
|
onClick = {
|
||||||
|
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||||
|
navController.navigate(NavigationRoute.Login.route)
|
||||||
|
} else {
|
||||||
|
onFavoriteClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 分享按钮
|
||||||
|
RecommendActionButton(
|
||||||
|
icon = R.drawable.rider_pro_video_share,
|
||||||
|
text = "分享",
|
||||||
|
onClick = {
|
||||||
|
// TODO: 实现分享功能
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部信息
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.BottomStart
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(start = 16.dp, bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
// 用户信息
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "@${momentEntity.nickname}",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.White,
|
||||||
|
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容描述
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp),
|
||||||
|
text = momentEntity.momentTextContent,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.White,
|
||||||
|
style = TextStyle(fontWeight = FontWeight.Bold),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 3
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserAvatar(momentEntity: MomentEntity) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.size(40.dp)
|
||||||
|
.border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp))
|
||||||
|
.clip(RoundedCornerShape(40.dp))
|
||||||
|
) {
|
||||||
|
if (momentEntity.avatar.isNotEmpty()) {
|
||||||
|
AsyncImage(
|
||||||
|
imageUrl = momentEntity.avatar,
|
||||||
|
contentDescription = "用户头像",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.default_avatar),
|
||||||
|
contentDescription = "默认头像",
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RecommendActionButton(
|
||||||
|
icon: Int,
|
||||||
|
text: String,
|
||||||
|
isActive: Boolean = false,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.clickable { onClick() },
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
painter = painterResource(id = icon),
|
||||||
|
contentDescription = "",
|
||||||
|
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
|
||||||
|
if (isActive) Color.Red else Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color.White,
|
||||||
|
style = TextStyle(fontWeight = FontWeight.Bold)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatCount(count: Int): String {
|
||||||
|
return when {
|
||||||
|
count >= 1000000 -> "${(count / 1000000.0).let { "%.1f".format(it) }}M"
|
||||||
|
count >= 1000 -> "${(count / 1000.0).let { "%.1f".format(it) }}k"
|
||||||
|
else -> count.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.draggable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
import androidx.compose.ui.layout.ParentDataModifier
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class RecommendPagerState(
|
||||||
|
currentPage: Int = 0,
|
||||||
|
minPage: Int = 0,
|
||||||
|
maxPage: Int = 0
|
||||||
|
) {
|
||||||
|
private var _minPage by mutableStateOf(minPage)
|
||||||
|
var minPage: Int
|
||||||
|
get() = _minPage
|
||||||
|
set(value) {
|
||||||
|
_minPage = value.coerceAtMost(_maxPage)
|
||||||
|
_currentPage = _currentPage.coerceIn(_minPage, _maxPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _maxPage by mutableStateOf(maxPage, structuralEqualityPolicy())
|
||||||
|
var maxPage: Int
|
||||||
|
get() = _maxPage
|
||||||
|
set(value) {
|
||||||
|
_maxPage = value.coerceAtLeast(_minPage)
|
||||||
|
_currentPage = _currentPage.coerceIn(_minPage, maxPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _currentPage by mutableStateOf(currentPage.coerceIn(minPage, maxPage))
|
||||||
|
var currentPage: Int
|
||||||
|
get() = _currentPage
|
||||||
|
set(value) {
|
||||||
|
_currentPage = value.coerceIn(minPage, maxPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SelectionState { Selected, Undecided }
|
||||||
|
|
||||||
|
var selectionState by mutableStateOf(SelectionState.Selected)
|
||||||
|
|
||||||
|
suspend inline fun <R> selectPage(block: RecommendPagerState.() -> R): R = try {
|
||||||
|
selectionState = SelectionState.Undecided
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
selectPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun selectPage() {
|
||||||
|
currentPage -= currentPageOffset.roundToInt()
|
||||||
|
snapToOffset(0f)
|
||||||
|
selectionState = SelectionState.Selected
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _currentPageOffset = Animatable(0f).apply {
|
||||||
|
updateBounds(-1f, 1f)
|
||||||
|
}
|
||||||
|
val currentPageOffset: Float
|
||||||
|
get() = _currentPageOffset.value
|
||||||
|
|
||||||
|
suspend fun snapToOffset(offset: Float) {
|
||||||
|
val max = if (currentPage == minPage) 0f else 1f
|
||||||
|
val min = if (currentPage == maxPage) 0f else -1f
|
||||||
|
_currentPageOffset.snapTo(offset.coerceIn(min, max))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fling(velocity: Float) {
|
||||||
|
if (velocity < 0 && currentPage == maxPage) return
|
||||||
|
if (velocity > 0 && currentPage == minPage) return
|
||||||
|
|
||||||
|
// 根据 fling 的方向滑动到下一页或上一页
|
||||||
|
_currentPageOffset.animateTo(velocity)
|
||||||
|
selectPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "RecommendPagerState{minPage=$minPage, maxPage=$maxPage, " +
|
||||||
|
"currentPage=$currentPage, currentPageOffset=$currentPageOffset}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
private data class PageData(val page: Int) : ParentDataModifier {
|
||||||
|
override fun Density.modifyParentData(parentData: Any?): Any? = this@PageData
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Measurable.page: Int
|
||||||
|
get() = (parentData as? PageData)?.page ?: error("no PageData for measurable $this")
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RecommendPager(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
state: RecommendPagerState,
|
||||||
|
orientation: Orientation = Orientation.Horizontal,
|
||||||
|
offscreenLimit: Int = 2,
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
||||||
|
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||||
|
onLoadMore: (() -> Unit)? = null,
|
||||||
|
content: @Composable RecommendPagerScope.() -> Unit
|
||||||
|
) {
|
||||||
|
var pageSize by remember { mutableStateOf(0) }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// 监听当前页面变化,当到达倒数第一个时触发加载更多
|
||||||
|
LaunchedEffect(state.currentPage, state.maxPage) {
|
||||||
|
if (state.currentPage >= state.maxPage - 1 && onLoadMore != null) {
|
||||||
|
onLoadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout(
|
||||||
|
content = {
|
||||||
|
// 根据 offscreenLimit 计算页面范围
|
||||||
|
val minPage = maxOf(state.currentPage - offscreenLimit, state.minPage)
|
||||||
|
val maxPage = minOf(state.currentPage + offscreenLimit, state.maxPage)
|
||||||
|
|
||||||
|
for (page in minPage..maxPage) {
|
||||||
|
val pageData = PageData(page)
|
||||||
|
val scope = RecommendPagerScope(state, page)
|
||||||
|
key(pageData) {
|
||||||
|
Column(
|
||||||
|
modifier = pageData
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
scope.content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier.draggable(
|
||||||
|
orientation = orientation,
|
||||||
|
onDragStarted = {
|
||||||
|
state.selectionState = RecommendPagerState.SelectionState.Undecided
|
||||||
|
},
|
||||||
|
onDragStopped = { velocity ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
// 根据速度判断是否滑动到下一页
|
||||||
|
val threshold = 1000f // 速度阈值,可调整
|
||||||
|
if (velocity > threshold) {
|
||||||
|
state.fling(1f) // 向下滑动
|
||||||
|
} else if (velocity < -threshold) {
|
||||||
|
state.fling(-1f) // 向上滑动
|
||||||
|
} else {
|
||||||
|
state.fling(0f) // 保持当前页
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state = rememberDraggableState { dy ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
with(state) {
|
||||||
|
val pos = pageSize * currentPageOffset
|
||||||
|
val max = if (currentPage == minPage) 0 else pageSize
|
||||||
|
val min = if (currentPage == maxPage) 0 else -pageSize
|
||||||
|
|
||||||
|
// 直接将手指的位移应用到 currentPageOffset
|
||||||
|
val newPos = (pos + dy).coerceIn(min.toFloat(), max.toFloat())
|
||||||
|
snapToOffset(newPos / pageSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
) { measurables, constraints ->
|
||||||
|
layout(constraints.maxWidth, constraints.maxHeight) {
|
||||||
|
val currentPage = state.currentPage
|
||||||
|
val offset = state.currentPageOffset
|
||||||
|
val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||||
|
|
||||||
|
measurables.forEach { measurable ->
|
||||||
|
val placeable = measurable.measure(childConstraints)
|
||||||
|
val page = measurable.page
|
||||||
|
|
||||||
|
// 根据对齐参数计算 x 和 y 位置
|
||||||
|
val xPosition = when (horizontalAlignment) {
|
||||||
|
Alignment.Start -> 0
|
||||||
|
Alignment.CenterHorizontally -> (constraints.maxWidth - placeable.width) / 2
|
||||||
|
Alignment.End -> constraints.maxWidth - placeable.width
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val yPosition = when (verticalAlignment) {
|
||||||
|
Alignment.Top -> 0
|
||||||
|
Alignment.CenterVertically -> (constraints.maxHeight - placeable.height) / 2
|
||||||
|
Alignment.Bottom -> constraints.maxHeight - placeable.height
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage == page) { // 只在当前页面设置 pageSize,避免不必要的设置
|
||||||
|
pageSize = if (orientation == Orientation.Horizontal) {
|
||||||
|
placeable.width
|
||||||
|
} else {
|
||||||
|
placeable.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isVisible = abs(page - (currentPage - offset)) <= 1
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
// 修正 y 的计算(垂直滑动)
|
||||||
|
val yOffset = if (orientation == Orientation.Vertical) {
|
||||||
|
((page - currentPage) * pageSize + offset * pageSize).roundToInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保内容不会溢出到容器顶部
|
||||||
|
val finalYPosition = (yPosition + yOffset).coerceAtLeast(0)
|
||||||
|
|
||||||
|
// 使用 placeRelative 进行放置
|
||||||
|
placeable.placeRelative(
|
||||||
|
x = xPosition + if (orientation == Orientation.Horizontal) ((page - (currentPage - offset)) * placeable.width).roundToInt() else 0,
|
||||||
|
y = finalYPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecommendPagerScope(
|
||||||
|
private val state: RecommendPagerState,
|
||||||
|
val page: Int
|
||||||
|
) {
|
||||||
|
val currentPage: Int
|
||||||
|
get() = state.currentPage
|
||||||
|
|
||||||
|
val currentPageOffset: Float
|
||||||
|
get() = state.currentPageOffset
|
||||||
|
|
||||||
|
val selectionState: RecommendPagerState.SelectionState
|
||||||
|
get() = state.selectionState
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend
|
||||||
|
|
||||||
|
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||||
|
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
|
object RecommendViewModel : BaseMomentModel() {
|
||||||
|
init {
|
||||||
|
EventBus.getDefault().register(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun extraArgs(): MomentLoaderExtraArgs {
|
||||||
|
return MomentLoaderExtraArgs(trend = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,23 @@ object Utils {
|
|||||||
return Locale.getDefault().language
|
return Locale.getDefault().language
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前系统语言的完整标签,包含国家/地区代码
|
||||||
|
* 返回格式:en, ja, zh-CN, zh-TW 等
|
||||||
|
*/
|
||||||
|
fun getCurrentLanguageTag(): String {
|
||||||
|
val locale = Locale.getDefault()
|
||||||
|
val language = locale.language
|
||||||
|
val country = locale.country
|
||||||
|
|
||||||
|
return when {
|
||||||
|
// 中文需要区分简体和繁体
|
||||||
|
language == "zh" && country.isNotEmpty() -> "$language-$country"
|
||||||
|
// 其他语言通常只需要语言代码
|
||||||
|
else -> language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
|
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
|||||||
@@ -33,4 +33,28 @@
|
|||||||
<string name="index_following">フォロー中</string>
|
<string name="index_following">フォロー中</string>
|
||||||
<string name="index_hot">ホット</string>
|
<string name="index_hot">ホット</string>
|
||||||
<string name="index_news">ニュース</string>
|
<string name="index_news">ニュース</string>
|
||||||
|
|
||||||
|
<!-- Main navigation -->
|
||||||
|
<string name="main_home">ホーム</string>
|
||||||
|
<string name="main_ai">エージェント</string>
|
||||||
|
<string name="main_message">メッセージ</string>
|
||||||
|
<string name="main_profile">プロフィール</string>
|
||||||
|
|
||||||
|
<!-- Agent related strings -->
|
||||||
|
<string name="agent_mine">マイ</string>
|
||||||
|
<string name="agent_hot">ホット</string>
|
||||||
|
<string name="agent_recommend">おすすめ</string>
|
||||||
|
<string name="agent_other">その他</string>
|
||||||
|
<string name="agent_find">AI発見</string>
|
||||||
|
<string name="chat">チャット</string>
|
||||||
|
<string name="agent_chat_room">チャット</string>
|
||||||
|
<string name="agent_recommended_chat_rooms">おすすめチャットルーム</string>
|
||||||
|
<string name="search">検索</string>
|
||||||
|
<string name="agent_recommend_agent">おすすめエージェント</string>
|
||||||
|
|
||||||
|
<!-- Chat related -->
|
||||||
|
<string name="chat_ai">AI</string>
|
||||||
|
<string name="chat_group">グループ</string>
|
||||||
|
<string name="chat_friend">友達</string>
|
||||||
|
<string name="hot_rooms">人気チャットルーム</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -188,7 +188,7 @@
|
|||||||
<string name="group_room_enter_success">成功加入房间</string>
|
<string name="group_room_enter_success">成功加入房间</string>
|
||||||
<string name="group_room_enter_fail">加入房间失败</string>
|
<string name="group_room_enter_fail">加入房间失败</string>
|
||||||
<string name="agent_createing">创建中...</string>
|
<string name="agent_createing">创建中...</string>
|
||||||
<string name="agent_find">发现</string>
|
<string name="agent_find">发现AI</string>
|
||||||
<string name="text_error_password_too_long">密码不能超过 %1$d 个字符</string>
|
<string name="text_error_password_too_long">密码不能超过 %1$d 个字符</string>
|
||||||
|
|
||||||
<!-- Create Bottom Sheet -->
|
<!-- Create Bottom Sheet -->
|
||||||
@@ -218,4 +218,8 @@
|
|||||||
<string name="create_agent_v2_edit_icon_desc">编辑图标</string>
|
<string name="create_agent_v2_edit_icon_desc">编辑图标</string>
|
||||||
<string name="create_agent_v2_select_avatar_desc">选择头像</string>
|
<string name="create_agent_v2_select_avatar_desc">选择头像</string>
|
||||||
|
|
||||||
|
<!-- Agent related strings -->
|
||||||
|
<string name="agent_chat_room">聊天</string>
|
||||||
|
<string name="agent_recommended_chat_rooms">推荐聊天房间</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -123,6 +123,7 @@
|
|||||||
<string name="index_following">Following</string>
|
<string name="index_following">Following</string>
|
||||||
<string name="index_hot">Hot</string>
|
<string name="index_hot">Hot</string>
|
||||||
<string name="index_news">News</string>
|
<string name="index_news">News</string>
|
||||||
|
<string name="index_recommend">Recommend</string>
|
||||||
<string name="main_home">Home</string>
|
<string name="main_home">Home</string>
|
||||||
<string name="main_ai">Agent</string>
|
<string name="main_ai">Agent</string>
|
||||||
<string name="main_message">Message</string>
|
<string name="main_message">Message</string>
|
||||||
@@ -185,7 +186,7 @@
|
|||||||
<string name="group_room_enter_success">成功加入房间</string>
|
<string name="group_room_enter_success">成功加入房间</string>
|
||||||
<string name="group_room_enter_fail">加入房间失败</string>
|
<string name="group_room_enter_fail">加入房间失败</string>
|
||||||
<string name="agent_createing">创建中...</string>
|
<string name="agent_createing">创建中...</string>
|
||||||
<string name="agent_find">发现</string>
|
<string name="agent_find">发现AI</string>
|
||||||
<string name="text_error_password_too_long">Password cannot exceed %1$d characters</string>
|
<string name="text_error_password_too_long">Password cannot exceed %1$d characters</string>
|
||||||
|
|
||||||
<!-- Create Bottom Sheet -->
|
<!-- Create Bottom Sheet -->
|
||||||
@@ -215,4 +216,8 @@
|
|||||||
<string name="create_agent_v2_edit_icon_desc">Edit Icon</string>
|
<string name="create_agent_v2_edit_icon_desc">Edit Icon</string>
|
||||||
<string name="create_agent_v2_select_avatar_desc">Select Avatar</string>
|
<string name="create_agent_v2_select_avatar_desc">Select Avatar</string>
|
||||||
|
|
||||||
|
<!-- Agent related strings -->
|
||||||
|
<string name="agent_chat_room">Chat</string>
|
||||||
|
<string name="agent_recommended_chat_rooms">Recommended Chat Rooms</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user