Merge branch 'main' into zhong_1
@@ -53,7 +53,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.App.Starting"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="fontScale|orientation|screenSize|keyboardHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -22,6 +23,8 @@ import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
@@ -57,6 +60,21 @@ class MainActivity : ComponentActivity() {
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
val context = this
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
// 禁用字体缩放,固定字体大小为系统默认大小
|
||||
val configuration = Configuration(newBase.resources.configuration)
|
||||
configuration.fontScale = 1.0f
|
||||
val context = newBase.createConfigurationContext(configuration)
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
// 确保配置变化时字体缩放保持为 1.0
|
||||
val config = Configuration(newConfig)
|
||||
config.fontScale = 1.0f
|
||||
super.onConfigurationChanged(config)
|
||||
}
|
||||
|
||||
// 请求通知权限
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission(),
|
||||
@@ -128,6 +146,15 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
setContent {
|
||||
// 强制字体缩放为 1.0 - 通过覆盖 Density 来实现
|
||||
val density = LocalDensity.current
|
||||
val fixedDensity = remember {
|
||||
androidx.compose.ui.unit.Density(
|
||||
density = density.density,
|
||||
fontScale = 1.0f
|
||||
)
|
||||
}
|
||||
|
||||
var showSplash by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -139,7 +166,8 @@ class MainActivity : ComponentActivity() {
|
||||
SplashScreen()
|
||||
} else {
|
||||
CompositionLocalProvider(
|
||||
LocalAppTheme provides AppState.appTheme
|
||||
LocalAppTheme provides AppState.appTheme,
|
||||
LocalDensity provides fixedDensity
|
||||
) {
|
||||
CheckUpdateDialog()
|
||||
// 全局挂载积分底部弹窗 Host
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.aiosman.ravenow
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import com.google.firebase.FirebaseApp
|
||||
import com.google.firebase.perf.FirebasePerformance
|
||||
@@ -11,6 +12,14 @@ import com.google.firebase.perf.FirebasePerformance
|
||||
*/
|
||||
class RaveNowApplication : Application() {
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
// 禁用字体缩放,固定字体大小为系统默认大小
|
||||
val configuration = Configuration(base.resources.configuration)
|
||||
configuration.fontScale = 1.0f
|
||||
val context = base.createConfigurationContext(configuration)
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,525 @@
|
||||
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.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 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 needsPayment = viewModel.needsPrivacyPayment()
|
||||
val privacyCost = 100 // 默认100钥匙,后续可以从PointService获取
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = appColors.background),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
|
||||
// 顶部导航栏
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = appColors.background)
|
||||
.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(appColors.background)
|
||||
) {
|
||||
// 头像选择
|
||||
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 = appColors.inputBackground2,
|
||||
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 = appColors.inputBackground2,
|
||||
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(12.dp))
|
||||
.background(appColors.inputBackground2)
|
||||
.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) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(appColors.inputBackground2.copy(alpha = 0.9f))
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = "首次解锁 AI 权限",
|
||||
fontSize = 13.sp,
|
||||
color = appColors.text,
|
||||
fontWeight = FontWeight.W500
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "将消耗",
|
||||
fontSize = 12.sp,
|
||||
color = appColors.text.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "$privacyCost",
|
||||
fontSize = 12.sp,
|
||||
color = appColors.brandColorsColor,
|
||||
fontWeight = FontWeight.W500
|
||||
)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
Text(
|
||||
text = "钥匙",
|
||||
fontSize = 12.sp,
|
||||
color = appColors.brandColorsColor,
|
||||
fontWeight = FontWeight.W500
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "解锁后可随时切换",
|
||||
fontSize = 12.sp,
|
||||
color = appColors.text.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
// 添加智能体记忆按钮
|
||||
AddAgentMemoryButton(
|
||||
onAddMemoryClick = {
|
||||
// TODO: 导航到记忆管理页面
|
||||
// navController.navigate(NavigationRoute.AgentMemoryManage.route.replace("{chatAIId}", chatAIId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 底部保存按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = appColors.background)
|
||||
.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) {
|
||||
// TODO: 实现付费确认对话框
|
||||
// 暂时直接切换,后续可以添加积分检查和扣减逻辑
|
||||
androidx.compose.material3.AlertDialog(
|
||||
onDismissRequest = { showPrivacyConfirmDialog = false },
|
||||
title = { Text("升级隐私权限") },
|
||||
text = { Text("首次切换智能体的公开/私有状态需要支付一次性费用。支付后可自由在公有/私有之间切换,后续不再扣费。") },
|
||||
confirmButton = {
|
||||
androidx.compose.material3.TextButton(
|
||||
onClick = {
|
||||
showPrivacyConfirmDialog = false
|
||||
scope.launch {
|
||||
try {
|
||||
viewModel.isPublic = false
|
||||
viewModel.updatePrompt(context)
|
||||
viewModel.paidForPrivacyEdit = true
|
||||
navController.navigateUp()
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message ?: "保存失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("确认支付")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
androidx.compose.material3.TextButton(
|
||||
onClick = { showPrivacyConfirmDialog = false }
|
||||
) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 错误提示
|
||||
errorMessage?.let { error ->
|
||||
LaunchedEffect(error) {
|
||||
kotlinx.coroutines.delay(3000)
|
||||
errorMessage = null
|
||||
}
|
||||
// TODO: 显示Toast或Snackbar
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddAgentMemoryButton(
|
||||
onAddMemoryClick: () -> Unit
|
||||
) {
|
||||
val appColors = LocalAppTheme.current
|
||||
|
||||
// 定义渐变边框颜色:紫色到蓝色
|
||||
val borderGradient = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF7C45ED), // 紫色
|
||||
Color(0xFF4A90E2) // 蓝色
|
||||
)
|
||||
)
|
||||
|
||||
// 浅紫色背景
|
||||
val lightPurpleBackground = Color(0xFFF5F0FF)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
// 使用两层Box来实现渐变边框效果
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(brush = borderGradient)
|
||||
.padding(1.5.dp) // 边框宽度
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(lightPurpleBackground)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp)
|
||||
.noRippleClickable {
|
||||
onAddMemoryClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// 脑部图标
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_brain_add),
|
||||
contentDescription = "添加智能体记忆",
|
||||
modifier = Modifier.size(20.dp),
|
||||
colorFilter = ColorFilter.tint(Color(0xFF7C45ED))
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
// 文字
|
||||
Text(
|
||||
text = "添加智能体记忆",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = Color(0xFF7C45ED)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
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.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
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空数据
|
||||
*/
|
||||
fun clearData() {
|
||||
chatAIId = ""
|
||||
title = ""
|
||||
desc = ""
|
||||
isPublic = true
|
||||
originalIsPublic = true
|
||||
paidForPrivacyEdit = false
|
||||
avatarUrl = null
|
||||
croppedBitmap = null
|
||||
isUpdating = false
|
||||
isLoading = false
|
||||
errorMessage = null
|
||||
isSelectingAvatar = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.CommentEntity
|
||||
import com.aiosman.ravenow.ui.composables.EditCommentBottomModal
|
||||
@@ -88,6 +89,7 @@ fun CommentModalContent(
|
||||
}
|
||||
)
|
||||
val commentViewModel = model.commentsViewModel
|
||||
val AppColors = LocalAppTheme.current
|
||||
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
@@ -99,10 +101,24 @@ fun CommentModalContent(
|
||||
var bottomPadding by remember { mutableStateOf(0.dp) }
|
||||
var softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
|
||||
var shouldAutoFocus by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(imePadding) {
|
||||
bottomPadding = imePadding.dp
|
||||
}
|
||||
|
||||
// 当设置回复评论时,自动聚焦到输入框
|
||||
LaunchedEffect(replyComment) {
|
||||
if (replyComment != null) {
|
||||
// 延迟一下,确保输入框已经渲染
|
||||
kotlinx.coroutines.delay(100)
|
||||
shouldAutoFocus = true
|
||||
// 请求显示键盘
|
||||
softwareKeyboardController?.show()
|
||||
} else {
|
||||
shouldAutoFocus = false
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
onDismiss()
|
||||
@@ -113,7 +129,7 @@ fun CommentModalContent(
|
||||
onDismissRequest = {
|
||||
showCommentMenu = false
|
||||
},
|
||||
containerColor = Color.White,
|
||||
containerColor = AppColors.background,
|
||||
sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
),
|
||||
@@ -152,12 +168,13 @@ fun CommentModalContent(
|
||||
stringResource(R.string.comment),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.text,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = Color(0xFFF7F7F7)
|
||||
color = AppColors.divider
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -169,7 +186,7 @@ fun CommentModalContent(
|
||||
Text(
|
||||
text = stringResource(id = R.string.comment_count, commentCount),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xff666666)
|
||||
color = AppColors.secondaryText
|
||||
)
|
||||
OrderSelectionComponent {
|
||||
commentViewModel.order = it
|
||||
@@ -193,7 +210,9 @@ fun CommentModalContent(
|
||||
|
||||
},
|
||||
onReply = { parentComment, _, _, _ ->
|
||||
|
||||
// 设置回复的评论,这样 EditCommentBottomModal 会显示回复输入框
|
||||
// CommentContent 内部已经处理了游客模式检查,所以这里直接设置即可
|
||||
replyComment = parentComment
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(72.dp))
|
||||
@@ -204,9 +223,12 @@ fun CommentModalContent(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xfff7f7f7))
|
||||
.background(AppColors.secondaryBackground)
|
||||
) {
|
||||
EditCommentBottomModal(
|
||||
replyComment = replyComment,
|
||||
autoFocus = shouldAutoFocus
|
||||
) {
|
||||
EditCommentBottomModal(replyComment) {
|
||||
commentViewModel.viewModelScope.launch {
|
||||
if (replyComment != null) {
|
||||
if (replyComment?.parentCommentId != null) {
|
||||
@@ -224,6 +246,13 @@ fun CommentModalContent(
|
||||
// 顶级评论
|
||||
commentViewModel.createComment(it)
|
||||
}
|
||||
// 评论创建成功后调用回调
|
||||
onCommentAdded()
|
||||
// 清空回复状态和自动聚焦状态
|
||||
replyComment = null
|
||||
shouldAutoFocus = false
|
||||
// 隐藏键盘
|
||||
softwareKeyboardController?.hide()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
@@ -60,10 +61,15 @@ fun EditCommentBottomModal(
|
||||
var navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val context = LocalContext.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
LaunchedEffect(autoFocus) {
|
||||
if (autoFocus) {
|
||||
// 延迟一下,确保输入框已经渲染完成
|
||||
kotlinx.coroutines.delay(150)
|
||||
focusRequester.requestFocus()
|
||||
// 显示键盘
|
||||
keyboardController?.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +89,7 @@ fun EditCommentBottomModal(
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(Color.Gray.copy(alpha = 0.1f))
|
||||
.background(AppColors.inputBackground)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
@@ -100,7 +106,7 @@ fun EditCommentBottomModal(
|
||||
.weight(1f)
|
||||
.focusRequester(focusRequester),
|
||||
textStyle = TextStyle(
|
||||
color = Color.Black,
|
||||
color = AppColors.text,
|
||||
fontWeight = FontWeight.Normal
|
||||
),
|
||||
decorationBox = { innerTextField ->
|
||||
|
||||
@@ -155,6 +155,17 @@ fun FavouriteListPage() {
|
||||
) {
|
||||
items(moments.itemCount) { idx ->
|
||||
val momentItem = moments[idx] ?: return@items
|
||||
// 获取缩略图URL:优先使用图片,如果没有图片则使用视频缩略图
|
||||
val thumbnailUrl = when {
|
||||
momentItem.images.isNotEmpty() -> momentItem.images[0].thumbnail
|
||||
momentItem.videos != null && momentItem.videos.isNotEmpty() -> {
|
||||
momentItem.videos.first().thumbnailUrl ?: momentItem.videos.first().thumbnailDirectUrl
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (thumbnailUrl == null) return@items
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -169,7 +180,7 @@ fun FavouriteListPage() {
|
||||
}
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
imageUrl = momentItem.images[0].thumbnail,
|
||||
imageUrl = thumbnailUrl,
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,13 +154,13 @@ fun ShortVideoScreen() {
|
||||
}
|
||||
},
|
||||
onCommentClick = { moment ->
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
|
||||
navController.navigate(NavigationRoute.Login.route)
|
||||
} else {
|
||||
// 点击评论图标只是打开评论弹窗,不应该增加评论数
|
||||
},
|
||||
onCommentAdded = { moment ->
|
||||
// 评论添加后的回调,更新评论数
|
||||
scope.launch {
|
||||
viewModel.onAddComment(moment.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFavoriteClick = { moment ->
|
||||
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
|
||||
@@ -178,6 +178,15 @@ fun ShortVideoScreen() {
|
||||
onShareClick = { moment ->
|
||||
// TODO: 实现分享功能
|
||||
},
|
||||
onAvatarClick = { moment ->
|
||||
// 点击头像进入用户界面
|
||||
navController.navigate(
|
||||
com.aiosman.ravenow.ui.NavigationRoute.AccountProfile.route.replace(
|
||||
"{id}",
|
||||
moment.authorId.toString()
|
||||
)
|
||||
)
|
||||
},
|
||||
onPageChanged = { idx -> currentIndex.value = idx }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -43,7 +44,9 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -67,6 +70,7 @@ import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.ui.comment.CommentModalContent
|
||||
@@ -75,6 +79,9 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// 激活状态的颜色(点赞/收藏时的红色)
|
||||
private val ActiveIconColor = Color(0xFFD80264)
|
||||
|
||||
@Composable
|
||||
fun ShortViewCompose(
|
||||
videoItemsUrl: List<String> = emptyList(),
|
||||
@@ -84,8 +91,10 @@ fun ShortViewCompose(
|
||||
videoBottom: @Composable ((MomentEntity) -> Unit)? = null,
|
||||
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
||||
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
||||
onCommentAdded: ((MomentEntity) -> Unit)? = null,
|
||||
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
|
||||
onShareClick: ((MomentEntity) -> Unit)? = null,
|
||||
onAvatarClick: ((MomentEntity) -> Unit)? = null,
|
||||
onPageChanged: ((Int) -> Unit)? = null
|
||||
) {
|
||||
// 优先使用 videoMoments,如果没有则使用 videoItemsUrl
|
||||
@@ -159,8 +168,10 @@ fun ShortViewCompose(
|
||||
VideoBottom = videoBottom,
|
||||
onLikeClick = onLikeClick,
|
||||
onCommentClick = onCommentClick,
|
||||
onCommentAdded = onCommentAdded,
|
||||
onFavoriteClick = onFavoriteClick,
|
||||
onShareClick = onShareClick
|
||||
onShareClick = onShareClick,
|
||||
onAvatarClick = onAvatarClick
|
||||
)
|
||||
}
|
||||
|
||||
@@ -183,8 +194,10 @@ private fun SingleVideoItemContent(
|
||||
VideoBottom: @Composable ((MomentEntity) -> Unit)? = null,
|
||||
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
||||
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
||||
onCommentAdded: ((MomentEntity) -> Unit)? = null,
|
||||
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
|
||||
onShareClick: ((MomentEntity) -> Unit)? = null
|
||||
onShareClick: ((MomentEntity) -> Unit)? = null,
|
||||
onAvatarClick: ((MomentEntity) -> Unit)? = null
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -199,8 +212,10 @@ private fun SingleVideoItemContent(
|
||||
pauseIconVisibleState = pauseIconVisibleState,
|
||||
onLikeClick = onLikeClick,
|
||||
onCommentClick = onCommentClick,
|
||||
onCommentAdded = onCommentAdded,
|
||||
onFavoriteClick = onFavoriteClick,
|
||||
onShareClick = onShareClick
|
||||
onShareClick = onShareClick,
|
||||
onAvatarClick = onAvatarClick
|
||||
)
|
||||
VideoHeader.invoke()
|
||||
if (moment != null && VideoBottom != null) {
|
||||
@@ -228,12 +243,17 @@ fun VideoPlayer(
|
||||
pauseIconVisibleState: MutableState<Boolean>,
|
||||
onLikeClick: ((MomentEntity) -> Unit)? = null,
|
||||
onCommentClick: ((MomentEntity) -> Unit)? = null,
|
||||
onCommentAdded: ((MomentEntity) -> Unit)? = null,
|
||||
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
|
||||
onShareClick: ((MomentEntity) -> Unit)? = null,
|
||||
onAvatarClick: ((MomentEntity) -> Unit)? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val configuration = androidx.compose.ui.platform.LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp.dp
|
||||
val sheetHeight = screenHeight * 0.7f // 屏幕的一大半高度
|
||||
var showCommentModal by remember { mutableStateOf(false) }
|
||||
var sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
@@ -371,42 +391,51 @@ fun VideoPlayer(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (moment != null) {
|
||||
UserAvatar(avatarUrl = moment.avatar)
|
||||
// 使用 key 确保状态变化时重新组合
|
||||
androidx.compose.runtime.key(moment.id, moment.isFavorite) {
|
||||
UserAvatar(
|
||||
avatarUrl = moment.avatar,
|
||||
onClick = { onAvatarClick?.invoke(moment) }
|
||||
)
|
||||
VideoBtn(
|
||||
icon = R.drawable.rider_pro_video_like,
|
||||
text = formatCount(moment.likeCount)
|
||||
) {
|
||||
moment?.let { onLikeClick?.invoke(it) }
|
||||
}
|
||||
icon = if (moment.liked) R.drawable.rider_pro_moment_liked else R.drawable.rider_pro_moment_like,
|
||||
text = formatCount(moment.likeCount),
|
||||
isActive = moment.liked,
|
||||
onClick = { onLikeClick?.invoke(moment) }
|
||||
)
|
||||
VideoBtn(
|
||||
icon = R.drawable.rider_pro_video_comment,
|
||||
text = formatCount(moment.commentCount)
|
||||
) {
|
||||
moment?.let {
|
||||
icon = R.mipmap.icon_comment,
|
||||
text = formatCount(moment.commentCount),
|
||||
onClick = {
|
||||
showCommentModal = true
|
||||
onCommentClick?.invoke(it)
|
||||
}
|
||||
onCommentClick?.invoke(moment)
|
||||
}
|
||||
)
|
||||
VideoBtn(
|
||||
icon = R.drawable.rider_pro_video_favor,
|
||||
text = formatCount(moment.favoriteCount)
|
||||
) {
|
||||
moment?.let { onFavoriteClick?.invoke(it) }
|
||||
}
|
||||
icon = if (moment.isFavorite) R.mipmap.icon_variant_2 else R.mipmap.icon_collect,
|
||||
text = formatCount(moment.favoriteCount),
|
||||
isActive = false, // 收藏后不使用红色滤镜,保持图标原本颜色
|
||||
keepOriginalColor = moment.isFavorite, // 收藏后保持原始颜色
|
||||
onClick = { onFavoriteClick?.invoke(moment) }
|
||||
)
|
||||
VideoBtn(
|
||||
icon = R.drawable.rider_pro_video_share,
|
||||
text = formatCount(moment.shareCount)
|
||||
) {
|
||||
moment?.let { onShareClick?.invoke(it) }
|
||||
icon = R.mipmap.icon_share,
|
||||
text = formatCount(moment.shareCount),
|
||||
onClick = { onShareClick?.invoke(moment) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
UserAvatar()
|
||||
VideoBtn(icon = R.drawable.rider_pro_video_like, text = "0")
|
||||
VideoBtn(icon = R.drawable.rider_pro_video_comment, text = "0") {
|
||||
VideoBtn(icon = R.drawable.rider_pro_moment_like, text = "0")
|
||||
VideoBtn(
|
||||
icon = R.mipmap.icon_comment,
|
||||
text = "0",
|
||||
onClick = {
|
||||
showCommentModal = true
|
||||
}
|
||||
VideoBtn(icon = R.drawable.rider_pro_video_favor, text = "0")
|
||||
VideoBtn(icon = R.drawable.rider_pro_video_share, text = "0")
|
||||
)
|
||||
VideoBtn(icon = R.mipmap.icon_collect, text = "0")
|
||||
VideoBtn(icon = R.mipmap.icon_share, text = "0")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,26 +494,44 @@ fun VideoPlayer(
|
||||
}
|
||||
|
||||
if (showCommentModal && moment != null) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showCommentModal = false },
|
||||
containerColor = Color.White,
|
||||
sheetState = sheetState
|
||||
containerColor = AppColors.background,
|
||||
sheetState = sheetState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(sheetHeight)
|
||||
) {
|
||||
CommentModalContent(postId = moment.id) {
|
||||
|
||||
CommentModalContent(
|
||||
postId = moment.id,
|
||||
commentCount = moment.commentCount,
|
||||
onCommentAdded = {
|
||||
onCommentAdded?.invoke(moment)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserAvatar(avatarUrl: String? = null) {
|
||||
fun UserAvatar(
|
||||
avatarUrl: String? = null,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.size(40.dp)
|
||||
.border(width = 3.dp, color = Color.White, shape = RoundedCornerShape(40.dp))
|
||||
.clip(RoundedCornerShape(40.dp))
|
||||
.then(
|
||||
if (onClick != null) {
|
||||
Modifier.noRippleClickable { onClick() }
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
) {
|
||||
if (avatarUrl != null && avatarUrl.isNotEmpty()) {
|
||||
CustomAsyncImage(
|
||||
@@ -512,19 +559,31 @@ private fun formatCount(count: Int): String {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoBtn(@DrawableRes icon: Int, text: String, onClick: (() -> Unit)? = null) {
|
||||
fun VideoBtn(
|
||||
@DrawableRes icon: Int,
|
||||
text: String,
|
||||
onClick: (() -> Unit)? = null,
|
||||
isActive: Boolean = false,
|
||||
keepOriginalColor: Boolean = false // 是否保持原始颜色(不应用白色滤镜)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.clickable {
|
||||
.noRippleClickable {
|
||||
onClick?.invoke()
|
||||
},
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(36.dp),
|
||||
modifier = Modifier.size(30.dp),
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = ""
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.FillBounds, // 填满容器,让图标看起来更大
|
||||
colorFilter = when {
|
||||
isActive -> ColorFilter.tint(ActiveIconColor)
|
||||
keepOriginalColor -> null // 保持原始颜色
|
||||
else -> ColorFilter.tint(Color.White) // 未激活状态时图标为白色
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
|
||||
@@ -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/ts_dsp_z_wxz_btn.png
Normal file
|
After Width: | Height: | Size: 970 B |
BIN
app/src/main/res/mipmap-hdpi/ts_dsp_z_xz_btn.png
Normal file
|
After Width: | Height: | Size: 423 B |
BIN
app/src/main/res/mipmap-mdpi/ts_dsp_z_wxz_btn.png
Normal file
|
After Width: | Height: | Size: 657 B |
BIN
app/src/main/res/mipmap-mdpi/ts_dsp_z_xz_btn.png
Normal file
|
After Width: | Height: | Size: 333 B |
BIN
app/src/main/res/mipmap-xhdpi/ts_dsp_z_wxz_btn.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ts_dsp_z_xz_btn.png
Normal file
|
After Width: | Height: | Size: 482 B |
BIN
app/src/main/res/mipmap-xxhdpi/ts_dsp_z_wxz_btn.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ts_dsp_z_xz_btn.png
Normal file
|
After Width: | Height: | Size: 647 B |
BIN
app/src/main/res/mipmap-xxxhdpi/ts_dsp_z_wxz_btn.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ts_dsp_z_xz_btn.png
Normal file
|
After Width: | Height: | Size: 805 B |
@@ -368,6 +368,14 @@
|
||||
<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="connect_world_start_following">连接世界,从关注开始</string>
|
||||
<string name="why_not_start_with_agent">不如从一个 Agent 开始认识这世界?</string>
|
||||
<string name="explore">去探索</string>
|
||||
|
||||
@@ -361,6 +361,14 @@
|
||||
<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="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>
|
||||
|
||||