3 Commits

Author SHA1 Message Date
b06f2d6c4a 动态模块新增推荐Tab,UI优化及调整
- 新增推荐Tab,采用垂直滑动样式,展示推荐动态内容。
- 推荐Tab支持预加载周围图片,提升滑动体验,并增加loading和错误状态指示。
- 优化评论弹窗UI,移除自动聚焦,调整背景色和输入框样式。
- 动态Tab样式调整,使用下划线指示当前选中Tab。
- 调整MomentLoaderExtraArgs,增加trend参数用于推荐动态加载。
- 新增字符串资源 `index_recommend`。
2025-09-16 16:43:42 +08:00
a215d79ce8 Feat: Add News tab and related functionality
- Added a "News" tab to the main index screen.
- Implemented API parameters for fetching news-specific posts: `imageTag`, `search`, `advancedSearch`, `newsFilter`, `onlyNews`, `newsSource`, `newsLanguage`, `newsCategory`, `requireImageCache`.
- Updated `Moment` data class and `MomentEntity` to include news-related fields like `isNews`, `newsTitle`, `newsUrl`, etc.
- Created `News.kt` composable and `NewsViewModel.kt` to display and manage news items.
- Updated `MomentLoader` to include a `newsOnly` parameter for fetching only news items.
- Added Japanese translations for new index tab strings: "Worldwide", "Dynamic", "Following", "Hot", and "News".
- Adjusted tab count and layout based on guest/logged-in user status to accommodate the new "News" tab.
2025-09-16 14:08:50 +08:00
0e0d622864 Refactor: Optimize Agent tab UI and add chat room recommendations
- Implemented lazy loading for Agent list with pagination.
- Added a section for recommended chat rooms.
- Restructured Agent tab UI:
    - Fixed the top search bar.
    - Organized content into scrollable sections for "Today's Picks", "Recommended Chat Rooms", and "Find Agents".
    - Improved Agent card design with theme-aware backgrounds.
- Introduced `ChatRoom` data class and integrated chat room loading logic in `AgentViewModel`.
- Updated `RiderProAPI` to include `random` parameter for fetching random rooms.
- Enhanced Agent card and chat room card click handling with debounce.
2025-09-15 23:30:10 +08:00
19 changed files with 1669 additions and 357 deletions

View File

@@ -4,11 +4,12 @@ object ConstVars {
// api 地址 - 根据构建类型自动选择
// Debug: http://192.168.0.201:8088
// Release: https://rider-pro.aiosman.com/beta_api
val BASE_SERVER = if (BuildConfig.DEBUG) {
"http://47.109.137.67:6363" // Debug环境
} else {
"https://rider-pro.aiosman.com/beta_api" // Release环境
}
// val BASE_SERVER = if (!BuildConfig.DEBUG) {
// "http://47.109.137.67:6363" // Debug环境
// } else {
// "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_NAME = "Moment Like"

View File

@@ -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
)

View File

@@ -6,6 +6,7 @@ import com.aiosman.ravenow.data.AccountLike
import com.aiosman.ravenow.data.AccountNotice
import com.aiosman.ravenow.data.AccountProfile
import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.Category
import com.aiosman.ravenow.data.Comment
import com.aiosman.ravenow.data.DataContainer
import com.aiosman.ravenow.data.ListContainer
@@ -570,19 +571,39 @@ interface RaveNowAPI {
@Body body: RemoveAccountRequestBody
): Response<Unit>
@GET("outside/prompts")
suspend fun getAgent(
@Query("page") page: Int = 1,
@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("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>>>
@GET("outside/my/prompts")
suspend fun getMyAgent(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("withWorkflow") withWorkflow: Int = 1,
@Query("withWorkflow") withWorkflow: String = "1",
): Response<ListContainer<Agent>>
@Multipart
@@ -619,6 +640,7 @@ interface RaveNowAPI {
suspend fun getRooms(@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("isRecommended") isRecommended: Int = 1,
@Query("random") random: Int? = null
): Response<ListContainer<Room>>
@GET("outside/rooms/detail")
@@ -632,5 +654,12 @@ interface RaveNowAPI {
@POST("outside/generate/agent-info")
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>>
}

View 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)
}
}
}

View File

