Merge remote-tracking branch 'origin/main' into nagisa
@@ -32,6 +32,7 @@ import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel
|
||||
import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
|
||||
import com.aiosman.ravenow.utils.Utils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import com.aiosman.ravenow.im.OpenIMManager
|
||||
import io.openim.android.sdk.OpenIMClient
|
||||
@@ -51,7 +52,7 @@ object AppState {
|
||||
suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
|
||||
// 如果是游客模式,使用简化的初始化流程
|
||||
if (AppStore.isGuest) {
|
||||
initWithGuestAccount()
|
||||
initWithGuestAccount(scope)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,18 +91,50 @@ object AppState {
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppState", "刷新积分失败: ${e.message}")
|
||||
}
|
||||
|
||||
// 并行加载积分规则和房间规则配置(不阻塞主流程)
|
||||
scope.launch {
|
||||
try {
|
||||
PointService.refreshPointsRules()
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppState", "加载积分规则失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
scope.launch {
|
||||
try {
|
||||
PointService.refreshRoomMaxMembers()
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppState", "加载房间规则失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 游客模式的简化初始化
|
||||
*/
|
||||
private fun initWithGuestAccount() {
|
||||
private fun initWithGuestAccount(scope: CoroutineScope) {
|
||||
// 游客模式下,不初始化推送和TRTC
|
||||
// 设置默认的用户信息
|
||||
UserId = 0
|
||||
profile = null
|
||||
enableChat = false
|
||||
Log.d("AppState", "Guest mode initialized without push notifications and TRTC")
|
||||
|
||||
// 游客模式下也加载规则配置(用于查看费用信息)
|
||||
scope.launch {
|
||||
try {
|
||||
PointService.refreshPointsRules()
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppState", "加载积分规则失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
scope.launch {
|
||||
try {
|
||||
PointService.refreshRoomMaxMembers()
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppState", "加载房间规则失败: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initChat(context: Context){
|
||||
|
||||
@@ -69,6 +69,10 @@ data class AccountProfile(
|
||||
val aiRoleAvatar: String? = null,
|
||||
val aiRoleAvatarMedium: String? = null,
|
||||
val aiRoleAvatarLarge: String? = null,
|
||||
|
||||
// 创建者信息(仅AI账号有)
|
||||
@SerializedName("creatorProfile")
|
||||
val creatorProfile: com.aiosman.ravenow.data.CreatorProfile? = null,
|
||||
) {
|
||||
/**
|
||||
* 转换为Entity
|
||||
@@ -103,7 +107,8 @@ data class AccountProfile(
|
||||
},
|
||||
aiRoleAvatarLarge = aiRoleAvatarLarge?.let {
|
||||
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
||||
}
|
||||
},
|
||||
creatorProfile = creatorProfile?.toCreatorProfileEntity()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,16 @@ interface DictService {
|
||||
* 获取字典列表
|
||||
*/
|
||||
suspend fun getDistList(keys: List<String>): List<DictItem>
|
||||
|
||||
/**
|
||||
* 获取外部字典项
|
||||
*/
|
||||
suspend fun getOutsideDictByKey(key: String): DictItem
|
||||
|
||||
/**
|
||||
* 获取外部字典列表
|
||||
*/
|
||||
suspend fun getOutsideDistList(keys: List<String>): List<DictItem>
|
||||
}
|
||||
|
||||
class DictServiceImpl : DictService {
|
||||
@@ -26,4 +36,13 @@ class DictServiceImpl : DictService {
|
||||
val resp = ApiClient.api.getDicts(keys.joinToString(","))
|
||||
return resp.body()?.list ?: throw Exception("failed to get dict list")
|
||||
}
|
||||
override suspend fun getOutsideDictByKey(key: String): DictItem {
|
||||
val resp = ApiClient.api.getOutsideDict(key)
|
||||
return resp.body()?.data ?: throw Exception("failed to get outside dict")
|
||||
}
|
||||
|
||||
override suspend fun getOutsideDistList(keys: List<String>): List<DictItem> {
|
||||
val resp = ApiClient.api.getOutsideDicts(keys.joinToString(","))
|
||||
return resp.body()?.list ?: throw Exception("failed to get outside dict list")
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,36 @@ object PointService {
|
||||
private val dictService: DictService = DictServiceImpl()
|
||||
private val gson = Gson()
|
||||
|
||||
/**
|
||||
* 积分规则key常量
|
||||
* 对应积分规则JSON中的key值
|
||||
*/
|
||||
object PointsRuleKey {
|
||||
// 获得积分类型(add)
|
||||
/** 每日登录奖励 */
|
||||
const val DAILY_LOGIN = "daily_login"
|
||||
/** 用户注册奖励 */
|
||||
const val USER_REGISTER = "user_register"
|
||||
|
||||
// 消费积分类型(sub)
|
||||
/** 添加Agent记忆 */
|
||||
const val ADD_AGENT_MEMORY = "add_agent_memory"
|
||||
/** 增加房间容量 */
|
||||
const val ADD_ROOM_CAP = "add_room_cap"
|
||||
/** 创建房间 */
|
||||
const val CREATE_ROOM = "create_room"
|
||||
/** 创建定时事件 */
|
||||
const val CREATE_SCHEDULE_EVENT = "create_schedule_event"
|
||||
/** 房间私密模式 */
|
||||
const val ROOM_PRIVATE = "room_private"
|
||||
/** Agent私密模式 */
|
||||
const val SPEND_AGENT_PRIVATE = "spend_agent_private"
|
||||
/** 自定义聊天背景 */
|
||||
const val SPEND_CHAT_BACKGROUND = "spend_chat_background"
|
||||
/** 房间记忆添加 */
|
||||
const val SPEND_ROOM_MEMORY = "spend_room_memory"
|
||||
}
|
||||
|
||||
sealed class RuleAmount {
|
||||
data class Fixed(val value: Int) : RuleAmount()
|
||||
data class Range(val min: Int, val max: Int) : RuleAmount()
|
||||
@@ -50,6 +80,64 @@ object PointService {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 群聊人数限制(字典 points-rule)相关 ==========
|
||||
/**
|
||||
* 群聊人数限制配置
|
||||
* @param defaultMaxTotal 初始最大人数(默认值)
|
||||
* @param maxTotal 最大人数(上限)
|
||||
*/
|
||||
data class RoomMaxMembers(
|
||||
val defaultMaxTotal: Int,
|
||||
val maxTotal: Int
|
||||
)
|
||||
|
||||
private val _roomMaxMembers = MutableStateFlow<RoomMaxMembers?>(null)
|
||||
val roomMaxMembers: StateFlow<RoomMaxMembers?> = _roomMaxMembers.asStateFlow()
|
||||
|
||||
/**
|
||||
* 刷新群聊人数限制配置(从外部字典表加载 points-rule)
|
||||
* 加载时机与 refreshPointsRules 一致
|
||||
*/
|
||||
suspend fun refreshRoomMaxMembers(key: String = "points-rule") {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val dict = dictService.getOutsideDictByKey(key)
|
||||
val config = parseRoomMaxMembers(dict)
|
||||
_roomMaxMembers.value = config
|
||||
} catch (_: Exception) {
|
||||
_roomMaxMembers.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析群聊人数限制配置
|
||||
* 解析格式:{"room":{"default":{"max-total":5},"max":{"max-total":200}}}
|
||||
*/
|
||||
private fun parseRoomMaxMembers(dict: DictItem): RoomMaxMembers? {
|
||||
val raw = dict.value
|
||||
val jsonStr = when (raw) {
|
||||
is String -> raw
|
||||
else -> gson.toJson(raw)
|
||||
}
|
||||
return try {
|
||||
val root = JsonParser.parseString(jsonStr).asJsonObject
|
||||
val roomObj = root.getAsJsonObject("room")
|
||||
val defaultObj = roomObj?.getAsJsonObject("default")
|
||||
val maxObj = roomObj?.getAsJsonObject("max")
|
||||
|
||||
val defaultMaxTotal = defaultObj?.get("max-total")?.takeIf { it.isJsonPrimitive }?.asInt ?: 5
|
||||
val maxTotal = maxObj?.get("max-total")?.takeIf { it.isJsonPrimitive }?.asInt ?: 200
|
||||
|
||||
RoomMaxMembers(
|
||||
defaultMaxTotal = defaultMaxTotal,
|
||||
maxTotal = maxTotal
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parsePointsRules(dict: DictItem): PointsRules? {
|
||||
val raw = dict.value
|
||||
val jsonStr = when (raw) {
|
||||
|
||||
@@ -331,3 +331,21 @@ class RecommendationServiceImpl : RecommendationService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CreatorProfile 扩展函数,转换为 CreatorProfileEntity
|
||||
*/
|
||||
fun CreatorProfile.toCreatorProfileEntity(): com.aiosman.ravenow.entity.CreatorProfileEntity {
|
||||
return com.aiosman.ravenow.entity.CreatorProfileEntity(
|
||||
id = id,
|
||||
username = username,
|
||||
nickname = nickname,
|
||||
avatar = avatar?.let {
|
||||
if (it.isNotEmpty()) "${ApiClient.BASE_SERVER}$it" else null
|
||||
},
|
||||
bio = bio,
|
||||
trtcUserId = trtcUserId,
|
||||
chatAIId = chatAIId,
|
||||
aiAccount = aiAccount
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1335,6 +1335,16 @@ interface RaveNowAPI {
|
||||
@Query("keys") keys: String
|
||||
): Response<ListContainer<DictItem>>
|
||||
|
||||
@GET("/outside/dict")
|
||||
suspend fun getOutsideDict(
|
||||
@Query("key") key: String
|
||||
): Response<DataContainer<DictItem>>
|
||||
|
||||
@GET("/outside/dicts")
|
||||
suspend fun getOutsideDicts(
|
||||
@Query("keys") keys: String
|
||||
): Response<ListContainer<DictItem>>
|
||||
|
||||
@POST("captcha/generate")
|
||||
suspend fun generateCaptcha(
|
||||
@Body body: CaptchaRequestBody
|
||||
@@ -1500,6 +1510,35 @@ interface RaveNowAPI {
|
||||
@Query("pageSize") pageSize: Int? = null
|
||||
): Response<ListContainer<Agent>>
|
||||
|
||||
/**
|
||||
* 获取Prompt详情(支持ID或OpenId)
|
||||
* @param promptId Prompt ID或OpenId(UUID格式)
|
||||
*/
|
||||
@GET("outside/prompt/{promptId}")
|
||||
suspend fun getPromptDetail(
|
||||
@Path("promptId") promptId: String
|
||||
): Response<DataContainer<Agent>>
|
||||
|
||||
/**
|
||||
* 更新Prompt(支持ID或OpenId)
|
||||
* @param promptId Prompt ID或OpenId(UUID格式)
|
||||
* @param avatar 头像文件(可选)
|
||||
* @param title 标题(可选)
|
||||
* @param desc 描述(可选)
|
||||
* @param value 内容(可选)
|
||||
* @param isPublic 是否公开(可选)
|
||||
*/
|
||||
@Multipart
|
||||
@PATCH("outside/prompt/{promptId}")
|
||||
suspend fun updatePrompt(
|
||||
@Path("promptId") promptId: String,
|
||||
@Part avatar: MultipartBody.Part?,
|
||||
@Part("title") title: RequestBody?,
|
||||
@Part("desc") desc: RequestBody?,
|
||||
@Part("value") value: RequestBody?,
|
||||
@Part("public") isPublic: RequestBody?,
|
||||
): Response<DataContainer<Agent>>
|
||||
|
||||
// ========== Agent Rule API ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -72,6 +72,9 @@ data class AccountProfileEntity(
|
||||
val aiRoleAvatar: String? = null,
|
||||
val aiRoleAvatarMedium: String? = null,
|
||||
val aiRoleAvatarLarge: String? = null,
|
||||
|
||||
// 创建者信息(仅AI账号有)
|
||||
val creatorProfile: CreatorProfileEntity? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -115,6 +118,28 @@ data class NoticeUserEntity(
|
||||
val avatar: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建者信息
|
||||
*/
|
||||
data class CreatorProfileEntity(
|
||||
// 用户ID
|
||||
val id: Long,
|
||||
// 用户名
|
||||
val username: String? = null,
|
||||
// 昵称
|
||||
val nickname: String,
|
||||
// 头像
|
||||
val avatar: String? = null,
|
||||
// 个人简介
|
||||
val bio: String? = null,
|
||||
// trtcUserId
|
||||
val trtcUserId: String? = null,
|
||||
// chatAIId
|
||||
val chatAIId: String? = null,
|
||||
// 是否为AI账号
|
||||
val aiAccount: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* 用户点赞消息分页数据加载器
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.aiosman.ravenow.entity
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.aiosman.ravenow.data.ListContainer
|
||||
import com.aiosman.ravenow.data.Room
|
||||
import com.aiosman.ravenow.data.ServiceException
|
||||
import com.aiosman.ravenow.data.api.ApiClient
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* 群聊房间
|
||||
@@ -253,3 +257,58 @@ class RoomLoader : DataLoader<AgentEntity,AgentLoaderExtraArgs>() {
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间远程数据源
|
||||
*/
|
||||
class RoomRemoteDataSource {
|
||||
suspend fun searchRooms(
|
||||
pageNumber: Int,
|
||||
pageSize: Int = 20,
|
||||
search: String
|
||||
): ListContainer<RoomEntity>? {
|
||||
val resp = ApiClient.api.getRooms(
|
||||
page = pageNumber,
|
||||
pageSize = pageSize,
|
||||
search = search,
|
||||
roomType = "public" // 搜索时只显示公有房间
|
||||
)
|
||||
val body = resp.body() ?: return null
|
||||
return ListContainer(
|
||||
total = body.total,
|
||||
page = pageNumber,
|
||||
pageSize = pageSize,
|
||||
list = body.list.map { it.toRoomtEntity() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间搜索分页加载器
|
||||
*/
|
||||
class RoomSearchPagingSource(
|
||||
private val roomRemoteDataSource: RoomRemoteDataSource,
|
||||
private val keyword: String,
|
||||
) : PagingSource<Int, RoomEntity>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RoomEntity> {
|
||||
return try {
|
||||
val currentPage = params.key ?: 1
|
||||
val rooms = roomRemoteDataSource.searchRooms(
|
||||
pageNumber = currentPage,
|
||||
pageSize = params.loadSize,
|
||||
search = keyword
|
||||
)
|
||||
LoadResult.Page(
|
||||
data = rooms?.list ?: listOf(),
|
||||
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||
nextKey = if (rooms?.list?.isNotEmpty() == true) currentPage + 1 else null
|
||||
)
|
||||
} catch (exception: IOException) {
|
||||
LoadResult.Error(exception)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? {
|
||||
return state.anchorPosition
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import com.aiosman.ravenow.ui.account.ResetPasswordScreen
|
||||
import com.aiosman.ravenow.ui.account.ZodiacSelectScreen
|
||||
import com.aiosman.ravenow.ui.agent.AddAgentScreen
|
||||
import com.aiosman.ravenow.ui.agent.AgentImageCropScreen
|
||||
import com.aiosman.ravenow.ui.agent.AiPromptEditScreen
|
||||
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
|
||||
import com.aiosman.ravenow.ui.chat.ChatAiScreen
|
||||
import com.aiosman.ravenow.ui.chat.ChatSettingScreen
|
||||
@@ -133,6 +134,7 @@ sealed class NavigationRoute(
|
||||
data object MbtiSelect : NavigationRoute("MbtiSelect")
|
||||
data object ZodiacSelect : NavigationRoute("ZodiacSelect")
|
||||
data object ScanQr : NavigationRoute("ScanQr")
|
||||
data object AiPromptEdit : NavigationRoute("AiPromptEdit/{chatAIId}")
|
||||
}
|
||||
|
||||
|
||||
@@ -684,6 +686,18 @@ fun NavigationController(
|
||||
}
|
||||
}
|
||||
|
||||
composable(
|
||||
route = NavigationRoute.AiPromptEdit.route,
|
||||
arguments = listOf(navArgument("chatAIId") { type = NavType.StringType })
|
||||
) {
|
||||
val chatAIId = it.arguments?.getString("chatAIId") ?: ""
|
||||
CompositionLocalProvider(
|
||||
LocalAnimatedContentScope provides this,
|
||||
) {
|
||||
AiPromptEditScreen(chatAIId = chatAIId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,12 +20,14 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.ConstVars
|
||||
import com.aiosman.ravenow.data.api.ErrorCode
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
@@ -99,10 +101,11 @@ fun ResetPasswordScreen() {
|
||||
if (e.code == ErrorCode.USER_NOT_EXIST.code){
|
||||
usernameError = context.getString(R.string.error_40002_user_not_exist)
|
||||
} else {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
// 其他错误,不显示Toast
|
||||
isSendSuccess = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
// 异常错误,不显示Toast
|
||||
isSendSuccess = false
|
||||
} finally {
|
||||
isLoading = false
|
||||
@@ -133,12 +136,21 @@ fun ResetPasswordScreen() {
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// 暗色模式下的 hint 文本颜色
|
||||
val isDarkMode = AppState.darkMode
|
||||
val hintColor = if (isDarkMode) {
|
||||
Color(0xFFFFFFFF).copy(alpha = 0.7f)
|
||||
} else {
|
||||
null // 使用默认颜色
|
||||
}
|
||||
|
||||
TextInputField(
|
||||
text = username,
|
||||
onValueChange = { username = it },
|
||||
hint = stringResource(R.string.text_hint_email),
|
||||
enabled = !isLoading && countDown == null,
|
||||
error = usernameError,
|
||||
customHintColor = hintColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Box(
|
||||
@@ -178,9 +190,11 @@ fun ResetPasswordScreen() {
|
||||
} else {
|
||||
stringResource(R.string.recover)
|
||||
},
|
||||
backgroundColor = appColors.main,
|
||||
backgroundColor = Color(0xFF7C45ED), // 紫色背景
|
||||
loadingBackgroundColor = Color(0xFF7C45ED), // loading 时保持紫色
|
||||
disabledBackgroundColor = Color(0xFF7C45ED), // disabled 时保持紫色
|
||||
color = appColors.mainText,
|
||||
isLoading = isLoading,
|
||||
isLoading = isLoading && countDown == null, // 只在未发送成功时显示loading
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
enabled = countDown == null,
|
||||
) {
|
||||
@@ -193,6 +207,8 @@ fun ResetPasswordScreen() {
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
text = stringResource(R.string.back_upper),
|
||||
backgroundColor = Color(0xFF7C45ED), // 紫色背景
|
||||
color = Color.White, // 白色文字
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
) {
|
||||
navController.navigateUp()
|
||||
|
||||
@@ -59,6 +59,7 @@ import java.io.InputStream
|
||||
|
||||
/**
|
||||
* 专门用于智能体头像裁剪的页面
|
||||
* 支持创建和编辑两种模式
|
||||
*/
|
||||
@Composable
|
||||
fun AgentImageCropScreen() {
|
||||
@@ -71,6 +72,14 @@ fun AgentImageCropScreen() {
|
||||
val density = LocalDensity.current
|
||||
val navController = LocalNavController.current
|
||||
|
||||
// 检查是否在编辑模式(通过检查是否有编辑ViewModel的实例)
|
||||
val isEditMode = remember {
|
||||
// 通过检查导航栈或使用其他方式判断
|
||||
// 暂时使用一个简单的方法:检查AddAgentViewModel是否正在选择头像
|
||||
// 如果不是,则可能是编辑模式
|
||||
!AddAgentViewModel.isSelectingAvatar
|
||||
}
|
||||
|
||||
val imagePickLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
@@ -84,7 +93,9 @@ fun AgentImageCropScreen() {
|
||||
}
|
||||
if (uri == null) {
|
||||
// 用户取消选择图片,重置标志
|
||||
if (!isEditMode) {
|
||||
AddAgentViewModel.isSelectingAvatar = false
|
||||
}
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
@@ -122,7 +133,9 @@ fun AgentImageCropScreen() {
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clickable {
|
||||
// 用户取消头像选择,重置标志
|
||||
if (!isEditMode) {
|
||||
AddAgentViewModel.isSelectingAvatar = false
|
||||
}
|
||||
navController.popBackStack()
|
||||
},
|
||||
colorFilter = ColorFilter.tint(Color.White)
|
||||
@@ -137,6 +150,12 @@ fun AgentImageCropScreen() {
|
||||
modifier = Modifier.clickable {
|
||||
if (croppedBitmap != null) {
|
||||
// 如果已经有裁剪结果,直接返回
|
||||
if (isEditMode) {
|
||||
// 编辑模式:需要找到当前的编辑ViewModel实例
|
||||
// 由于无法直接访问,我们使用一个全局状态或者通过其他方式传递
|
||||
// 暂时先保存到AddAgentViewModel,编辑页面会检查
|
||||
AddAgentViewModel.croppedBitmap = croppedBitmap
|
||||
} else {
|
||||
AddAgentViewModel.croppedBitmap = croppedBitmap
|
||||
// 重置头像选择标志
|
||||
AddAgentViewModel.isSelectingAvatar = false
|
||||
@@ -144,6 +163,8 @@ fun AgentImageCropScreen() {
|
||||
AddAgentViewModel.updateAgentAvatar(context)
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
navController.popBackStack()
|
||||
} else {
|
||||
// 进行裁剪
|
||||
imageCrop?.let {
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
package com.aiosman.ravenow.ui.agent
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.PointsPaymentDialog
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.composables.form.FormTextInput
|
||||
import com.aiosman.ravenow.ui.composables.form.FormTextInput2
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* AI Prompt 编辑页面
|
||||
*/
|
||||
@Composable
|
||||
fun AiPromptEditScreen(
|
||||
chatAIId: String,
|
||||
viewModel: AiPromptEditViewModel = viewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val appColors = LocalAppTheme.current
|
||||
|
||||
// 加载Prompt详情
|
||||
LaunchedEffect(chatAIId) {
|
||||
viewModel.loadPromptDetail(chatAIId)
|
||||
}
|
||||
|
||||
// 监听头像裁剪结果(从AgentImageCropScreen返回)
|
||||
LaunchedEffect(viewModel.isSelectingAvatar) {
|
||||
if (!viewModel.isSelectingAvatar && AddAgentViewModel.croppedBitmap != null) {
|
||||
// 从裁剪页面返回,检查是否有新的裁剪结果
|
||||
viewModel.croppedBitmap = AddAgentViewModel.croppedBitmap
|
||||
// 清空AddAgentViewModel的裁剪结果,避免影响创建页面
|
||||
AddAgentViewModel.croppedBitmap = null
|
||||
}
|
||||
}
|
||||
|
||||
// 状态
|
||||
var showPrivacyConfirmDialog by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// 获取积分规则和余额
|
||||
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
|
||||
val pointsBalance by PointService.pointsBalance.collectAsState(initial = null)
|
||||
|
||||
// 计算是否需要付费
|
||||
val needsPayment = viewModel.needsPrivacyPayment()
|
||||
val privacyCost = viewModel.getPrivacyCost()
|
||||
val currentBalance = viewModel.getCurrentBalance()
|
||||
val balanceAfterCost = viewModel.calculateBalanceAfterCost(privacyCost)
|
||||
val isBalanceSufficient = viewModel.isBalanceSufficient(privacyCost)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = Color(0xFFFAFAFB)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
|
||||
// 顶部导航栏
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = Color(0xFFFAFAFB))
|
||||
.padding(horizontal = 14.dp, vertical = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_back_icon),
|
||||
contentDescription = "返回",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
navController.navigateUp()
|
||||
},
|
||||
colorFilter = ColorFilter.tint(appColors.text)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
Text(
|
||||
"编辑Ai",
|
||||
fontWeight = FontWeight.W600,
|
||||
modifier = Modifier.weight(1f),
|
||||
fontSize = 17.sp,
|
||||
color = appColors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(1.dp))
|
||||
|
||||
// 内容区域
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.background(Color(0xFFFAFAFB))
|
||||
) {
|
||||
// 头像选择
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 18.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.avatar),
|
||||
fontSize = 12.sp,
|
||||
color = appColors.text,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(72.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0x777c45ed),
|
||||
Color(0x777c68ef),
|
||||
Color(0x557bd8f8)
|
||||
)
|
||||
)
|
||||
)
|
||||
.noRippleClickable {
|
||||
viewModel.isSelectingAvatar = true
|
||||
// 标记为编辑模式
|
||||
AddAgentViewModel.isSelectingAvatar = false
|
||||
navController.navigate(NavigationRoute.AgentImageCrop.route)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when {
|
||||
viewModel.croppedBitmap != null -> {
|
||||
Image(
|
||||
bitmap = viewModel.croppedBitmap!!.asImageBitmap(),
|
||||
contentDescription = "Avatar",
|
||||
modifier = Modifier
|
||||
.size(72.dp)
|
||||
.clip(CircleShape),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
viewModel.avatarUrl != null -> {
|
||||
CustomAsyncImage(
|
||||
context = context,
|
||||
imageUrl = viewModel.avatarUrl!!,
|
||||
contentDescription = "Avatar",
|
||||
modifier = Modifier
|
||||
.size(72.dp)
|
||||
.clip(CircleShape),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.icons_infor_edit),
|
||||
contentDescription = "Edit",
|
||||
colorFilter = ColorFilter.tint(Color.White),
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
// 名称输入
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.agent_name),
|
||||
fontSize = 12.sp,
|
||||
color = appColors.text,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
FormTextInput(
|
||||
value = viewModel.title,
|
||||
hint = stringResource(R.string.agent_name_hint_1),
|
||||
background = Color.White,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { value ->
|
||||
viewModel.title = value
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
// 描述输入
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.agent_desc),
|
||||
fontSize = 12.sp,
|
||||
color = appColors.text,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
FormTextInput2(
|
||||
value = viewModel.desc,
|
||||
hint = stringResource(R.string.agent_desc_hint),
|
||||
background = Color.White,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { value ->
|
||||
viewModel.desc = value
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
// 设定权限区域
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "设定权限",
|
||||
fontSize = 12.sp,
|
||||
color = appColors.text,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 公开/私有切换
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(25.dp))
|
||||
.background(Color.White)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f),
|
||||
shape = RoundedCornerShape(25.dp)
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = if (viewModel.isPublic) "公开" else "私有",
|
||||
fontSize = 14.sp,
|
||||
color = appColors.text,
|
||||
fontWeight = FontWeight.W500
|
||||
)
|
||||
Switch(
|
||||
checked = viewModel.isPublic,
|
||||
onCheckedChange = { checked ->
|
||||
if (!checked && needsPayment && !viewModel.paidForPrivacyEdit) {
|
||||
// 需要付费,显示确认对话框
|
||||
showPrivacyConfirmDialog = true
|
||||
} else {
|
||||
viewModel.isPublic = checked
|
||||
}
|
||||
},
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color.White,
|
||||
checkedTrackColor = appColors.brandColorsColor,
|
||||
uncheckedThumbColor = Color.White,
|
||||
uncheckedTrackColor = appColors.brandColorsColor.copy(alpha = 0.5f),
|
||||
uncheckedBorderColor = Color.Transparent
|
||||
),
|
||||
modifier = Modifier.size(width = 64.dp, height = 28.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// 首次解锁AI权限提示
|
||||
if (needsPayment && !viewModel.paidForPrivacyEdit && privacyCost > 0) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
// 主要内容容器(去掉阴影)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(
|
||||
color = Color(red = 251f / 255f, green = 248f / 255f, blue = 239f / 255f)
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color(red = 243f / 255f, green = 234f / 255f, blue = 206f / 255f),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 锁图标容器
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.background(
|
||||
color = Color(red = 1f, green = 204f / 255f, blue = 0f, alpha = 0.12f),
|
||||
shape = RoundedCornerShape(10.7.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// 锁图标(使用文本代替,实际项目中可以使用图片资源)
|
||||
Text(
|
||||
text = "🔒",
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = "首次解锁Ai权限",
|
||||
fontSize = 13.sp,
|
||||
color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f),
|
||||
fontWeight = FontWeight.W500
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "将消耗",
|
||||
fontSize = 12.sp,
|
||||
color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f)
|
||||
)
|
||||
Text(
|
||||
text = "$privacyCost",
|
||||
fontSize = 12.sp,
|
||||
color = Color(red = 1f, green = 141f / 255f, blue = 40f / 255f)
|
||||
)
|
||||
// 小硬币图标
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFFFD700),
|
||||
Color(0xFFFFA500)
|
||||
)
|
||||
),
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "解锁后可随时切换",
|
||||
fontSize = 12.sp,
|
||||
color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部保存按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = Color(0xFFFAFAFB))
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
) {
|
||||
ActionButton(
|
||||
text = "保存",
|
||||
enabled = !viewModel.isUpdating && !viewModel.isLoading,
|
||||
isLoading = viewModel.isUpdating,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// 验证输入
|
||||
val validationError = viewModel.validate()
|
||||
if (validationError != null) {
|
||||
errorMessage = validationError
|
||||
return@ActionButton
|
||||
}
|
||||
|
||||
// 检查是否需要付费确认
|
||||
if (needsPayment && !viewModel.paidForPrivacyEdit) {
|
||||
showPrivacyConfirmDialog = true
|
||||
return@ActionButton
|
||||
}
|
||||
|
||||
// 执行保存
|
||||
scope.launch {
|
||||
try {
|
||||
viewModel.updatePrompt(context)
|
||||
navController.navigateUp()
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message ?: "保存失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 隐私权限付费确认对话框
|
||||
if (showPrivacyConfirmDialog) {
|
||||
PointsPaymentDialog(
|
||||
cost = privacyCost,
|
||||
currentBalance = currentBalance,
|
||||
balanceAfterCost = balanceAfterCost,
|
||||
isBalanceSufficient = isBalanceSufficient,
|
||||
onConfirm = {
|
||||
showPrivacyConfirmDialog = false
|
||||
scope.launch {
|
||||
try {
|
||||
viewModel.isPublic = false
|
||||
viewModel.updatePrompt(context)
|
||||
viewModel.paidForPrivacyEdit = true
|
||||
navController.navigateUp()
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message ?: "保存失败"
|
||||
}
|
||||
}
|
||||
},
|
||||
onCancel = {
|
||||
showPrivacyConfirmDialog = false
|
||||
},
|
||||
title = "首次解锁AI权限",
|
||||
description = "将消耗 $privacyCost 派币解锁后可随时切换"
|
||||
)
|
||||
}
|
||||
|
||||
// 错误提示
|
||||
errorMessage?.let { error ->
|
||||
LaunchedEffect(error) {
|
||||
kotlinx.coroutines.delay(3000)
|
||||
errorMessage = null
|
||||
}
|
||||
// TODO: 显示Toast或Snackbar
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.aiosman.ravenow.ui.agent
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aiosman.ravenow.data.UploadImage
|
||||
import com.aiosman.ravenow.data.ServiceException
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import com.aiosman.ravenow.data.api.ApiClient
|
||||
import com.aiosman.ravenow.entity.AgentEntity
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.File
|
||||
|
||||
class AiPromptEditViewModel : ViewModel() {
|
||||
var chatAIId by mutableStateOf("")
|
||||
var title by mutableStateOf("")
|
||||
var desc by mutableStateOf("")
|
||||
var isPublic by mutableStateOf(true)
|
||||
var originalIsPublic by mutableStateOf(true)
|
||||
var paidForPrivacyEdit by mutableStateOf(false)
|
||||
var avatarUrl by mutableStateOf<String?>(null)
|
||||
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
||||
var isUpdating by mutableStateOf(false)
|
||||
var isLoading by mutableStateOf(false)
|
||||
var errorMessage by mutableStateOf<String?>(null)
|
||||
var isSelectingAvatar by mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* 加载Prompt详情
|
||||
*/
|
||||
fun loadPromptDetail(chatAIId: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
this@AiPromptEditViewModel.chatAIId = chatAIId
|
||||
|
||||
val response = ApiClient.api.getPromptDetail(chatAIId)
|
||||
val body = response.body()?.data ?: throw ServiceException("Failed to get prompt detail")
|
||||
|
||||
// 填充数据
|
||||
title = body.title
|
||||
desc = body.desc
|
||||
isPublic = body.isPublic
|
||||
originalIsPublic = body.isPublic
|
||||
avatarUrl = "${ApiClient.BASE_API_URL}/outside${body.avatar}?token=${com.aiosman.ravenow.AppStore.token}"
|
||||
|
||||
// 注意:Agent数据模型可能没有paidForPrivacyEdit字段,需要从其他地方获取
|
||||
// 暂时设为false,后续可以根据实际API响应调整
|
||||
paidForPrivacyEdit = false
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("AiPromptEditViewModel", "Error loading prompt detail", e)
|
||||
errorMessage = "加载失败: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Prompt
|
||||
*/
|
||||
suspend fun updatePrompt(context: Context): AgentEntity? {
|
||||
try {
|
||||
isUpdating = true
|
||||
errorMessage = null
|
||||
|
||||
// 准备头像文件
|
||||
val avatarFile = if (croppedBitmap != null) {
|
||||
val file = File(context.cacheDir, "agent_avatar_edit.jpg")
|
||||
croppedBitmap!!.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
||||
UploadImage(file, "agent_avatar_edit.jpg", "", "jpg")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// 准备请求参数
|
||||
val textTitle = title.trim().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
val textDesc = desc.trim().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
val textValue = desc.trim().toRequestBody("text/plain".toMediaTypeOrNull()) // value通常和desc相同
|
||||
val isPublicBody = isPublic.toString().toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
|
||||
val avatarPart: MultipartBody.Part? = avatarFile?.let {
|
||||
val requestFile = it.file.asRequestBody("image/*".toMediaTypeOrNull())
|
||||
MultipartBody.Part.createFormData("avatar", it.filename, requestFile)
|
||||
}
|
||||
|
||||
// 调用更新API
|
||||
val response = ApiClient.api.updatePrompt(
|
||||
promptId = chatAIId,
|
||||
avatar = avatarPart,
|
||||
title = textTitle,
|
||||
desc = textDesc,
|
||||
value = textValue,
|
||||
isPublic = isPublicBody
|
||||
)
|
||||
|
||||
val body = response.body()?.data ?: throw ServiceException("Failed to update prompt")
|
||||
|
||||
// 更新本地状态
|
||||
originalIsPublic = isPublic
|
||||
|
||||
return body.toAgentEntity()
|
||||
} catch (e: Exception) {
|
||||
Log.e("AiPromptEditViewModel", "Error updating prompt", e)
|
||||
errorMessage = "更新失败: ${e.message}"
|
||||
throw e
|
||||
} finally {
|
||||
isUpdating = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证输入
|
||||
*/
|
||||
fun validate(): String? {
|
||||
return when {
|
||||
title.trim().isEmpty() -> "智能体名称不能为空"
|
||||
title.trim().length < 2 -> "智能体名称长度不能少于2个字符"
|
||||
title.trim().length > 20 -> "智能体名称长度不能超过20个字符"
|
||||
desc.trim().isEmpty() -> "智能体描述不能为空"
|
||||
desc.trim().length > 512 -> "智能体描述长度不能超过512个字符"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要付费解锁隐私切换
|
||||
*/
|
||||
fun needsPrivacyPayment(): Boolean {
|
||||
// 如果已经解锁过,则不需要付费
|
||||
if (paidForPrivacyEdit) {
|
||||
return false
|
||||
}
|
||||
// 只有从公开(true)切换到私有(false)才需要付费
|
||||
return originalIsPublic == true && isPublic == false
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解锁隐私权限的费用
|
||||
* @return 费用金额,如果无法获取则返回 0
|
||||
*/
|
||||
fun getPrivacyCost(): Int {
|
||||
val rules = PointService.pointsRules.value
|
||||
val costRule = rules?.sub?.get(PointService.PointsRuleKey.SPEND_AGENT_PRIVATE)
|
||||
return when (costRule) {
|
||||
is PointService.RuleAmount.Fixed -> costRule.value
|
||||
is PointService.RuleAmount.Range -> costRule.min // 使用最小值作为默认费用
|
||||
null -> 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前余额
|
||||
* @return 当前余额,如果无法获取则返回 0
|
||||
*/
|
||||
fun getCurrentBalance(): Int {
|
||||
return PointService.pointsBalance.value?.balance ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算消耗后余额
|
||||
* @param cost 费用
|
||||
* @return 消耗后余额
|
||||
*/
|
||||
fun calculateBalanceAfterCost(cost: Int): Int {
|
||||
val currentBalance = getCurrentBalance()
|
||||
return (currentBalance - cost).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查余额是否充足
|
||||
* @param cost 费用
|
||||
* @return 是否充足
|
||||
*/
|
||||
fun isBalanceSufficient(cost: Int): Boolean {
|
||||
return getCurrentBalance() >= cost
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空数据
|
||||
*/
|
||||
fun clearData() {
|
||||
chatAIId = ""
|
||||
title = ""
|
||||
desc = ""
|
||||
isPublic = true
|
||||
originalIsPublic = true
|
||||
paidForPrivacyEdit = false
|
||||
avatarUrl = null
|
||||
croppedBitmap = null
|
||||
isUpdating = false
|
||||
isLoading = false
|
||||
errorMessage = null
|
||||
isSelectingAvatar = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
package com.aiosman.ravenow.ui.composables
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
|
||||
/**
|
||||
* 全局付费确认对话框组件
|
||||
* 参考 iOS 版本的 PointsConfirmDialog
|
||||
*
|
||||
* @param cost 需要支付的费用
|
||||
* @param currentBalance 当前余额
|
||||
* @param balanceAfterCost 支付后余额
|
||||
* @param isBalanceSufficient 余额是否充足
|
||||
* @param onConfirm 确认支付回调
|
||||
* @param onCancel 取消回调
|
||||
* @param title 对话框标题
|
||||
* @param description 对话框描述
|
||||
* @param isProcessing 是否正在处理中
|
||||
*/
|
||||
@Composable
|
||||
fun PointsPaymentDialog(
|
||||
cost: Int,
|
||||
currentBalance: Int,
|
||||
balanceAfterCost: Int,
|
||||
isBalanceSufficient: Boolean,
|
||||
onConfirm: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
title: String,
|
||||
description: String,
|
||||
isProcessing: Boolean = false
|
||||
) {
|
||||
val appColors = LocalAppTheme.current
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenWidth = configuration.screenWidthDp.dp
|
||||
val dialogWidth = (screenWidth - 48.dp).coerceAtMost(360.dp)
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
if (!isProcessing) {
|
||||
onCancel()
|
||||
}
|
||||
},
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = !isProcessing,
|
||||
dismissOnClickOutside = !isProcessing
|
||||
)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.width(dialogWidth)
|
||||
.shadow(
|
||||
elevation = 20.dp,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
spotColor = Color.Black.copy(alpha = 0.2f)
|
||||
),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = appColors.background
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// 顶部图标 - 使用 paip_coin_img
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.paip_coin_img),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
|
||||
// 标题
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = appColors.text,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
// 描述
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = description,
|
||||
fontSize = 14.sp,
|
||||
color = appColors.secondaryText,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 20.dp)
|
||||
)
|
||||
|
||||
// 积分消耗信息区域
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = appColors.inputBackground.copy(alpha = 0.5f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 需要消耗
|
||||
CostInfoRow(
|
||||
label = stringResource(R.string.cost_required),
|
||||
amount = cost,
|
||||
appColors = appColors,
|
||||
amountColor = Color(0xFFFF8C00) // 橙色
|
||||
)
|
||||
|
||||
HorizontalDivider(color = appColors.divider)
|
||||
|
||||
// 当前余额
|
||||
CostInfoRow(
|
||||
label = stringResource(R.string.current_balance),
|
||||
amount = currentBalance,
|
||||
appColors = appColors,
|
||||
amountColor = if (isBalanceSufficient) appColors.text else Color.Red
|
||||
)
|
||||
|
||||
HorizontalDivider(color = appColors.divider)
|
||||
|
||||
// 支付后余额
|
||||
CostInfoRow(
|
||||
label = stringResource(R.string.balance_after),
|
||||
amount = balanceAfterCost,
|
||||
appColors = appColors,
|
||||
amountColor = appColors.text
|
||||
)
|
||||
}
|
||||
|
||||
// 余额不足提示
|
||||
if (!isBalanceSufficient) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFFFF8C00), // 橙色
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.insufficient_pai_coin_balance),
|
||||
fontSize = 13.sp,
|
||||
color = Color(0xFFFF8C00), // 橙色
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 取消按钮
|
||||
Button(
|
||||
onClick = {
|
||||
if (!isProcessing) {
|
||||
onCancel()
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp),
|
||||
enabled = !isProcessing,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = appColors.inputBackground,
|
||||
contentColor = appColors.text,
|
||||
disabledContainerColor = appColors.inputBackground,
|
||||
disabledContentColor = appColors.text.copy(alpha = 0.5f)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.cancel),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W500
|
||||
)
|
||||
}
|
||||
|
||||
// 确认按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(50.dp)
|
||||
.background(
|
||||
brush = if (isBalanceSufficient) {
|
||||
Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
appColors.main,
|
||||
appColors.main
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFFF8C00), // 橙色
|
||||
Color.Red
|
||||
)
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.then(
|
||||
if (!isProcessing) {
|
||||
Modifier.noRippleClickable {
|
||||
if (!isBalanceSufficient) {
|
||||
// 积分不足,跳转充值页面
|
||||
onCancel()
|
||||
// 这里可以发送通知或回调来跳转充值页面
|
||||
} else {
|
||||
// 积分充足,确认消费
|
||||
onConfirm()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isProcessing) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = if (isBalanceSufficient) {
|
||||
stringResource(R.string.confirm_consumption)
|
||||
} else {
|
||||
stringResource(R.string.go_recharge)
|
||||
},
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用信息行组件
|
||||
*/
|
||||
@Composable
|
||||
private fun CostInfoRow(
|
||||
label: String,
|
||||
amount: Int,
|
||||
appColors: com.aiosman.ravenow.AppThemeData,
|
||||
amountColor: Color? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 14.sp,
|
||||
color = appColors.secondaryText
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
// 星形图标(参考 iOS 版本)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(14.dp),
|
||||
tint = Color(0xFFFFD700) // 黄色
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${amount.formatNumber()}",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = amountColor ?: appColors.text
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.pai_coin),
|
||||
fontSize = 14.sp,
|
||||
color = appColors.secondaryText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字,添加千位分隔符
|
||||
*/
|
||||
private fun Int.formatNumber(): String {
|
||||
return this.toString().reversed().chunked(3).joinToString(",").reversed()
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ fun PolicyCheckbox(
|
||||
|
||||
addStyle(
|
||||
style = SpanStyle(
|
||||
color = appColor.main,
|
||||
color = Color(0xFF7C45ED), // 紫色
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
start = template.length + 1,
|
||||
|
||||
@@ -67,6 +67,13 @@ fun FormTextInput(
|
||||
.let {
|
||||
if (error != null) {
|
||||
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp))
|
||||
} else if (background != null && background == Color.White) {
|
||||
// 如果传入白色背景,添加灰色边框
|
||||
it.border(
|
||||
1.dp,
|
||||
Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f),
|
||||
RoundedCornerShape(25.dp)
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
|
||||
@@ -68,6 +68,13 @@ fun FormTextInput2(
|
||||
.let {
|
||||
if (error != null) {
|
||||
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp))
|
||||
} else if (background != null && background == Color.White) {
|
||||
// 如果传入白色背景,添加灰色边框
|
||||
it.border(
|
||||
1.dp,
|
||||
Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f),
|
||||
RoundedCornerShape(25.dp)
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.aiosman.ravenow.ui.group
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -14,11 +15,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
@@ -27,11 +32,15 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.composables.TabItem
|
||||
@@ -91,6 +100,19 @@ fun CreateGroupChatScreen() {
|
||||
|
||||
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
|
||||
// 获取费用和余额信息
|
||||
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
|
||||
val pointsBalance by PointService.pointsBalance.collectAsState(initial = null)
|
||||
val roomMaxMembers by PointService.roomMaxMembers.collectAsState(initial = null)
|
||||
|
||||
val cost = CreateGroupChatViewModel.getCreateRoomCost()
|
||||
val currentBalance = CreateGroupChatViewModel.getCurrentBalance()
|
||||
val balanceAfterCost = CreateGroupChatViewModel.calculateBalanceAfterCost(cost)
|
||||
val isBalanceSufficient = CreateGroupChatViewModel.isBalanceSufficient(cost)
|
||||
|
||||
// 获取群聊初始上限
|
||||
val maxMemberLimit = roomMaxMembers?.defaultMaxTotal ?: 5
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setNavigationBarColor(Color.Transparent)
|
||||
}
|
||||
@@ -367,14 +389,19 @@ fun CreateGroupChatScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
// Tab切换和成员数量显示
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Tab左对齐
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TabItem(
|
||||
text = stringResource(R.string.chat_ai),
|
||||
@@ -397,6 +424,15 @@ fun CreateGroupChatScreen() {
|
||||
)
|
||||
}
|
||||
|
||||
// 成员数量显示右对齐(x/x格式)
|
||||
Text(
|
||||
text = "${selectedMembers.size}/$maxMemberLimit",
|
||||
fontSize = 14.sp,
|
||||
color = if (selectedMembers.size > maxMemberLimit) AppColors.error else AppColors.secondaryText,
|
||||
fontWeight = FontWeight.W500
|
||||
)
|
||||
}
|
||||
|
||||
// 内容区域 - 自适应填满剩余高度
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
@@ -436,11 +472,44 @@ fun CreateGroupChatScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// 余额和扣减积分显示(创建按钮上方)
|
||||
val buttonTopPadding = if (cost > 0) 4.dp else 16.dp
|
||||
if (cost > 0) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 4.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "${stringResource(R.string.create_group_chat_current_balance)}: ${currentBalance.formatNumber()} ${stringResource(R.string.pai_coin)}",
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = "${stringResource(R.string.create_group_chat_required_cost)} ${cost.formatNumber()} ${stringResource(R.string.pai_coin)}",
|
||||
fontSize = 12.sp,
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建群聊按钮 - 固定在底部
|
||||
Button(
|
||||
onClick = {
|
||||
// 创建群聊逻辑
|
||||
if (selectedMembers.isNotEmpty()) {
|
||||
// 检查是否超过上限
|
||||
if (selectedMembers.size > maxMemberLimit) {
|
||||
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
|
||||
return@Button
|
||||
}
|
||||
// 如果费用大于0,显示确认弹窗
|
||||
if (cost > 0) {
|
||||
CreateGroupChatViewModel.showConfirmDialog()
|
||||
} else {
|
||||
// 费用为0,直接创建
|
||||
scope.launch {
|
||||
val success = CreateGroupChatViewModel.createGroupChat(
|
||||
groupName = groupName.text,
|
||||
@@ -452,10 +521,11 @@ fun CreateGroupChatScreen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = navigationBarPadding + 16.dp),
|
||||
.padding(start = 16.dp, end = 16.dp, top = buttonTopPadding, bottom = navigationBarPadding + 16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = AppColors.main,
|
||||
contentColor = AppColors.mainText,
|
||||
@@ -482,6 +552,38 @@ fun CreateGroupChatScreen() {
|
||||
|
||||
}
|
||||
|
||||
// 消费确认弹窗
|
||||
if (CreateGroupChatViewModel.showConfirmDialog) {
|
||||
CreateGroupChatConfirmDialog(
|
||||
cost = cost,
|
||||
currentBalance = currentBalance,
|
||||
balanceAfterCost = balanceAfterCost,
|
||||
isBalanceSufficient = isBalanceSufficient,
|
||||
onConfirm = {
|
||||
// 检查是否超过上限
|
||||
if (selectedMembers.size > maxMemberLimit) {
|
||||
CreateGroupChatViewModel.hideConfirmDialog()
|
||||
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
|
||||
} else {
|
||||
CreateGroupChatViewModel.hideConfirmDialog()
|
||||
scope.launch {
|
||||
val success = CreateGroupChatViewModel.createGroupChat(
|
||||
groupName = groupName.text,
|
||||
selectedMembers = selectedMembers,
|
||||
context = context
|
||||
)
|
||||
if (success) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onCancel = {
|
||||
CreateGroupChatViewModel.hideConfirmDialog()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 居中显示的错误提示弹窗
|
||||
CreateGroupChatViewModel.errorMessage?.let { error ->
|
||||
Box(
|
||||
@@ -496,7 +598,7 @@ fun CreateGroupChatScreen() {
|
||||
horizontalAlignment = Alignment.CenterHorizontally, // 水平居中
|
||||
verticalArrangement = Arrangement.Center // 垂直居中
|
||||
) {
|
||||
androidx.compose.material3.Card(
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.8f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
@@ -508,7 +610,7 @@ fun CreateGroupChatScreen() {
|
||||
.fillMaxWidth(),
|
||||
color = Color.Red,
|
||||
fontSize = 14.sp,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -516,3 +618,219 @@ fun CreateGroupChatScreen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建群聊消费确认弹窗
|
||||
*/
|
||||
@Composable
|
||||
fun CreateGroupChatConfirmDialog(
|
||||
cost: Int,
|
||||
currentBalance: Int,
|
||||
balanceAfterCost: Int,
|
||||
isBalanceSufficient: Boolean,
|
||||
onConfirm: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onCancel,
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = true
|
||||
)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// 硬币图标(使用文本代替,实际项目中可以使用图片资源)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFFFD700), // 金色
|
||||
Color(0xFFFFA500) // 橙色
|
||||
)
|
||||
),
|
||||
shape = CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "pai",
|
||||
color = Color.White,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 标题
|
||||
Text(
|
||||
text = stringResource(R.string.create_group_chat_confirm_title),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 需要消耗
|
||||
CostInfoRow(
|
||||
label = stringResource(R.string.create_group_chat_required_cost),
|
||||
amount = cost,
|
||||
AppColors = AppColors
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// 当前余额
|
||||
CostInfoRow(
|
||||
label = stringResource(R.string.create_group_chat_current_balance),
|
||||
amount = currentBalance,
|
||||
AppColors = AppColors
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// 消耗后余额
|
||||
CostInfoRow(
|
||||
label = stringResource(R.string.create_group_chat_balance_after),
|
||||
amount = balanceAfterCost,
|
||||
AppColors = AppColors
|
||||
)
|
||||
|
||||
// 余额不足提示
|
||||
if (!isBalanceSufficient) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.create_group_chat_insufficient_balance),
|
||||
color = Color.Red,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 按钮行
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 取消按钮
|
||||
OutlinedButton(
|
||||
onClick = onCancel,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = AppColors.secondaryText.copy(alpha = 0.3f),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = AppColors.text
|
||||
),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.cancel),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
}
|
||||
|
||||
// 确认消耗按钮
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
enabled = isBalanceSufficient,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = AppColors.main,
|
||||
contentColor = AppColors.mainText,
|
||||
disabledContainerColor = AppColors.disabledBackground,
|
||||
disabledContentColor = AppColors.text
|
||||
),
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.create_group_chat_confirm_consume),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用信息行组件
|
||||
*/
|
||||
@Composable
|
||||
fun CostInfoRow(
|
||||
label: String,
|
||||
amount: Int,
|
||||
AppColors: com.aiosman.ravenow.AppThemeData
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 14.sp,
|
||||
color = AppColors.text
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
// 小硬币图标(使用简单的圆形)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFFFFD700),
|
||||
Color(0xFFFFA500)
|
||||
)
|
||||
),
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${amount.formatNumber()} ${stringResource(R.string.pai_coin)}",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = AppColors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字,添加千位分隔符
|
||||
*/
|
||||
fun Int.formatNumber(): String {
|
||||
return this.toString().reversed().chunked(3).joinToString(",").reversed()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.aiosman.ravenow.ConstVars
|
||||
import com.aiosman.ravenow.data.AccountNotice
|
||||
import com.aiosman.ravenow.data.AccountService
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import com.aiosman.ravenow.data.UserService
|
||||
import com.aiosman.ravenow.data.UserServiceImpl
|
||||
import com.aiosman.ravenow.R
|
||||
@@ -36,6 +37,7 @@ object CreateGroupChatViewModel : ViewModel() {
|
||||
// 状态管理
|
||||
var isLoading by mutableStateOf(false)
|
||||
var errorMessage by mutableStateOf<String?>(null)
|
||||
var showConfirmDialog by mutableStateOf(false)
|
||||
|
||||
// 创建群聊
|
||||
suspend fun createGroupChat(
|
||||
@@ -76,6 +78,11 @@ object CreateGroupChatViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示错误信息(公开方法)
|
||||
fun showError(message: String) {
|
||||
showToast(message)
|
||||
}
|
||||
|
||||
// 清除错误信息
|
||||
fun clearError() {
|
||||
errorMessage = null
|
||||
@@ -107,4 +114,59 @@ object CreateGroupChatViewModel : ViewModel() {
|
||||
addSelectedMember(member, selectedMemberIds, selectedMembers)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取创建群聊的费用
|
||||
* @return 费用金额,如果无法获取则返回 0
|
||||
*/
|
||||
fun getCreateRoomCost(): Int {
|
||||
val rules = PointService.pointsRules.value
|
||||
val costRule = rules?.sub?.get(PointService.PointsRuleKey.CREATE_ROOM)
|
||||
return when (costRule) {
|
||||
is PointService.RuleAmount.Fixed -> costRule.value
|
||||
is PointService.RuleAmount.Range -> costRule.min // 使用最小值作为默认费用
|
||||
null -> 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前余额
|
||||
* @return 当前余额,如果无法获取则返回 0
|
||||
*/
|
||||
fun getCurrentBalance(): Int {
|
||||
return PointService.pointsBalance.value?.balance ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算消耗后余额
|
||||
* @param cost 费用
|
||||
* @return 消耗后余额
|
||||
*/
|
||||
fun calculateBalanceAfterCost(cost: Int): Int {
|
||||
val currentBalance = getCurrentBalance()
|
||||
return (currentBalance - cost).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查余额是否充足
|
||||
* @param cost 费用
|
||||
* @return 是否充足
|
||||
*/
|
||||
fun isBalanceSufficient(cost: Int): Boolean {
|
||||
return getCurrentBalance() >= cost
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示确认弹窗
|
||||
*/
|
||||
fun showConfirmDialog() {
|
||||
showConfirmDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏确认弹窗
|
||||
*/
|
||||
fun hideConfirmDialog() {
|
||||
showConfirmDialog = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ fun Agent() {
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = AppColors.background
|
||||
),
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
modifier = Modifier
|
||||
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
|
||||
.padding(top = statusBarPaddingValues.calculateTopPadding())
|
||||
|
||||
@@ -54,6 +54,8 @@ import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToGroupChat
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import com.aiosman.ravenow.data.api.ApiClient
|
||||
import android.util.Base64
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
@@ -209,7 +211,12 @@ fun RoomItem(
|
||||
val avatarUrl = if (room.avatar.isNotEmpty()) {
|
||||
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
|
||||
} else {
|
||||
""
|
||||
// 如果头像为空,使用群头像接口
|
||||
val groupIdBase64 = Base64.encodeToString(
|
||||
room.trtcType.toByteArray(),
|
||||
Base64.NO_WRAP
|
||||
)
|
||||
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=${groupIdBase64}&token=${AppStore.token}"
|
||||
}
|
||||
|
||||
Row(
|
||||
|
||||
@@ -74,7 +74,9 @@ import com.aiosman.ravenow.ui.composables.MomentCard
|
||||
import com.aiosman.ravenow.ui.composables.TabItem
|
||||
import com.aiosman.ravenow.ui.composables.TabSpacer
|
||||
import com.aiosman.ravenow.ui.index.tabs.message.tab.AgentChatListViewModel
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.composable.RoomItem
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.navigateToGroupChat
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.launch
|
||||
import com.aiosman.ravenow.utils.NetworkUtils
|
||||
@@ -88,7 +90,7 @@ fun SearchScreen() {
|
||||
val context = LocalContext.current
|
||||
val model = SearchViewModel
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState(pageCount = { 3 })
|
||||
val pagerState = rememberPagerState(pageCount = { 4 })
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||
@@ -168,8 +170,9 @@ fun SearchScreen() {
|
||||
onClick = { term ->
|
||||
coroutineScope.launch {
|
||||
keyboardController?.hide()
|
||||
model.searchText = term
|
||||
model.onTextChanged(term)
|
||||
pagerState.scrollToPage(0)
|
||||
pagerState.animateScrollToPage(0)
|
||||
model.search()
|
||||
}
|
||||
},
|
||||
@@ -222,6 +225,18 @@ fun SearchScreen() {
|
||||
}
|
||||
)
|
||||
}
|
||||
TabSpacer()
|
||||
Box {
|
||||
TabItem(
|
||||
text = stringResource(R.string.chat_group),
|
||||
isSelected = pagerState.currentPage == 3,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(3)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,6 +391,7 @@ fun SearchPager(
|
||||
0 -> MomentResultTab()
|
||||
1 -> UserResultTab()
|
||||
2 -> AiResultTab()
|
||||
3 -> RoomResultTab()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -753,6 +769,100 @@ fun AiResultTab() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun RoomResultTab() {
|
||||
val model = SearchViewModel
|
||||
val rooms = model.roomsFlow.collectAsLazyPagingItems()
|
||||
val AppColors = LocalAppTheme.current
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background)
|
||||
) {
|
||||
if (rooms.itemCount == 0 && model.showResult) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||
|
||||
if (isNetworkAvailable) {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(
|
||||
id = if (AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
||||
else R.mipmap.invalid_name_1
|
||||
),
|
||||
contentDescription = "No Result",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Text(
|
||||
text = "咦,什么都没找到...",
|
||||
color = LocalAppTheme.current.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "换个关键词试试吧,也许会有新发现!",
|
||||
color = LocalAppTheme.current.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
} else {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||
contentDescription = "network error",
|
||||
modifier = Modifier.size(140.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_title),
|
||||
color = LocalAppTheme.current.text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W600
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||
color = LocalAppTheme.current.secondaryText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W400
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
ReloadButton(
|
||||
onClick = {
|
||||
SearchViewModel.ResetModel()
|
||||
SearchViewModel.search()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(rooms.itemCount) { idx ->
|
||||
val roomItem = rooms[idx] ?: return@items
|
||||
RoomItem(
|
||||
room = roomItem,
|
||||
onRoomClick = { roomEntity ->
|
||||
navController.navigateToGroupChat(
|
||||
id = roomEntity.trtcRoomId,
|
||||
name = roomEntity.name,
|
||||
avatar = roomEntity.avatar
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReloadButton(
|
||||
onClick: () -> Unit
|
||||
|
||||
@@ -23,6 +23,9 @@ import com.aiosman.ravenow.entity.AgentEntity
|
||||
import com.aiosman.ravenow.entity.AgentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.AgentSearchPagingSource
|
||||
import com.aiosman.ravenow.entity.AgentServiceImpl
|
||||
import com.aiosman.ravenow.entity.RoomEntity
|
||||
import com.aiosman.ravenow.entity.RoomRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.RoomSearchPagingSource
|
||||
import com.aiosman.ravenow.utils.SearchHistoryStore
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -40,6 +43,8 @@ object SearchViewModel : ViewModel() {
|
||||
val usersFlow = _usersFlow.asStateFlow()
|
||||
private val _agentsFlow = MutableStateFlow<PagingData<AgentEntity>>(PagingData.empty())
|
||||
val agentsFlow = _agentsFlow.asStateFlow()
|
||||
private val _roomsFlow = MutableStateFlow<PagingData<RoomEntity>>(PagingData.empty())
|
||||
val roomsFlow = _roomsFlow.asStateFlow()
|
||||
private lateinit var historyStore: SearchHistoryStore
|
||||
private val _historyFlow = MutableStateFlow<List<String>>(emptyList())
|
||||
val historyFlow = _historyFlow.asStateFlow()
|
||||
@@ -108,6 +113,19 @@ object SearchViewModel : ViewModel() {
|
||||
_agentsFlow.value = it
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
RoomSearchPagingSource(
|
||||
RoomRemoteDataSource(),
|
||||
keyword = searchText
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_roomsFlow.value = it
|
||||
}
|
||||
}
|
||||
showResult = true
|
||||
}
|
||||
|
||||
@@ -153,6 +171,7 @@ object SearchViewModel : ViewModel() {
|
||||
_momentsFlow.value = PagingData.empty()
|
||||
_usersFlow.value = PagingData.empty()
|
||||
_agentsFlow.value = PagingData.empty()
|
||||
_roomsFlow.value = PagingData.empty()
|
||||
showResult = false
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +160,13 @@ fun EmailSignupScreen() {
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
// 顶部导航栏:返回箭头 + "注册"标题,左对齐
|
||||
// 根据暗色模式适配颜色
|
||||
val isDarkMode = AppState.darkMode
|
||||
val textColor = if (isDarkMode) {
|
||||
Color.White
|
||||
} else {
|
||||
Color.Black
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -174,14 +181,14 @@ fun EmailSignupScreen() {
|
||||
.noRippleClickable {
|
||||
navController.navigateUp()
|
||||
},
|
||||
colorFilter = ColorFilter.tint(Color.Black)
|
||||
colorFilter = ColorFilter.tint(textColor)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.sign_up_upper),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = Color.Black
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
|
||||
@@ -194,6 +201,19 @@ fun EmailSignupScreen() {
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 暗色模式下的输入框颜色配置
|
||||
val isDarkMode = AppState.darkMode
|
||||
val inputBackgroundColor = if (isDarkMode) {
|
||||
Color(0xFF1C1C1C) // 暗色模式下深灰色背景
|
||||
} else {
|
||||
null // 亮色模式下使用默认背景
|
||||
}
|
||||
val textColor = if (isDarkMode) {
|
||||
Color(0xFFFFFFFF).copy(alpha = 0.7f) // 暗色模式下 label 和 hint 文本颜色
|
||||
} else {
|
||||
null // 使用默认颜色
|
||||
}
|
||||
|
||||
// 邮箱输入框
|
||||
TextInputField(
|
||||
modifier = Modifier
|
||||
@@ -214,7 +234,9 @@ fun EmailSignupScreen() {
|
||||
colorFilter = ColorFilter.tint(IconGray)
|
||||
)
|
||||
},
|
||||
customBackgroundColor = LightGrayBackground,
|
||||
customBackgroundColor = inputBackgroundColor,
|
||||
customHintColor = textColor,
|
||||
customLabelColor = textColor,
|
||||
customCornerRadius = 16f
|
||||
)
|
||||
|
||||
@@ -239,7 +261,9 @@ fun EmailSignupScreen() {
|
||||
colorFilter = ColorFilter.tint(IconGray)
|
||||
)
|
||||
},
|
||||
customBackgroundColor = LightGrayBackground,
|
||||
customBackgroundColor = inputBackgroundColor,
|
||||
customHintColor = textColor,
|
||||
customLabelColor = textColor,
|
||||
customCornerRadius = 16f
|
||||
)
|
||||
|
||||
@@ -264,7 +288,9 @@ fun EmailSignupScreen() {
|
||||
colorFilter = ColorFilter.tint(IconGray)
|
||||
)
|
||||
},
|
||||
customBackgroundColor = LightGrayBackground,
|
||||
customBackgroundColor = inputBackgroundColor,
|
||||
customHintColor = textColor,
|
||||
customLabelColor = textColor,
|
||||
customCornerRadius = 16f
|
||||
)
|
||||
|
||||
|
||||
@@ -280,10 +280,7 @@ fun LoginPage() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.background(
|
||||
color = AppColors.text.copy(alpha = 0.1f),
|
||||
shape = androidx.compose.foundation.shape.CircleShape
|
||||
)
|
||||
|
||||
.noRippleClickable {
|
||||
guestLogin()
|
||||
},
|
||||
@@ -292,7 +289,7 @@ fun LoginPage() {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_close),
|
||||
contentDescription = "Close",
|
||||
modifier = Modifier.size(16.dp),
|
||||
modifier = Modifier.size(24.dp),
|
||||
colorFilter = ColorFilter.tint(AppColors.text)
|
||||
)
|
||||
}
|
||||
@@ -313,26 +310,26 @@ fun LoginPage() {
|
||||
.height(400.dp)
|
||||
|
||||
) {
|
||||
val lottieFile = if (AppState.darkMode) "login.lottie" else "login_light.lottie"
|
||||
val lottieFile = "login.lottie"
|
||||
LottieAnimation(
|
||||
composition = rememberLottieComposition(LottieCompositionSpec.Asset(lottieFile)).value,
|
||||
iterations = LottieConstants.IterateForever,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(if(AppState.darkMode) R.mipmap.login_paipia_dark else R.mipmap.login_paipia_light),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.size(width = 140.dp, height = 30.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.join_party_carnival),
|
||||
fontSize = 17.sp,
|
||||
color = AppColors.text.copy(alpha = 0.6f),
|
||||
fontWeight = FontWeight.W600,
|
||||
color = AppColors.text
|
||||
)
|
||||
// Image(
|
||||
// painter = painterResource(id = R.mipmap.invalid_name),
|
||||
// contentDescription = "Rave Now",
|
||||
// modifier = Modifier
|
||||
// .size(52.dp)
|
||||
// .clip(RoundedCornerShape(10.dp))
|
||||
// )
|
||||
// Spacer(modifier = Modifier.height(8.dp))
|
||||
// Text(
|
||||
// "Rave Now",
|
||||
@@ -347,16 +344,16 @@ fun LoginPage() {
|
||||
// fontWeight = FontWeight.W700,
|
||||
// color = AppColors.text
|
||||
// )
|
||||
//注册tab
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
//登录tab
|
||||
Spacer(modifier = Modifier.height(35.dp))
|
||||
ActionButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.sign_up_upper),
|
||||
text = stringResource(R.string.login_upper),
|
||||
color = if (AppState.darkMode) Color.Black else Color.White,
|
||||
backgroundColor = if (AppState.darkMode) Color.White else Color.Black
|
||||
) {
|
||||
navController.navigate(
|
||||
NavigationRoute.EmailSignUp.route,
|
||||
NavigationRoute.UserAuth.route,
|
||||
)
|
||||
}
|
||||
//谷歌登录tab
|
||||
@@ -386,10 +383,10 @@ fun LoginPage() {
|
||||
googleLogin()
|
||||
}
|
||||
|
||||
//登录tab
|
||||
//注册tab
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.login_upper),
|
||||
text = stringResource(R.string.sign_up_upper),
|
||||
color = AppColors.text.copy(alpha = 0.5f),
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -397,7 +394,7 @@ fun LoginPage() {
|
||||
.fillMaxWidth()
|
||||
.noRippleClickable {
|
||||
navController.navigate(
|
||||
NavigationRoute.UserAuth.route,
|
||||
NavigationRoute.EmailSignUp.route,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -23,10 +23,11 @@ object PointsViewModel : ViewModel() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
loading = true
|
||||
// 并行预加载积分定价表(不影响UI)
|
||||
// 并行预加载积分定价表和群聊人数限制(不影响UI)
|
||||
launch {
|
||||
try {
|
||||
PointService.refreshPointsRules()
|
||||
PointService.refreshRoomMaxMembers()
|
||||
} catch (e: Exception) {
|
||||
Log.e("PointsViewModel", "refresh rules error", e)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.aiosman.ravenow.ui.profile
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -10,16 +11,21 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -55,7 +61,16 @@ fun AiProfileV3(
|
||||
onComment: (MomentEntity) -> Unit = {},
|
||||
) {
|
||||
val appColors = LocalAppTheme.current
|
||||
val navController = LocalNavController.current
|
||||
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
// 判断是否是创建者
|
||||
val isCreator = remember(profile) {
|
||||
profile?.creatorProfile?.id?.let { creatorId ->
|
||||
AppState.UserId?.toLong() == creatorId
|
||||
} ?: false
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -140,12 +155,42 @@ fun AiProfileV3(
|
||||
}
|
||||
|
||||
// 顶部返回按钮
|
||||
TopBar()
|
||||
TopBar(
|
||||
onMenuClick = { showMenu = true }
|
||||
)
|
||||
}
|
||||
|
||||
// 底部菜单
|
||||
if (showMenu) {
|
||||
AiProfileMenuModal(
|
||||
onDismiss = { showMenu = false },
|
||||
onChatClick = {
|
||||
showMenu = false
|
||||
onChatClick()
|
||||
},
|
||||
onShareClick = {
|
||||
showMenu = false
|
||||
onShareClick()
|
||||
},
|
||||
onEditClick = {
|
||||
showMenu = false
|
||||
// 导航到编辑页面
|
||||
profile?.chatAIId?.let { chatAIId ->
|
||||
navController.navigate(
|
||||
NavigationRoute.AiPromptEdit.route.replace("{chatAIId}", chatAIId)
|
||||
)
|
||||
}
|
||||
},
|
||||
profile = profile,
|
||||
showEdit = isCreator
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopBar() {
|
||||
private fun TopBar(
|
||||
onMenuClick: () -> Unit = {}
|
||||
) {
|
||||
val navController = LocalNavController.current
|
||||
val appColors = LocalAppTheme.current
|
||||
|
||||
@@ -182,6 +227,25 @@ private fun TopBar() {
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
// 菜单按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Black.copy(alpha = 0.3f))
|
||||
.noRippleClickable {
|
||||
onMenuClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rider_pro_more_horizon),
|
||||
contentDescription = "Menu",
|
||||
modifier = Modifier.size(24.dp),
|
||||
colorFilter = ColorFilter.tint(Color.White)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,3 +518,130 @@ private fun AiProfileActions(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AiProfileMenuModal(
|
||||
onDismiss: () -> Unit,
|
||||
onChatClick: () -> Unit,
|
||||
onShareClick: () -> Unit,
|
||||
onEditClick: () -> Unit,
|
||||
showEdit: Boolean = false,
|
||||
profile: AccountProfileEntity? = null
|
||||
) {
|
||||
val appColors = LocalAppTheme.current
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
containerColor = appColors.background,
|
||||
dragHandle = {},
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(if (showEdit) 240.dp else 160.dp)
|
||||
.background(appColors.background)
|
||||
.padding(vertical = 47.dp, horizontal = 20.dp)
|
||||
) {
|
||||
// 私信选项
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.noRippleClickable {
|
||||
onChatClick()
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_message),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(24.dp),
|
||||
colorFilter = ColorFilter.tint(appColors.text)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.chat_upper),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = appColors.text
|
||||
)
|
||||
}
|
||||
|
||||
// 分享选项
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = if (showEdit) 16.dp else 0.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.noRippleClickable {
|
||||
onShareClick()
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rider_pro_share),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(24.dp),
|
||||
colorFilter = ColorFilter.tint(appColors.text)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.share),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = appColors.text
|
||||
)
|
||||
}
|
||||
|
||||
// 编辑选项(仅创建者可见)
|
||||
if (showEdit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.noRippleClickable {
|
||||
onEditClick()
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.group_info_edit),
|
||||
contentDescription = "",
|
||||
modifier = Modifier.size(24.dp),
|
||||
colorFilter = ColorFilter.tint(appColors.text)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.edit_profile),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = appColors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,12 @@ class AiProfileViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
try {
|
||||
profile = userService.getUserProfile(id)
|
||||
// 先通过用户ID获取基本信息,获取chatAIId
|
||||
val basicProfile = userService.getUserProfile(id)
|
||||
profileId = id.toInt()
|
||||
|
||||
// 使用chatAIId通过getUserProfileByOpenId获取完整信息(包含creatorProfile)
|
||||
profile = userService.getUserProfileByOpenId(basicProfile.chatAIId)
|
||||
} catch (e: Exception) {
|
||||
Log.e("AiProfileViewModel", "Error loading profile", e)
|
||||
e.printStackTrace()
|
||||
|
||||
20
app/src/main/res/drawable/ic_brain_add.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- 脑部轮廓 - 更简洁的设计 -->
|
||||
<path
|
||||
android:pathData="M12,4C9.2,4 7,6.2 7,9C7,10.2 7.4,11.3 8,12.1C7.6,12.7 7.3,13.4 7.3,14.1C7.3,15.7 8.6,17 10.2,17C10.6,17 11,16.9 11.3,16.7C11.7,17.3 12.4,17.7 13.2,17.7C14.5,17.7 15.5,16.7 15.5,15.4C15.5,15 15.4,14.6 15.2,14.3C15.7,14 16,13.4 16,12.8C16,11.8 15.4,11 14.5,10.7C14.7,10.2 14.8,9.6 14.8,9C14.8,6.2 12.6,4 9.8,4H12Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.8"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#7C45ED"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeLineCap="round"/>
|
||||
<!-- 加号 - 位于右上角 -->
|
||||
<path
|
||||
android:pathData="M17,7h-1.5v1.5h-1.5v1.5h1.5v1.5h1.5v-1.5h1.5v-1.5h-1.5v-1.5z"
|
||||
android:fillColor="#7C45ED"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
BIN
app/src/main/res/mipmap-hdpi/login_paipia_dark.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/mipmap-hdpi/login_paipia_light.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/login_paipia_dark.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/login_paipia_light.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/login_paipia_dark.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/login_paipia_light.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/login_paipia_dark.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/login_paipia_light.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/login_paipia_dark.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/login_paipia_light.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -367,5 +367,6 @@
|
||||
<string name="why_not_start_with_agent">エージェントから世界を知り始めませんか?</string>
|
||||
<string name="explore">探検する</string>
|
||||
<string name="reply_to_user">返信@%1$s</string>
|
||||
<string name="error_select_at_least_one_image">少なくとも1枚の画像を選択してください。</string>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -368,8 +368,22 @@
|
||||
<string name="recharge_pai_coin">充值派币</string>
|
||||
<string name="recharge_pai_coin_desc">多种套餐可选,立即充值</string>
|
||||
<string name="create_group_chat_failed">创建群聊失败: %1$s</string>
|
||||
<string name="create_group_chat_confirm_title">创建群聊确认</string>
|
||||
<string name="create_group_chat_required_cost">需要消耗:</string>
|
||||
<string name="create_group_chat_current_balance">当前余额</string>
|
||||
<string name="create_group_chat_balance_after">消耗后余额:</string>
|
||||
<string name="create_group_chat_confirm_consume">确认消耗</string>
|
||||
<string name="create_group_chat_insufficient_balance">余额不足</string>
|
||||
<string name="create_group_chat_exceed_limit">成员数量超过上限(%1$d)</string>
|
||||
<string name="pai_coin">派币</string>
|
||||
<string name="cost_required">需要消耗</string>
|
||||
<string name="balance_after">支付后余额</string>
|
||||
<string name="insufficient_pai_coin_balance">派币余额不足</string>
|
||||
<string name="go_recharge">去充值</string>
|
||||
<string name="confirm_consumption">确认消费</string>
|
||||
<string name="connect_world_start_following">连接世界,从关注开始</string>
|
||||
<string name="why_not_start_with_agent">不如从一个 Agent 开始认识这世界?</string>
|
||||
<string name="explore">去探索</string>
|
||||
<string name="reply_to_user">回复@%1$s</string>
|
||||
<string name="error_select_at_least_one_image">请至少选择一张图片</string>
|
||||
</resources>
|
||||
@@ -361,8 +361,22 @@
|
||||
<string name="recharge_pai_coin">Recharge Pai Coin</string>
|
||||
<string name="recharge_pai_coin_desc">Multiple packages available, recharge now</string>
|
||||
<string name="create_group_chat_failed">Failed to create group chat: %1$s</string>
|
||||
<string name="create_group_chat_confirm_title">Create Group Chat</string>
|
||||
<string name="create_group_chat_required_cost">Required consumption:</string>
|
||||
<string name="create_group_chat_current_balance">Current balance</string>
|
||||
<string name="create_group_chat_balance_after">Balance after consumption:</string>
|
||||
<string name="create_group_chat_confirm_consume">Confirm consumption</string>
|
||||
<string name="create_group_chat_insufficient_balance">Insufficient balance</string>
|
||||
<string name="create_group_chat_exceed_limit">Member count exceeds the limit (%1$d)</string>
|
||||
<string name="pai_coin">Pai Coin</string>
|
||||
<string name="cost_required">Cost Required</string>
|
||||
<string name="balance_after">Balance After</string>
|
||||
<string name="insufficient_pai_coin_balance">Insufficient Pai Coin Balance</string>
|
||||
<string name="go_recharge">Go Recharge</string>
|
||||
<string name="confirm_consumption">Confirm Consumption</string>
|
||||
<string name="connect_world_start_following">Connect the world, start by following</string>
|
||||
<string name="why_not_start_with_agent">Why not start exploring the world with an Agent?</string>
|
||||
<string name="explore">Explore</string>
|
||||
<string name="reply_to_user">Reply @%1$s</string>
|
||||
<string name="error_select_at_least_one_image">Please select at least one image</string>
|
||||
</resources>
|
||||