接入声优接口

This commit is contained in:
renhaoting 2025-11-06 16:45:09 +08:00
parent 2d967c52a0
commit 063a8bca04
17 changed files with 116 additions and 750 deletions

View File

@ -5,22 +5,17 @@ import com.remax.visualnovel.entity.response.basenew.ResponseNew
import com.remax.visualnovel.entity.request.ParamActorList
import com.remax.visualnovel.entity.request.ParamActorTag
import com.remax.visualnovel.entity.response.ActorBean
import com.remax.visualnovel.entity.response.ChatSound
import com.remax.visualnovel.widget.custom.ActorTag
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
interface ActorsService {
@POST(BuildConfig.API_BASE + "/tag/selectByCondition")
@POST(BuildConfig.API_BASE + "/tag/list")
suspend fun requestActorTags(@Body param: ParamActorTag): ResponseNew<List<ActorTag>>
@POST(BuildConfig.API_BASE + "/character/select/list")
@POST(BuildConfig.API_BASE + "/character/list")
suspend fun requestActorList(@Body param: ParamActorList): ResponseNew<List<ActorBean>>
@GET(BuildConfig.API_BASE + "/tts/config/select/list")
suspend fun requestSoundList(): ResponseNew<List<ChatSound>>
}

View File

@ -8,6 +8,7 @@ import com.remax.visualnovel.entity.request.ChatAlbum
import com.remax.visualnovel.entity.request.ChatSetting
import com.remax.visualnovel.entity.request.HeartbeatBuy
import com.remax.visualnovel.entity.request.ParamBgUpload
import com.remax.visualnovel.entity.request.ParamSoundList
import com.remax.visualnovel.entity.request.RTCRequest
import com.remax.visualnovel.entity.request.SearchPage
import com.remax.visualnovel.entity.request.SimpleDataDTO
@ -24,6 +25,7 @@ import com.remax.visualnovel.entity.response.Pageable
import com.remax.visualnovel.entity.response.Token
import com.remax.visualnovel.entity.response.VoiceASR
import com.remax.visualnovel.entity.response.base.Response
import com.remax.visualnovel.entity.response.basenew.ResponseNew
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
@ -93,7 +95,7 @@ interface ChatService {
* 获取聊天设置
*/
@POST("/web/chat-set/get-my")
suspend fun getChatSetting(@Body request: ChatSetting): Response<ChatSet>
suspend fun getChatSetting(@Body request: ChatSetting): ResponseNew<ChatSet>
/**
* 修改聊天设置
@ -111,7 +113,7 @@ interface ChatService {
* 修改聊天模型
*/
@POST("/web/chat-set/set-chat-model")
suspend fun setChatModel(@Body request: ChatSetting): Response<Any>
suspend fun setChatModel(@Body request: ChatSetting): ResponseNew<Any>
/**
* 修改是否自动播放语音
@ -141,7 +143,7 @@ interface ChatService {
* 语音转文本
*/
@POST(BuildConfig.API_COW + "/web/voice/asr-v2")
suspend fun voiceASR(@Body request: SimpleDataDTO): Response<VoiceASR>
suspend fun voiceASR(@Body request: SimpleDataDTO): ResponseNew<VoiceASR>
/**
* 生成语音
@ -165,13 +167,16 @@ interface ChatService {
//------------------------------------------------------
@GET(BuildConfig.API_BASE + "/tts/config/select/list")
suspend fun loadSoundList(@Query("language") language: Int = 1): Response<List<ChatSound>>
@POST(BuildConfig.API_BASE + "/tts/config/list")
suspend fun requestSoundList(@Body param: ParamSoundList): ResponseNew<List<ChatSound>>
@GET(BuildConfig.API_BASE + "/model/config/select/list")
suspend fun loadAiModelList(@Query("language") language: Int = 1): Response<List<ChatModel>>
@POST(BuildConfig.API_BASE + "/model/config/list")
suspend fun requestAiModelList(@Body language: Int = 1): ResponseNew<List<ChatModel>>
@POST(BuildConfig.API_BASE + "/bg_image/config/upload")
suspend fun uploadCustomBgPic(@Body param: ParamBgUpload): Response<Any>
suspend fun uploadCustomBgPic(@Body param: ParamBgUpload): ResponseNew<Any>
@POST(BuildConfig.API_BASE + "/bg_image/config/list")
suspend fun requestChatBgList(@Body userId: Int): ResponseNew<List<ChatBackgroundBase.ChatBackground>>
}

View File

@ -9,6 +9,7 @@ import com.remax.visualnovel.entity.response.ChatBubble
import com.remax.visualnovel.entity.response.ChatModel
import com.remax.visualnovel.entity.response.Pageable
import com.remax.visualnovel.entity.response.base.Response
import com.remax.visualnovel.entity.response.basenew.ResponseNew
import retrofit2.http.Body
import retrofit2.http.POST
@ -30,11 +31,11 @@ interface DictService {
* 礼物字典
*/
@POST("/web/gift/dict-list")
suspend fun getGiftDict(@Body pageQuery: PageQuery = PageQuery(1).apply { page.ps = 100 }): Response<Pageable<Gift>>
suspend fun getGiftDict(@Body pageQuery: PageQuery = PageQuery(1).apply { page.ps = 100 }): ResponseNew<Pageable<Gift>>
/**
* chat模型
*/
@POST("/web/chat-model/dict-list")
suspend fun getAIChatModel(): Response<List<ChatModel>>
suspend fun getAIChatModel(): ResponseNew<List<ChatModel>>
}