@@ -327,7 +327,8 @@ class MomentLoaderExtraArgs(
val explore: Boolean? = false,
val timelineId: Int? = null,
val authorId : Int? = null,
val newsOnly: Boolean? = false
val newsOnly: Boolean? = false,
val trend: Boolean? = false
)
class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
override suspend fun fetchData(
@@ -341,7 +342,9 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
explore = if (extra.explore == true) "true" else "",
timelineId = extra.timelineId,
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 {
ListContainer(

View File

@@ -40,6 +40,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
@@ -79,6 +80,7 @@ fun CommentModalContent(
onCommentAdded: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val model = viewModel<CommentModalViewModel>(
key = "CommentModalViewModel_$postId",
factory = object : ViewModelProvider.Factory {
@@ -113,7 +115,7 @@ fun CommentModalContent(
onDismissRequest = {
showCommentMenu = false
},
containerColor = Color.White,
containerColor = AppColors.background,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
@@ -142,24 +144,8 @@ fun CommentModalContent(
}
Column(
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(
modifier = Modifier
.fillMaxWidth()
@@ -170,7 +156,8 @@ fun CommentModalContent(
Text(
text = stringResource(id = R.string.comment_count, commentCount),
fontSize = 14.sp,
color = Color(0xff666666)
fontWeight = FontWeight.Bold,
color = AppColors.nonActiveText
)
OrderSelectionComponent {
commentViewModel.order = it
@@ -205,7 +192,7 @@ fun CommentModalContent(
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xfff7f7f7))
.background(AppColors.background)
) {
EditCommentBottomModal(replyComment) {
commentViewModel.viewModelScope.launch {

View File

@@ -59,9 +59,10 @@ fun EditCommentBottomModal(
val focusRequester = remember { FocusRequester() }
val context = LocalContext.current
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
// 移除自动聚焦,避免自动弹出键盘
// LaunchedEffect(Unit) {
// focusRequester.requestFocus()
// }
Column(
modifier = Modifier
@@ -69,20 +70,6 @@ fun EditCommentBottomModal(
.background(AppColors.background)
.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) {
Row(
verticalAlignment = Alignment.CenterVertically
@@ -129,9 +116,9 @@ fun EditCommentBottomModal(
.fillMaxWidth()
.weight(1f)
.clip(RoundedCornerShape(20.dp))
.background(Color.White)
.border(1.dp, Color.Black, RoundedCornerShape(20.dp))
.padding(horizontal = 16.dp, vertical = 16.dp)
.background(AppColors.inputBackground)
.border(1.dp, AppColors.text.copy(alpha = 0.2f), RoundedCornerShape(20.dp))
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Row(
verticalAlignment = Alignment.Top
@@ -146,7 +133,7 @@ fun EditCommentBottomModal(
.weight(1f)
.focusRequester(focusRequester),
textStyle = TextStyle(
color = Color.Black,
color = AppColors.text,
fontWeight = FontWeight.Normal
),
minLines = 1

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -36,6 +37,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -69,13 +71,11 @@ import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.ResourceCleanupManager
import kotlinx.coroutines.launch
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.verticalScroll
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.ui.zIndex
@OptIn( ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Agent() {
val AppColors = LocalAppTheme.current
@@ -96,6 +96,18 @@ fun Agent() {
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) }
@@ -107,49 +119,61 @@ fun Agent() {
}
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(
top = statusBarPaddingValues.calculateTopPadding(),
bottom = navigationBarPaddings,
start = 16.dp,
end = 16.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
Box(
modifier = Modifier.fillMaxSize()
) {
Row(
// 固定顶部搜索条
Box(
modifier = Modifier
.wrapContentHeight()
.height(44.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
.fillMaxWidth()
.background(AppColors.background)
.zIndex(999.0f)
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
) {
androidx.compose.material3.Text(
text = "Rave AI",
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
Row(
modifier = Modifier
.align(Alignment.CenterVertically)
)
.fillMaxWidth()
.fillMaxHeight().padding(top = 32.dp, start = 16.dp, end = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
androidx.compose.material3.Text(
text = "Rave AI",
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
modifier = Modifier
.align(Alignment.CenterVertically)
)
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigate(NavigationRoute.Search.route)
},
colorFilter = ColorFilter.tint(AppColors.text)
)
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigate(NavigationRoute.Search.route)
},
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(
// modifier = Modifier
@@ -229,9 +253,10 @@ fun Agent() {
// )
// }
var selectedTabIndex by remember { mutableStateOf(0) }
// 使用 ViewModel 中的选中状态
val selectedTabIndex = viewModel.selectedCategoryIndex
// 标签页
// 动态标签页
LazyRow(
modifier = Modifier
.fillMaxWidth()
@@ -240,134 +265,129 @@ fun Agent() {
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
item {
CustomTabItem(
text = stringResource(R.string.agent_recommend),
isSelected = selectedTabIndex == 0,
onClick = {
selectedTabIndex = 0
viewModel.categories.forEachIndexed { index, category ->
item {
CustomTabItem(
text = category.getLocalizedName(),
isSelected = selectedTabIndex == index,
onClick = {
viewModel.selectCategory(index)
}
)
}
if (index < viewModel.categories.size - 1) {
item {
TabSpacer()
}
)
}
item {
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) {
0 -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
}
else -> {
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
}
}
// 显示当前选中分类的 Agent 数据
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
}
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
// center the tabs
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
Image(
painter = painterResource(R.mipmap.rider_pro_agent2),
contentDescription = "agent",
modifier = Modifier.size(28.dp),
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.agent_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text
// 推荐聊天房间
ChatRoomsSection(
chatRooms = viewModel.chatRooms,
navController = LocalNavController.current
)
Spacer(modifier = Modifier.height(20.dp))
}
Spacer(modifier = Modifier.height(50.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
val agentItems = viewModel.agentItems.take(15)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(50.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
// center the tabs
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
items(agentItems) { agentItem ->
AgentCardSquare(
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
Image(
painter = painterResource(R.mipmap.rider_pro_agent2),
contentDescription = "agent",
modifier = Modifier.size(28.dp),
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.agent_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text
)
}
Spacer(modifier = Modifier.height(20.dp))
// Agent两列网格布局
AgentGridLayout(
agentItems = viewModel.agentItems,
viewModel = viewModel,
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")
@Composable
fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navController: NavHostController) {
fun AgentCardSquare(
agentItem: AgentItem,
viewModel: AgentViewModel,
navController: NavHostController
) {
val AppColors = LocalAppTheme.current
val cardHeight = 180.dp
val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一
@@ -379,7 +399,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
modifier = Modifier
.fillMaxWidth()
.height(cardHeight)
.background(Color(0xFFE0E0E0), RoundedCornerShape(12.dp)) // 灰色背景
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp)) // 主题背景
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController)
@@ -394,7 +414,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
modifier = Modifier
.offset(y = -avatarSize / 2)
.size(avatarSize)
.background(Color.White, RoundedCornerShape(avatarSize / 2))
.background(AppColors.background, RoundedCornerShape(avatarSize / 2))
.clip(RoundedCornerShape(avatarSize / 2)),
contentAlignment = Alignment.Center
) {
@@ -421,7 +441,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
Column(
modifier = Modifier
.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
) {
androidx.compose.material3.Text(
@@ -443,49 +463,50 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false)
)
}
Spacer(modifier = Modifier.height(8.dp))
// 聊天按钮,位于底部居中
Box(
modifier = Modifier
.width(80.dp)
.height(32.dp)
.background(
color = Color(0X147c7480),
shape = RoundedCornerShape(8.dp)
)
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
viewModel.createSingleChat(agentItem.openId)
viewModel.goToChatAi(
agentItem.openId,
navController = navController
)
}
}) {
lastClickTime = System.currentTimeMillis()
}
},
contentAlignment = Alignment.Center
) {
androidx.compose.material3.Text(
text = stringResource(R.string.chat),
fontSize = 12.sp,
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
// 聊天按钮,固定在底部居中,距离底部有一定边距
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 12.dp) // 距离底部的边距
.width(80.dp)
.height(32.dp)
.background(
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
)
}
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
viewModel.createSingleChat(agentItem.openId)
viewModel.goToChatAi(
agentItem.openId,
navController = navController
)
}
}) {
lastClickTime = System.currentTimeMillis()
}
},
contentAlignment = Alignment.Center
) {
androidx.compose.material3.Text(
text = stringResource(R.string.chat),
fontSize = 12.sp,
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
val AppColors = LocalAppTheme.current
// 每页显示5个agent
@@ -558,7 +579,13 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
}
@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(
modifier = modifier
.fillMaxSize()
@@ -566,7 +593,11 @@ fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int,
) {
// 显示3个agent
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) {
Spacer(modifier = Modifier.height(8.dp))
}
@@ -576,7 +607,7 @@ fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int,
@SuppressLint("SuspiciousIndentation")
@Composable
fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: NavHostController) {
fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: NavHostController) {
val AppColors = LocalAppTheme.current
// 防抖状态
@@ -592,7 +623,7 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
Box(
modifier = Modifier
.size(48.dp)
.background(Color(0xFFF5F5F5), RoundedCornerShape(24.dp))
.background(AppColors.secondaryBackground, RoundedCornerShape(24.dp))
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController)
@@ -656,7 +687,7 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
modifier = Modifier
.size(width = 60.dp, height = 32.dp)
.background(
color = Color(0X147c7480),
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
)
.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
)
}
}
}

View File

@@ -6,6 +6,10 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.RaveNowAPI
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.index.tabs.message.MessageListViewModel.userService
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
data class ChatRoom(
val id: String,
val name: String,
val avatar: String = "",
val banner: String = "",
val memberCount: Int = 0
)
object AgentViewModel: ViewModel() {
private val apiClient: RaveNowAPI = ApiClient.api
@@ -22,32 +36,65 @@ object AgentViewModel: ViewModel() {
var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
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)
private set
var isRefreshing by mutableStateOf(false)
private set
var isLoading by mutableStateOf(false)
private set
private var currentPage = 1
private var hasMoreData = true
init {
loadAgentData()
loadChatRooms()
loadCategories()
}
private fun loadAgentData() {
private fun loadAgentData(categoryIndex: Int = 0) {
viewModelScope.launch {
isLoading = true
errorMessage = null
currentPage = 1
hasMoreData = true
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) {
val agents = response.body()?.data?.list ?: emptyList()
agentItems = agents.map { agent ->
AgentItem.fromAgent(agent)
}
hasMoreData = agents.size >= 20
} else {
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(
openId: String,
) {
@@ -116,9 +286,13 @@ object AgentViewModel: ViewModel() {
*/
fun ResetModel() {
agentItems = emptyList()
categories = emptyList()
selectedCategoryIndex = 0
errorMessage = null
isRefreshing = false
isLoading = false
currentPage = 1
hasMoreData = true
}
}

View File

@@ -18,6 +18,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
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.rememberPagerState
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.hot.HotMomentsList
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.search.SearchViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import kotlinx.coroutines.launch
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.composables.rememberDebouncer
data class TabData(
val text: String,
val index: Int
)
/**
* 动态列表
*/
@@ -67,8 +76,8 @@ fun MomentsList() {
val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
// 游客模式下显示3个tabWorldwide、Hot、News非游客模式显示4个tabWorldwide、Following、Hot、News
val tabCount = if (AppStore.isGuest) 3 else 4
// 游客模式下显示4个tabWorldwide、Hot、News、Recommend,非游客模式显示5个tabWorldwide、Following、Hot、News、Recommend
val tabCount = if (AppStore.isGuest) 4 else 5
var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope()
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
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Start,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.Bottom
) {
val tabDebouncer = rememberDebouncer()
// 新探索标签
Box {
CustomTabItem(
text = stringResource(R.string.index_worldwide),
isSelected = pagerState.currentPage == 0,
items(tabs) { tab ->
UnderlineTabItem(
text = tab.text,
isSelected = pagerState.currentPage == tab.index,
onClick = {
tabDebouncer {
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(
@@ -264,7 +205,7 @@ fun MomentsList() {
.weight(1f)
) {
if (AppStore.isGuest) {
// 游客模式Worldwide(0), Hot(1), News(2)
// 游客模式Worldwide(0), Hot(1), News(2), Recommend(3)
when (it) {
0 -> {
Dynamic()
@@ -275,9 +216,12 @@ fun MomentsList() {
2 -> {
News()
}
3 -> {
Recommend()
}
}
} else {
// 正常用户Worldwide(0), Following(1), Hot(2), News(3)
// 正常用户Worldwide(0), Following(1), Hot(2), News(3), Recommend(4)
when (it) {
0 -> {
Dynamic()
@@ -291,11 +235,49 @@ fun MomentsList() {
3 -> {
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
fun CustomTabItem(
text: String,

View File

@@ -70,7 +70,7 @@ class ExploreViewModel : ViewModel() {
isRefreshing = true
errorMessage = null
try {
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = "1")
if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList()
agentItems = agents.map { agent ->
@@ -114,7 +114,7 @@ class ExploreViewModel : ViewModel() {
isLoading = true
errorMessage = null
try {
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = "1")
if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList()
agentItems = agents.map { agent ->

View File

@@ -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
)
}
}
)
}
}

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -63,6 +63,23 @@ object Utils {
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 {
val inputStream = context.contentResolver.openInputStream(uri)
val originalBitmap = BitmapFactory.decodeStream(inputStream)

View File

@@ -33,4 +33,28 @@
<string name="index_following">フォロー中</string>
<string name="index_hot">ホット</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>

View File

@@ -188,7 +188,7 @@
<string name="group_room_enter_success">成功加入房间</string>
<string name="group_room_enter_fail">加入房间失败</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>
<!-- Create Bottom Sheet -->
@@ -218,4 +218,8 @@
<string name="create_agent_v2_edit_icon_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>

View File

@@ -123,6 +123,7 @@
<string name="index_following">Following</string>
<string name="index_hot">Hot</string>
<string name="index_news">News</string>
<string name="index_recommend">Recommend</string>
<string name="main_home">Home</string>
<string name="main_ai">Agent</string>
<string name="main_message">Message</string>
@@ -185,7 +186,7 @@
<string name="group_room_enter_success">成功加入房间</string>
<string name="group_room_enter_fail">加入房间失败</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>
<!-- Create Bottom Sheet -->
@@ -215,4 +216,8 @@
<string name="create_agent_v2_edit_icon_desc">Edit Icon</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>