View File

@ -0,0 +1,7 @@
package com.remax.visualnovel.entity.request
data class ParamSoundList(
var language: Int = 1,
var gender: Int? = null
)

View File

@ -13,11 +13,12 @@ data class ChatSound(
val rules: Int = 0,
val nameLanguage: String = "",
val headPortrait: String = "",
// Other needed
var sampleUrl: String = "",
var isSelected: Boolean = false,
var isPlaying: Boolean = false,
// Other needed
var sampleUrl: String = ""
) : Parcelable {
}

View File

@ -16,4 +16,5 @@ class ActorsRepository @Inject constructor(private val mActorsService: ActorsSer
mActorsService.requestActorList(param)
}
}

View File

@ -1,137 +1,50 @@
package com.remax.visualnovel.repository.api
import com.pengxr.modular.eventbus.generated.events.EventDefineOfChatSettingEvents
import com.pengxr.modular.eventbus.generated.events.EventDefineOfUserAIEvents
import com.pengxr.modular.eventbus.generated.events.EventDefineOfUserEvents
import com.remax.visualnovel.api.service.ChatService
import com.remax.visualnovel.entity.request.AIFeedback
import com.remax.visualnovel.entity.request.AIIDRequest
import com.remax.visualnovel.entity.request.AIIsShowDTO
import com.remax.visualnovel.entity.request.ChatAlbum
import com.remax.visualnovel.entity.request.ChatSetting
import com.remax.visualnovel.entity.request.HeartbeatBuy
import com.remax.visualnovel.entity.request.RTCRequest
import com.remax.visualnovel.entity.request.SearchPage
import com.remax.visualnovel.entity.request.ParamBgUpload
import com.remax.visualnovel.entity.request.ParamSoundList
import com.remax.visualnovel.entity.request.SimpleDataDTO
import com.remax.visualnovel.entity.request.VoiceTTS
import com.remax.visualnovel.entity.response.Character
import com.remax.visualnovel.entity.response.ChatSet
import com.remax.visualnovel.repository.api.base.BaseRepository
import com.remax.visualnovel.ui.chat.message.events.model.ChatSetAutoPlayEvent
import com.remax.visualnovel.ui.chat.message.events.model.ChatSetBackgroundEvent
import com.remax.visualnovel.repository.api.base.BaseRepositoryNew
import com.remax.visualnovel.ui.wallet.manager.WalletManager
import javax.inject.Inject
/**
* Created by HJW on 2022/11/15
*/
class ChatRepository @Inject constructor(private val chatService: ChatService) : BaseRepository() {
suspend fun sendDialogueMsg(aiId: String) = executeHttp {
chatService.sendDialogueMsg(AIIDRequest(aiId))
}
suspend fun viewAlbumImg(request: ChatAlbum) = executeHttp {
chatService.viewAlbumImg(request)
}
suspend fun getIMAICharacterProfile(aiId: String) = executeHttp {
chatService.getIMAICharacterProfile(Character(aiId = aiId))
}
suspend fun getMyFriends(request: SearchPage) = executeHttp {
chatService.getMyFriends(request)
}
suspend fun getMyFriendRank() = executeHttp {
chatService.getMyFriendRank()
}
suspend fun getPrompts(aiId: String) = executeHttp {
chatService.getPrompts(AIIDRequest(aiId))
}.transformResult({
WalletManager.refreshWallet()
})
suspend fun aiFeedback(request: AIFeedback) = executeHttp {
chatService.aiFeedback(request)
}
suspend fun getRTCToken(roomId: String) = executeHttp {
chatService.getRTCToken(RTCRequest(roomId))
}
suspend fun voiceChatOpt(request: RTCRequest) = executeHttp {
chatService.voiceChatOpt(request)
}
suspend fun getChatBackgroundList(aiId: String) = executeHttp {
chatService.getChatBackgroundList(AIIDRequest(aiId))
}
suspend fun getChatSetting(aiId: String?) = executeHttp {
chatService.getChatSetting(ChatSetting(aiId))
}
suspend fun setChatSetting(request: ChatSet) = executeHttp {
chatService.setChatSetting(request)
}.transformResult({
EventDefineOfUserEvents.onUserInfoChanged().post(null)
})
suspend fun setChatBubble(request: ChatSetting) = executeHttp {
chatService.setChatBubble(request)
}
class ChatRepository @Inject constructor(private val chatService: ChatService) : BaseRepositoryNew() {
suspend fun setChatModel(request: ChatSetting) = executeHttp {
chatService.setChatModel(request)
}
suspend fun setChatAutoPlay(request: ChatSetting) = executeHttp {
chatService.setChatAutoPlay(request)
}.transformResult({
EventDefineOfChatSettingEvents.settingChanged()
.post(ChatSetAutoPlayEvent(request.aiId ?: "", if (request.isAutoPlayVoice == true) 1 else 0))
})
suspend fun setChatBackground(request: ChatSetting) = executeHttp {
chatService.setChatBackground(request)
}.transformResult({
EventDefineOfChatSettingEvents.settingChanged()
.post(ChatSetBackgroundEvent(request.aiId ?: "", request.backgroundImg))
})
suspend fun deleteChatBackground(request: ChatSetting) = executeHttp {
chatService.deleteChatBackground(request)
suspend fun getChatSetting(aiId: String?) = executeHttp {
chatService.getChatSetting(ChatSetting(aiId))
}
suspend fun relationSwitch(request: AIIsShowDTO) = executeHttp {
chatService.relationSwitch(request)
}.transformResult({
EventDefineOfUserAIEvents.onAIHeartIsOpenChanged().post(request)
})
suspend fun voiceASR(aiId: String, url: String?) = executeHttp {
chatService.voiceASR(SimpleDataDTO(aiId = aiId, url = url))
}.transformResult({
WalletManager.refreshWallet()
})
suspend fun voiceTTS(request: VoiceTTS) = executeHttp {
chatService.voiceTTS(request)
// ------------------------ new ---------------------------------
suspend fun getSoundList(param: ParamSoundList) = executeHttp {
chatService.requestSoundList(param)
}
suspend fun getHeartbeatLevel(aiId: String) = executeHttp {
chatService.getHeartbeatLevel(Character(aiId = aiId))
suspend fun getAiModelList(language: Int = 1) = executeHttp {
chatService.requestAiModelList(language)
}
suspend fun buyHeartbeatVal(request: HeartbeatBuy) = executeHttp {
chatService.buyHeartbeatVal(request)
suspend fun uploadCustomBgPic(param: ParamBgUpload) = executeHttp {
chatService.uploadCustomBgPic(param)
}
suspend fun loadSoundList(lang: Int) = executeHttp {
chatService.loadSoundList(lang)
suspend fun getChatBgList(userId: Int) = executeHttp {
chatService.requestChatBgList(userId)
}
}

View File

@ -2,22 +2,13 @@ package com.remax.visualnovel.repository.api
import com.remax.visualnovel.api.service.DictService
import com.remax.visualnovel.entity.request.AIIDRequest
import com.remax.visualnovel.repository.api.base.BaseRepository
import com.remax.visualnovel.repository.api.base.BaseRepositoryNew
import javax.inject.Inject
/**
* Created by HJW on 2022/11/15
*/
class DictRepository @Inject constructor(private val dictService: DictService) : BaseRepository() {
suspend fun getChatBubbleList(aiId: String) = executeHttp {
dictService.getChatBubbleList(AIIDRequest(aiId))
}
class DictRepository @Inject constructor(private val dictService: DictService) : BaseRepositoryNew() {
suspend fun getAIDict() = executeHttp {
dictService.getAIDict()
}
suspend fun getGiftDict() = executeHttp(false) { dictService.getGiftDict() }

View File

@ -23,10 +23,12 @@ import com.pengxr.modular.eventbus.generated.events.EventDefineOfUserEvents
import com.remax.visualnovel.R
import com.remax.visualnovel.databinding.ActivityActorChatBinding
import com.remax.visualnovel.entity.request.ChatSetting
import com.remax.visualnovel.entity.request.ParamSoundList
import com.remax.visualnovel.event.model.OnLoginEvent
import com.remax.visualnovel.extension.countDownCoroutines
import com.remax.visualnovel.extension.launchAndCollect
import com.remax.visualnovel.extension.launchAndCollect2
import com.remax.visualnovel.extension.launchAndLoadingCollect
import com.remax.visualnovel.extension.launchAndLoadingCollect2
import com.remax.visualnovel.extension.launchWithRequest
import com.remax.visualnovel.extension.setMargin
import com.remax.visualnovel.extension.toast
@ -71,12 +73,16 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
}
override fun initData() {
loadSoundDatas()
loadSoundDatas(null)
}
private fun loadSoundDatas() {
launchAndCollect({
mViewModel.loadSoundList(LanguageUtil.instance().getCurrentLanguageCode())
private fun loadSoundDatas(gender: Int?) {
launchAndCollect2({
val requestParam = ParamSoundList().apply {
this.gender = gender
this.language = LanguageUtil.instance().getCurrentLanguageCode()
}
mViewModel.loadSoundList(requestParam)
}) {
onSuccess = {
val dataList = it?: emptyList()
@ -87,7 +93,6 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
}
}
}
private val loginObserver = Observer<OnLoginEvent?> {
@ -227,7 +232,7 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
modelDialog.build(chatModels ?: emptyList(), chatSet?.modelCode, {
}) { model ->
launchAndLoadingCollect({
launchAndLoadingCollect2({
setChatModel(
ChatSetting(aiId, model.code)
)
@ -241,7 +246,7 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
}
if (chatModels.isNullOrEmpty()) {
launchAndLoadingCollect({
launchAndLoadingCollect2({
mViewModel.getChatModels()
}) {
onSuccess = {

View File

@ -1,58 +1,25 @@
package com.remax.visualnovel.ui.chat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.remax.visualnovel.app.viewmodel.base.OssViewModel
import com.remax.visualnovel.entity.imbean.IMAIInMessage
import com.remax.visualnovel.entity.imbean.IMMessageWrapper
import com.remax.visualnovel.entity.imbean.raw.CustomRawData
import com.remax.visualnovel.entity.imbean.raw.CustomScoreData
import com.remax.visualnovel.entity.request.AIFeedback
import com.remax.visualnovel.entity.request.AlbumDTO
import com.remax.visualnovel.entity.request.ChatAlbum
import com.remax.visualnovel.entity.request.ChatSetting
import com.remax.visualnovel.entity.request.Gift
import com.remax.visualnovel.entity.request.S3TypeDTO
import com.remax.visualnovel.entity.request.SendGift
import com.remax.visualnovel.entity.response.Album
import com.remax.visualnovel.entity.response.BucketBean
import com.remax.visualnovel.entity.response.Character
import com.remax.visualnovel.entity.response.ChatModel
import com.remax.visualnovel.entity.response.ChatSet
import com.remax.visualnovel.entity.response.HeartbeatLevelEnum
import com.remax.visualnovel.entity.response.VoiceASR
import com.remax.visualnovel.entity.response.base.ApiEmptyResponse
import com.remax.visualnovel.entity.response.base.ApiFailedResponse
import com.remax.visualnovel.entity.response.base.Response
import com.remax.visualnovel.extension.convertFromJson
import com.remax.visualnovel.manager.nim.FetchResult
import com.remax.visualnovel.manager.nim.LoadStatus
import com.remax.visualnovel.manager.nim.NimManager
import com.remax.visualnovel.repository.api.AIRepository
import com.remax.visualnovel.repository.api.ChatRepository
import com.remax.visualnovel.repository.api.DictRepository
import com.remax.visualnovel.repository.api.MessageRepository
import com.remax.visualnovel.repository.ext.convertMessage
import com.remax.visualnovel.ui.chat.message.call.manager.RTCManager
import com.google.gson.Gson
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum
import com.netease.nimlib.sdk.v2.message.V2NIMClearHistoryNotification
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
import com.netease.nimlib.sdk.v2.message.V2NIMMessageCreator
import com.netease.nimlib.sdk.v2.message.V2NIMMessageDeletedNotification
import com.netease.nimlib.sdk.v2.message.V2NIMMessageListener
import com.netease.nimlib.sdk.v2.message.V2NIMMessagePinNotification
import com.netease.nimlib.sdk.v2.message.V2NIMMessageQuickCommentNotification
import com.netease.nimlib.sdk.v2.message.V2NIMMessageRevokeNotification
import com.netease.nimlib.sdk.v2.message.V2NIMP2PMessageReadReceipt
import com.netease.nimlib.sdk.v2.message.V2NIMTeamMessageReadReceipt
import com.netease.nimlib.sdk.v2.utils.V2NIMConversationIdUtil
import com.remax.visualnovel.manager.gift.GiftManager
import com.remax.visualnovel.ui.chat.message.detail.flirting.FlirtingLevelActivity
import com.remax.visualnovel.entity.request.ParamSoundList
import com.remax.visualnovel.entity.response.BucketBean
import com.remax.visualnovel.entity.response.Character
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -64,34 +31,11 @@ class ChatViewModel @Inject constructor(
private val aiRepository: AIRepository,
) : OssViewModel() {
var aiId: String = ""
private var bucketBean: BucketBean? = null
var character: Character? = null
private set
val conversationId: String
get() = V2NIMConversationIdUtil.conversationId(character?.nimAccountId, SessionTypeEnum.P2P) ?: ""
private val _aiBaseInfoFlow = MutableLiveData<Pair<Boolean, Character?>>()
val aiBaseInfoFlow = _aiBaseInfoFlow
fun checkHeartbeatLevel(targetLevel: HeartbeatLevelEnum, checkSuccess: () -> Unit) {
val currLevel = character?.aiUserHeartbeatRelation?.currHeartbeatEnum
if ((currLevel?.level ?: 0) >= targetLevel.level) {
checkSuccess()
} else {
FlirtingLevelActivity.start(aiId)
}
}
suspend fun refreshAIBaseInfo(enterPage: Boolean) = chatRepository.getIMAICharacterProfile(aiId).transformResult({
character = it
NimManager.getUserList(listOf(character?.nimAccountId))
_aiBaseInfoFlow.value = Pair(enterPage, it)
getChatSetting()
getChatModels()
})
var aiId: String = ""
var chatModels: List<ChatModel>? = null
private set
@ -111,9 +55,20 @@ class ChatViewModel @Inject constructor(
})
/**
* 云信发送消息raw代表是自定义消息
*/
suspend fun voiceASR(filePath: String): Response<VoiceASR> {
/*ossUploadFile(filePath, S3TypeDTO.SOUND, isImg = false, token = bucketBean).transformResult({
return chatRepository.voiceASR(aiId, it?.urlPath)
}) {
return Response.createZipFailResponse(it)
}*/
return ApiEmptyResponse()
}
fun sendMsg(msgContent: String, raw: CustomRawData? = null, errorCallback: (Int) -> Unit) {
val v2Message = if (raw != null)
@ -126,238 +81,11 @@ class ChatViewModel @Inject constructor(
}
}
// 消息发送LiveData本地发送消息通过该LiveData通知UI
private val _sendScoreLiveData = MutableLiveData<Double?>()
val sendScoreLiveData: LiveData<Double?> = _sendScoreLiveData
/**
* 接受方添加消息接收回调
*/
private val messageListener = object : V2NIMMessageListener {
/**
* 消息接收
*/
override fun onReceiveMessages(messages: List<V2NIMMessage>) {
NimManager.log("消息接收回调 onReceiveMessages")
NimManager.clearUnreadCountByIds(conversationId)
messages.firstOrNull()?.let { message ->
if (message.conversationId == conversationId) {
/**
* message中的分数
* 本地实时加减一下
* AI发送的文本消息和语音通话时都会有serverExtension:{"score":0.1}的计算
*/
message.serverExtension?.convertFromJson<CustomScoreData>()?.let {
_sendScoreLiveData.value = it.score
RTCManager.sendIMScoreMessage(it.score)
}
val messageRecFetchResult = FetchResult<IMMessageWrapper>(LoadStatus.Success)
message.convertMessage()?.let { messageWrapper ->
if (messageWrapper.type == IMMessageWrapper.IN_TEXT_TYPE) {
/**
* 处理自动播放
*/
if (character?.isAutoPlayVoice == 1) {
(messageWrapper as? IMAIInMessage)?.imVoice?.autoPlay = true
}
}
messageRecFetchResult.data = messageWrapper
messageRecFetchResult.type = messageWrapper.fetchType
messageRecFetchResult.typeIndex = -1
_sendMessageLiveData.value = messageRecFetchResult
}
}
}
}
override fun onReceiveP2PMessageReadReceipts(readReceipts: List<V2NIMP2PMessageReadReceipt?>?) {
}
override fun onReceiveTeamMessageReadReceipts(readReceipts: List<V2NIMTeamMessageReadReceipt?>?) {
}
override fun onMessageRevokeNotifications(revokeNotifications: List<V2NIMMessageRevokeNotification?>?) {
}
override fun onMessagePinNotification(pinNotification: V2NIMMessagePinNotification?) {
}
override fun onMessageQuickCommentNotification(quickCommentNotification: V2NIMMessageQuickCommentNotification?) {
}
override fun onMessageDeletedNotifications(messageDeletedNotifications: List<V2NIMMessageDeletedNotification?>?) {
}
override fun onClearHistoryNotifications(clearHistoryNotifications: List<V2NIMClearHistoryNotification?>?) {
}
/**
* 本端发送消息状态回调
*/
override fun onSendMessage(message: V2NIMMessage) {
val sendingState = message.sendingState
NimManager.log("本端发送消息 $message")
NimManager.log("本端发送消息状态 $sendingState")
/**
* 发消息时需要刷新一下调用推荐回复接口
*/
refreshPrompts = true
if (message.conversationId == conversationId) {
postMessageSend(message)
}
}
override fun onReceiveMessagesModified(messages: List<V2NIMMessage>) {
NimManager.log("收到更新的消息 $messages")
messages.firstOrNull()?.let { message ->
NimManager.log("收到更新的消息 conversationId:${message.conversationId}")
if (message.conversationId == conversationId) {
val messageRecFetchResult = FetchResult<IMMessageWrapper>(LoadStatus.Success)
messageRecFetchResult.data = message.convertMessage()
messageRecFetchResult.type = FetchResult.FetchType.Update
messageRecFetchResult.typeIndex = -1
_sendMessageLiveData.value = messageRecFetchResult
}
}
}
}
fun updateMsg(message: V2NIMMessage) {
val messageRecFetchResult = FetchResult<IMMessageWrapper>(LoadStatus.Success)
messageRecFetchResult.data = message.convertMessage()
messageRecFetchResult.type = FetchResult.FetchType.Update
messageRecFetchResult.typeIndex = -1
_sendMessageLiveData.value = messageRecFetchResult
}
fun aiFeedback(request: AIFeedback) {
viewModelScope.launch {
chatRepository.aiFeedback(request)
}
}
// 消息发送LiveData本地发送消息通过该LiveData通知UI
private val _sendMessageLiveData = MutableLiveData<FetchResult<IMMessageWrapper>?>()
val sendMessageLiveData: LiveData<FetchResult<IMMessageWrapper>?> = _sendMessageLiveData
private val sendMessageFetchResult by lazy {
FetchResult<IMMessageWrapper>(LoadStatus.Finish)
}
// 同步发送消息
private fun postMessageSend(message: V2NIMMessage) {
message.convertMessage()?.let { messageWrapper ->
sendMessageFetchResult.loadStatus = LoadStatus.Success
sendMessageFetchResult.type = messageWrapper.fetchType
sendMessageFetchResult.data = messageWrapper
_sendMessageLiveData.value = sendMessageFetchResult
}
}
fun addMessageListener() {
messageRepository.setMessageListener(true, messageListener)
}
private var bucketBean: BucketBean? = null
override fun onStart() {
viewModelScope.launch {
getBucketToken("mp3", S3TypeDTO.SOUND_PATH).transformResult({
bucketBean = it
})
}
}
override fun onDestroy() {
messageRepository.setMessageListener(false, messageListener)
NimManager.clearUnreadCountByIds(conversationId)
GiftManager.initSelect()
}
private var anchorMessage: V2NIMMessage? = null
/**
* 获取历史消息
*/
suspend fun getMessageList(isRefresh: Boolean): FetchResult<List<IMMessageWrapper>> {
if (isRefresh) anchorMessage = null
val res = messageRepository.getMessageList(conversationId, anchorMessage, character)
// if (isRefresh && res.data?.size == 1) {
// sendDialogueMsg()
// }
anchorMessage = res.extraInfo as? V2NIMMessage
return res
}
suspend fun voiceASR(filePath: String): Response<VoiceASR> {
ossUploadFile(filePath, S3TypeDTO.SOUND, isImg = false, token = bucketBean).transformResult({
return chatRepository.voiceASR(aiId, it?.urlPath)
}) {
return Response.createZipFailResponse(it)
}
return ApiEmptyResponse()
}
suspend fun sendGift(request: SendGift, gift: Gift): Response<Any> {
return messageRepository.sendGift(request, gift)
}
var refreshPrompts = true
private set
suspend fun getPrompts(): Response<List<String>> {
return if (refreshPrompts) {
refreshPrompts = false
chatRepository.getPrompts(aiId).transformResult {
// 失败的话需要重新拉
refreshPrompts = true
}
} else {
ApiFailedResponse()
}
}
suspend fun viewAlbumImg(request: ChatAlbum) = chatRepository.viewAlbumImg(request)
suspend fun unlockAlbum(request: ChatAlbum) = aiRepository.unlockAlbum(request)
fun sendDialogueMsg() {
viewModelScope.launch {
chatRepository.sendDialogueMsg(aiId)
}
}
suspend fun changeLiked(): Response<Any> {
val isLiked = character?.liked == true
val request = AlbumDTO(
aiId = aiId,
likedStatus = if (isLiked) Album.LIKED else Album.CANCELED,
liked = isLiked
)
return aiRepository.setAILikeOrCancel(request).transformResult({
changeCharacterLiked(isLiked)
})
}
fun changeCharacterLiked(isLiked: Boolean) {
character?.apply {
liked = isLiked
likedNum = if (isLiked) {
likedNum?.plus(1)
} else {
likedNum?.minus(1)
}
}
}
//------------------------ new ------------------------
suspend fun loadSoundList(lang: Int) = chatRepository.loadSoundList(lang)
suspend fun loadSoundList(param: ParamSoundList) = chatRepository.getSoundList(param)

View File

@ -1,248 +0,0 @@
package com.remax.visualnovel.ui.chat.message.detail.flirting
/**
* Created by HJW on 2025/8/16
*/
import android.annotation.SuppressLint
import android.view.View
import androidx.activity.viewModels
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.airbnb.lottie.LottieAnimationView
import com.alibaba.android.arouter.facade.annotation.Autowired
import com.alibaba.android.arouter.facade.annotation.Route
import com.alibaba.android.arouter.launcher.ARouter
import com.remax.visualnovel.R
import com.remax.visualnovel.app.base.BaseBindingActivity
import com.remax.visualnovel.app.delegate.isFull
import com.remax.visualnovel.app.delegate.titleTextAlpha
import com.remax.visualnovel.app.widget.tips.TipsSwitchWindow
import com.remax.visualnovel.entity.request.AIIsShowDTO
import com.remax.visualnovel.entity.response.HeartbeatLevel
import com.remax.visualnovel.extension.addScrollerAlpha
import com.remax.visualnovel.extension.getTemperatureTxt
import com.remax.visualnovel.extension.glide.loadAndRoundCorner
import com.remax.visualnovel.extension.launchAndLoadingCollect
import com.remax.visualnovel.extension.setMargin
import com.remax.visualnovel.extension.setOnClick
import com.remax.visualnovel.extension.setSize
import com.remax.visualnovel.extension.setSpanTypeFace
import com.remax.visualnovel.extension.showMoreTxtDialog
import com.remax.visualnovel.extension.translationYObjectAnimator
import com.remax.visualnovel.utils.Routers
import com.remax.visualnovel.utils.spannablex.spannable
import com.remax.visualnovel.utils.spannablex.utils.dp
import com.remax.visualnovel.widget.ui.UserAvatarView
import com.remax.visualnovel.widget.uitoken.changeTextFont
import com.remax.visualnovel.widget.uitoken.handleUIToken
import com.drake.brv.annotaion.DividerOrientation
import com.drake.brv.utils.divider
import com.drake.brv.utils.grid
import com.drake.brv.utils.models
import com.drake.brv.utils.setup
import com.remax.visualnovel.databinding.ActivityFlirtingLevelBinding
import com.remax.visualnovel.databinding.ItemFlirtingLevelBinding
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
@AndroidEntryPoint
@Route(path = Routers.CHAT_FLIRTING_LEVEL)
class FlirtingLevelActivity : BaseBindingActivity<ActivityFlirtingLevelBinding>() {
private val flirtingLevelViewModel by viewModels<FlirtingLevelViewModel>()
@JvmField
@Autowired
var aiId = ""
@SuppressLint("SetTextI18n")
override fun initView() {
ARouter.getInstance().inject(this)
setToolbar(R.string.app_name) {
isFull = true
titleTextAlpha = 1f
setRightIconBtn1(R.string.icon_more) {
val switchWindow = TipsSwitchWindow()
switchWindow.build(
this@FlirtingLevelActivity,
R.string.hide_relations,
flirtingLevelViewModel.levelOutput?.aiUserHeartbeatRelation?.isShow != true
) { switchView, isChecked ->
launchAndLoadingCollect({
flirtingLevelViewModel.relationSwitch(AIIsShowDTO(aiId, if (isChecked) 0 else 1))
}) {
onSuccess = {
with(binding) {
listOf(tvMeet, dividerMeet1, dividerMeet2).forEach { view ->
view.isGone =
flirtingLevelViewModel.levelOutput?.aiUserHeartbeatRelation?.isShow != true
}
}
}
onFailed = { _, _ ->
switchView.isChecked = !isChecked
}
}
}
switchWindow.showAsDropDown(this)
}
setRightIconBtn2(R.string.icon_faq) {
showMoreTxtDialog(
texts = listOf(
R.string.flirting_tips_txt_1,
R.string.flirting_tips_txt_2,
R.string.flirting_tips_txt_3
).map { getString(it) })
}
}
with(binding) {
scrollView.addScrollerAlpha(aiAvatarView, includeBgAlpha = true, textAlpha = false)
tvMeet.text = "· ${getString(R.string.meet)} ·"
setOnClick(retrieveGroup) {
flirtingLevelViewModel.levelOutput?.aiUserHeartbeatRelation?.let {
val retrieveDialog = FlirtingRetrieveDialog(this@FlirtingLevelActivity)
retrieveDialog.build(it) {
launchAndLoadingCollect({
flirtingLevelViewModel.buyHeartbeatVal(aiId)
}) {
onSuccess = {
initData()
retrieveDialog.dismiss()
}
}
}
retrieveDialog.binding.run {
showHeartAnim(levelBg, levelBgTop, lottieView, 84f)
}
retrieveDialog.show()
}
}
rv.grid(2)
.divider {
setDivider(16, true)
orientation = DividerOrientation.VERTICAL
}.setup {
addType<HeartbeatLevel>(R.layout.item_flirting_level)
onBind {
val item = getModel<HeartbeatLevel>()
with(getBinding<ItemFlirtingLevelBinding>()) {
tvName.text = item.name
tvName.isEnabled = item.isUnlock
lockView.isVisible = !item.isUnlock
imageView.loadAndRoundCorner(item.imgUrl, 16)
tvTemperature.text = item.startVal.getTemperatureTxt()
tvTemperature.isEnabled = item.isUnlock
imageStroke.isVisible = item.isUnlock
}
}
}
}
}
@SuppressLint("SetTextI18n", "DefaultLocale")
override fun initData() {
launchAndLoadingCollect({
flirtingLevelViewModel.getHeartbeatLevel(aiId)
}) {
onSuccess = {
val relation = it?.aiUserHeartbeatRelation
with(binding) {
listOf(tvMeet, dividerMeet1, dividerMeet2).forEach { view ->
view.isGone = relation?.isShow != true
}
showHeartAnim(levelBg, levelBgTop, lottieView, avatarView = aiAvatarView)
aiAvatarView.loadAvatar(relation?.aiHeadImg)
myAvatarView.loadAvatar(relation?.userHeadImg)
val currHeartbeat = relation?.currHeartbeatEnum
if (currHeartbeat == null) {
tvMeet.changeTextFont {
textUITextToken = getString(R.string.txt_title_s)
}
tvMeet.setText(R.string.no_intention_yet)
} else {
tvMeet.changeTextFont {
textUITextToken = getString(R.string.txt_display_s)
}
tvMeet.setText(currHeartbeat.tagName)
}
val heartbeatScore = String.format("%.2f", (relation?.heartbeatScore ?: 0.0f) * 100)
tvMeetDesc.text =
getString(R.string.flirting_desc, relation?.dayCount ?: 0, heartbeatScore)
tvLevel.text = spannable {
if (currHeartbeat != null) {
currHeartbeat.levelContent.text()
"".color(handleUIToken(R.string.color_outline_normal)?.color ?: 0)
}
(relation?.heartbeatVal ?: 0.0).toString().text()
getString(R.string.temperature).span {
setSpanTypeFace(this@FlirtingLevelActivity, R.string.txt_numMonotype_s)
}
}
/**
* 已经扣减的心动分
*/
retrieveGroup.isVisible = (relation?.subtractHeartbeatVal ?: 0.0) != 0.0
retrieveTitle.text =
getString(R.string.flirting_deducted_desc, (relation?.subtractHeartbeatVal ?: 0.0).toString())
rv.models = it?.heartbeatLeveLDictList
}
}
}
}
private fun showHeartAnim(
levelBg: View,
levelBgTop: View,
heartLottie: LottieAnimationView,
bgBottomMargin: Float = 0f,
avatarView: UserAvatarView? = null,
) {
levelBg.post {
val currProgress =
flirtingLevelViewModel.levelOutput?.aiUserHeartbeatRelation?.currHeartbeatEnum?.level ?: 0
val progress = currProgress / 10f
// 弹窗中的切图需要往上移动
levelBg.setMargin(bottomMargin = -((bgBottomMargin / 400f) * levelBgTop.measuredHeight).toInt())
// 根据切图比例调整宽高
val heartSize = ((98f / 214f) * levelBgTop.measuredHeight).toInt()
heartLottie.setSize(heartSize, heartSize)
// 头像框距离顶部的边距
val marginTop = ((120f / 214f) * levelBgTop.measuredHeight) + (heartSize - 64.dp) / 2f
avatarView?.setMargin(topMargin = marginTop.toInt())
avatarView?.isVisible = true
binding.myAvatarView.isVisible = true
// 动画持续时间
val totalTime = 1000L
// 总高度进度算
val totalHeight = progress * heartSize
Timber.i("showHeartAnim heartSize:$heartSize totalHeight:$totalHeight")
heartLottie.translationYObjectAnimator(-totalHeight, totalTime)
}
}
companion object {
fun start(aiId: String?) {
if (aiId != null) {
ARouter.getInstance()
.build(Routers.CHAT_FLIRTING_LEVEL)
.withString("aiId", aiId)
.navigation()
}
}
}
}

View File

@ -17,7 +17,7 @@ class FlirtingLevelViewModel @Inject constructor(private val chatRepository: Cha
var levelOutput: HeartbeatLevelOutput? = null
private set
suspend fun getHeartbeatLevel(aiId: String) = chatRepository.getHeartbeatLevel(aiId).transformResult({
/*suspend fun getHeartbeatLevel(aiId: String) = chatRepository.getHeartbeatLevel(aiId).transformResult({
levelOutput = it
})
@ -28,5 +28,17 @@ class FlirtingLevelViewModel @Inject constructor(private val chatRepository: Cha
suspend fun buyHeartbeatVal(aiId: String) = chatRepository.buyHeartbeatVal(HeartbeatBuy(
aiId,
levelOutput?.aiUserHeartbeatRelation?.subtractHeartbeatVal
))
))*/
suspend fun getHeartbeatLevel(aiId: String) {
}
suspend fun relationSwitch(request: AIIsShowDTO) {
}
suspend fun buyHeartbeatVal(aiId: String) {
}
}

View File

@ -199,47 +199,7 @@ class ChatSettingView @JvmOverloads constructor(
nameLanguage = "名称-5"
),
)
/*val items = listOf(
ChatSound(
id = 1L,
name = "Sound-1",
description = "This is description for sound-1",
isMale = true,
imgUrl = ""
),
ChatSound(
id = 2L,
name = "Sound-2",
description = "This is description for sound-2",
isMale = false,
imgUrl = ""
),
ChatSound(
id = 3L,
name = "Sound-3",
description = "This is description for sound-3",
isMale = true,
imgUrl = ""
),
ChatSound(
id = 4L,
name = "Sound-4",
description = "This is description for sound-4",
isMale = false,
imgUrl = ""
),
ChatSound(
id = 5L,
name = "Sound-5",
description = "This is description for sound-5",
isMale = true,
imgUrl = ""
)
)
*/
with(mBinding.soundActorSelector) {
setItems(items)
setEventListener(

View File

@ -8,7 +8,6 @@ import android.widget.LinearLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.drake.brv.annotaion.DividerOrientation
import com.drake.brv.utils.addModels
import com.drake.brv.utils.bindingAdapter
import com.drake.brv.utils.divider
import com.drake.brv.utils.grid
@ -59,21 +58,6 @@ class ExpandBackgroundSubView @JvmOverloads constructor(
addType<ChatBackgroundBase.ChatBackgroundUpload>(R.layout.layout_item_setting_background_upload)
onClick(R.id.root) {
/*when (val item = getModel<Any>(it)) {
is ChatBackgroundBase.ChatBackground -> {
val chatBackground = getModel<ChatBackgroundBase.ChatBackground>()
if (!chatBackground.isSelected) {
itemsRv.bindingAdapter.models?.filterIsInstance<ChatBackgroundBase.ChatBackground>()?.forEach { item ->
item.isSelected = item == chatBackground
}
itemsRv.bindingAdapter.notifyDataSetChanged()
}
}
is ChatBackgroundBase.ChatBackgroundUpload -> {
selectCustomBg()
}
}*/
when (itemViewType) {
R.layout.layout_item_setting_background -> {
val chatBackground = getModel<ChatBackgroundBase.ChatBackground>()
@ -91,6 +75,12 @@ class ExpandBackgroundSubView @JvmOverloads constructor(
}
}
onClick(R.id.iv_del) {
// TODO - request delete method
val chatBackground = getModel<ChatBackgroundBase.ChatBackground>()
//itemsRv.bindingAdapter.
}
onBind {
when (itemViewType) {

View File

@ -15,8 +15,10 @@ import com.remax.visualnovel.configs.NovelApplication
import com.remax.visualnovel.databinding.FragmentMainActorBinding
import com.remax.visualnovel.entity.request.ParamActorList
import com.remax.visualnovel.entity.request.ParamActorTag
import com.remax.visualnovel.entity.request.ParamSoundList
import com.remax.visualnovel.entity.response.ActorBean
import com.remax.visualnovel.extension.launchAndCollect2
import com.remax.visualnovel.utils.LanguageUtil
import com.remax.visualnovel.utils.Routers
import com.remax.visualnovel.utils.StatusBarUtil3
import dagger.hilt.android.AndroidEntryPoint
@ -32,7 +34,7 @@ class ActorListFragment : BaseBindingFragment<FragmentMainActorBinding>() {
private val mActorsModel by viewModels<ActorsViewModel>()
private var mLoadedPageIndex = 0
private val mRequestParam by lazy { ParamActorList() }
private val mRequestTagsParam by lazy { ParamActorTag() }
override fun onCreated(bundle: Bundle?) {
@ -151,20 +153,22 @@ class ActorListFragment : BaseBindingFragment<FragmentMainActorBinding>() {
private fun getActorTags() {
mRequestTagsParam.limit = Int.MAX_VALUE
val tagsParam = ParamActorTag().apply {
limit = Int.MAX_VALUE
}
launchAndCollect2({
mActorsModel.getActorTags(mRequestTagsParam)
mActorsModel.getActorTags(tagsParam)
}, showLoading = false) {
onSuccess = {
val data = it ?: emptyList()
binding.tagFlowLayout.setTagDataList(data)
}
}
}
private fun createSampleData(): List<ActorBean> {
return listOf(
ActorBean(characterName = "testName-1", description = "Des-xxxxxxxxxxxxx"),

View File

@ -24,4 +24,5 @@ class ActorsViewModel @Inject constructor(private val mActorsRepository: ActorsR
suspend fun getActorTags(param: ParamActorTag) = mActorsRepository.getActorTags(param)
}

View File

@ -42,12 +42,12 @@
<com.remax.visualnovel.widget.uitoken.view.UITokenImageView
android:id="@+id/iv_del"
android:layout_width="@dimen/dp_15"
android:layout_height="@dimen/dp_15"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@mipmap/setting_delete"
app:radiusToken="@string/radius_l"
android:tint="@color/white"
android:layout_marginBottom="@dimen/dp_9"
android:paddingVertical="@dimen/dp_12"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"