1. 录音模块 2. oss, pay bill 等引入
This commit is contained in:
parent
c9d15ccc99
commit
db2f338acd
|
|
@ -124,6 +124,13 @@ android {
|
|||
buildConfigString("ABOUT_US", "https://www.xxxxx.ai/about")
|
||||
buildConfigString("API_FROG", "https://www.test-frog.xxxxx.ai")
|
||||
buildConfigString("EPAL_TERMS_SERVICES", "https://www.xxxxx.ai/policy/tos")
|
||||
buildConfigString("API_SHARK", "https://test-shark.xxxxx.ai")
|
||||
buildConfigString("API_COW", "https://test-cow.xxxxx.ai")
|
||||
buildConfigString("API_PIGEON", "https://test-pigeon.xxxx.ai")
|
||||
buildConfigString("API_LION", "https://test-lion.xxxx.ai")
|
||||
buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge")
|
||||
|
||||
buildConfigString("RTC_APP_ID", "689ade491323ae01797818e0-XXX-TODO")
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -135,6 +142,13 @@ android {
|
|||
buildConfigString("ABOUT_US", "https://test.xxxxx.ai/about")
|
||||
buildConfigString("API_FROG", "https://test-frog.xxxxx.ai")
|
||||
buildConfigString("EPAL_TERMS_SERVICES", "https://test.xxxxx.ai/policy/tos")
|
||||
buildConfigString("API_SHARK", "https://test-shark.xxxxx.ai")
|
||||
buildConfigString("API_COW", "https://test-cow.xxxxx.ai")
|
||||
buildConfigString("API_PIGEON", "https://test-pigeon.xxxx.ai")
|
||||
buildConfigString("API_LION", "https://test-lion.xxxx.ai")
|
||||
buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge")
|
||||
|
||||
buildConfigString("RTC_APP_ID", "689ade491323ae01797818e0-XXX-TODO")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -274,6 +288,19 @@ dependencies {
|
|||
implementation(Deps.exoplayer)
|
||||
implementation(Deps.subsamplingScaleImageView)
|
||||
|
||||
//s3图片上传 oss
|
||||
implementation(Deps.awsS3)
|
||||
implementation(Deps.awsCore)
|
||||
|
||||
// 网易 云信
|
||||
implementation(Deps.nimBase)
|
||||
implementation(Deps.nimPush)
|
||||
|
||||
//内购 / 充值
|
||||
implementation(Deps.billing)
|
||||
|
||||
// RTC : 实时通信
|
||||
implementation(Deps.BytePlusRTC)
|
||||
|
||||
|
||||
implementation(project(mapOf("path" to ":loadingstateview")))
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.Chat.ChatActivity"
|
||||
android:name=".ui.chat.ChatActivity"
|
||||
android:exported="false" >
|
||||
</activity>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
package com.remax.visualnovel.api.service
|
||||
|
||||
import com.remax.visualnovel.BuildConfig
|
||||
import com.remax.visualnovel.entity.request.AIGenerate
|
||||
import com.remax.visualnovel.entity.request.AIGenerateImage
|
||||
import com.remax.visualnovel.entity.request.AIHeadImgRequest
|
||||
import com.remax.visualnovel.entity.request.AIIDRequest
|
||||
import com.remax.visualnovel.entity.request.AlbumCreate
|
||||
import com.remax.visualnovel.entity.request.AlbumDTO
|
||||
import com.remax.visualnovel.entity.request.CardRequest
|
||||
import com.remax.visualnovel.entity.request.ChatAlbum
|
||||
import com.remax.visualnovel.entity.request.ClassificationRequest
|
||||
import com.remax.visualnovel.entity.request.Gift
|
||||
import com.remax.visualnovel.entity.request.QueryAlbumDTO
|
||||
import com.remax.visualnovel.entity.request.SimpleCountDTO
|
||||
import com.remax.visualnovel.entity.response.Album
|
||||
import com.remax.visualnovel.entity.response.AlbumCreateCountOutput
|
||||
import com.remax.visualnovel.entity.response.AppearanceImage
|
||||
import com.remax.visualnovel.entity.response.Character
|
||||
import com.remax.visualnovel.entity.response.ContentRes
|
||||
import com.remax.visualnovel.entity.response.ExploreInfo
|
||||
import com.remax.visualnovel.entity.response.MeetSdOutput
|
||||
import com.remax.visualnovel.entity.response.Pageable
|
||||
import com.remax.visualnovel.entity.response.base.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface AIService {
|
||||
|
||||
/**
|
||||
* 卡片上报绑定
|
||||
*/
|
||||
@POST("/web/meet/bd")
|
||||
suspend fun cardBind(@Body request: AIIDRequest): Response<Character>
|
||||
|
||||
/**
|
||||
* 卡片被喜欢推荐
|
||||
*/
|
||||
@POST("/web/meet/rc")
|
||||
suspend fun cardLiked(): Response<Album>
|
||||
|
||||
/**
|
||||
* 卡片上报
|
||||
*/
|
||||
@POST("/web/meet/sd")
|
||||
suspend fun reportCard(@Body request: CardRequest): Response<MeetSdOutput>
|
||||
|
||||
/**
|
||||
* 获取首页卡片列表
|
||||
*/
|
||||
@POST("/web/home/rm-list")
|
||||
suspend fun getHomeCard(@Body request: ClassificationRequest): Response<List<Character>>
|
||||
|
||||
|
||||
/**
|
||||
* 获取单个首页卡片
|
||||
*/
|
||||
@POST(" /web/home/meet-detail")
|
||||
suspend fun getHomeCardDetail(@Body request: AIIDRequest): Response<Character>
|
||||
|
||||
/**
|
||||
* 获取分类列表
|
||||
*/
|
||||
@POST("/web/home/classification-list")
|
||||
suspend fun getClassificationList(@Body request: ClassificationRequest): Response<List<Character>>
|
||||
|
||||
/**
|
||||
* 获取榜单
|
||||
*/
|
||||
@POST("/web/rank/heartbeat")
|
||||
suspend fun getHeartbeatRank(): Response<List<Character>>
|
||||
|
||||
/**
|
||||
* 获取榜单
|
||||
*/
|
||||
@POST("/web/rank/gift")
|
||||
suspend fun getGiftRank(): Response<List<Character>>
|
||||
|
||||
/**
|
||||
* 获取榜单
|
||||
*/
|
||||
@POST("/web/rank/chat")
|
||||
suspend fun getChatRank(): Response<List<Character>>
|
||||
|
||||
/**
|
||||
* 获取发现页顶部数据
|
||||
*/
|
||||
@POST("/web/explore/info")
|
||||
suspend fun getExploreInfo(): Response<ExploreInfo>
|
||||
|
||||
/**
|
||||
* 解锁加密图片
|
||||
*/
|
||||
@POST("/web/ai-user/unlock-album-img")
|
||||
suspend fun unlockAlbum(@Body dto: ChatAlbum): Response<Album>
|
||||
|
||||
/**
|
||||
* 解锁秘密爱慕者
|
||||
*/
|
||||
@POST("/web/meet/unlock")
|
||||
suspend fun unlockSecret(@Body dto: AIIDRequest): Response<Album>
|
||||
|
||||
/**
|
||||
* 设置当前图片价格
|
||||
*/
|
||||
@POST("/web/ai-user/set-album-unlock-price")
|
||||
suspend fun setAlbumUnlockPrice(@Body dto: AlbumDTO): Response<Any>
|
||||
|
||||
/**
|
||||
* 删除AI角色
|
||||
*/
|
||||
@POST("/web/ai-user/del")
|
||||
suspend fun deleteAICharacter(@Body request: Character): Response<Any>
|
||||
|
||||
/**
|
||||
* 设置当前默认图片
|
||||
*/
|
||||
@POST("/web/ai-user/set-default-album")
|
||||
suspend fun setAlbumDefault(@Body dto: AlbumDTO): Response<Any>
|
||||
|
||||
@POST("/web/ai-user/create-edit")
|
||||
suspend fun createOrEditAICharacter(@Body request: Character): Response<Character>
|
||||
|
||||
@POST("/web/ai-user/edit-head-img")
|
||||
suspend fun editAIAvatar(@Body request: AIHeadImgRequest): Response<Any>
|
||||
|
||||
|
||||
@POST(BuildConfig.API_COW + "/web/gen/user-content-v1")
|
||||
suspend fun generateAICharacter(@Body request: AIGenerate): Response<ContentRes>
|
||||
|
||||
/**
|
||||
* 编辑时获取我的ai角色信息
|
||||
*/
|
||||
@POST("/web/ai-user/get-my-ai-user/info")
|
||||
suspend fun getAICharacter(@Body request: Character): Response<Character>
|
||||
|
||||
/**
|
||||
* 访问AI个人主页时获取信息
|
||||
*/
|
||||
@POST("/web/ai-user-search/base-info")
|
||||
suspend fun getAICharacterProfile(@Body request: Character): Response<Character>
|
||||
|
||||
/**
|
||||
* 访问AI的统计信息
|
||||
*/
|
||||
@POST("/web/ai-user/stat")
|
||||
suspend fun getAICharacterStat(@Body request: Character): Response<Character>
|
||||
|
||||
/**
|
||||
* 修改点赞状态
|
||||
*/
|
||||
@POST("/web/ai-user/like-or-cancel")
|
||||
suspend fun setAILikeOrCancel(@Body request: AlbumDTO): Response<Any>
|
||||
|
||||
/**
|
||||
* 喜欢或取消喜欢相片
|
||||
*/
|
||||
@POST("/web/album/like_or_cancel")
|
||||
suspend fun setLikeOrDislike(@Body dto: AlbumDTO): Response<Any>
|
||||
|
||||
/**
|
||||
* 删除相片
|
||||
*/
|
||||
@POST("/web/ai-user/album-del")
|
||||
suspend fun deleteAlbum(@Body dto: AlbumDTO): Response<Any>
|
||||
|
||||
/**
|
||||
* 批量添加图片到相册
|
||||
*/
|
||||
@POST("/web/ai-user/batch-add-album")
|
||||
suspend fun addAlbum(@Body dto: AlbumCreate): Response<Any>
|
||||
|
||||
/**
|
||||
* 获取创作次数
|
||||
*/
|
||||
@POST("/web/user/get-user-create-count")
|
||||
suspend fun getAlbumCreateCount(): Response<AlbumCreateCountOutput>
|
||||
|
||||
/**
|
||||
* 购买创作次数
|
||||
*/
|
||||
@POST("/web/ai/buy-create-image-count")
|
||||
suspend fun buyAlbumCreateCount(@Body dto: SimpleCountDTO): Response<Any>
|
||||
|
||||
/**
|
||||
* 批量添加图片到聊天背景
|
||||
*/
|
||||
@POST("/web/chat-background/batch-add")
|
||||
suspend fun addChatBackground(@Body dto: AlbumCreate): Response<Any>
|
||||
|
||||
/**
|
||||
* 获取相册 分页
|
||||
*/
|
||||
@POST("/web/ai-user/album-list")
|
||||
suspend fun getAlbumList(@Body dto: QueryAlbumDTO): Response<Pageable<Album>>
|
||||
|
||||
/**
|
||||
* 获取用户礼物 分页
|
||||
*/
|
||||
@POST("/web/ai-user-gift/list")
|
||||
suspend fun getUserGiftList(@Body dto: QueryAlbumDTO): Response<Pageable<Gift>>
|
||||
|
||||
|
||||
/**
|
||||
* AI一键生成-创建生成人物形象图片任务
|
||||
*/
|
||||
@POST(BuildConfig.API_COW + "/web/gen/image-ct")
|
||||
suspend fun generateImageBatch(@Body request: AIGenerateImage): Response<AIGenerateImage>
|
||||
|
||||
/**
|
||||
* AI一键生成-删除图片生成任务
|
||||
*/
|
||||
@POST(BuildConfig.API_COW + "/web/gen/del")
|
||||
suspend fun generateImageBatchDel(@Body request: AIGenerateImage): Response<Any>
|
||||
|
||||
/**
|
||||
* AI一键生成-轮询查询图片生成结果
|
||||
*/
|
||||
@POST(BuildConfig.API_COW + "/web/gen/image-pl")
|
||||
suspend fun generateImageBatchQuery(@Body request: AIGenerateImage): Response<List<AppearanceImage>>
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package com.remax.visualnovel.api.service
|
||||
|
||||
import com.remax.visualnovel.BuildConfig
|
||||
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.SimpleDataDTO
|
||||
import com.remax.visualnovel.entity.request.VoiceTTS
|
||||
import com.remax.visualnovel.entity.response.Album
|
||||
import com.remax.visualnovel.entity.response.Character
|
||||
import com.remax.visualnovel.entity.response.ChatBackground
|
||||
import com.remax.visualnovel.entity.response.ChatSet
|
||||
import com.remax.visualnovel.entity.response.Friends
|
||||
import com.remax.visualnovel.entity.response.HeartbeatLevelOutput
|
||||
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 retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface ChatService {
|
||||
|
||||
/**
|
||||
* 发送开场白消息
|
||||
*/
|
||||
@POST("/web/chat/send-dialogue-prologue-message")
|
||||
suspend fun sendDialogueMsg(@Body request: AIIDRequest): Response<Any>
|
||||
|
||||
/**
|
||||
* 获取IM中AI的基础信息
|
||||
*/
|
||||
@POST("/web/ai-user-search/im-base-info")
|
||||
suspend fun getIMAICharacterProfile(@Body request: Character): Response<Character>
|
||||
|
||||
/**
|
||||
* 访问解锁加密图片
|
||||
*/
|
||||
@POST("/web/ai-user/view-unlock-album-img")
|
||||
suspend fun viewAlbumImg(@Body request: ChatAlbum): Response<Album>
|
||||
|
||||
/**
|
||||
* 关系列表
|
||||
*/
|
||||
@POST("/web/ai-user/heartbeat-relation-list")
|
||||
suspend fun getMyFriends(@Body request: SearchPage): Response<Pageable<Friends>>
|
||||
|
||||
|
||||
@POST("/web/ai-user/heartbeat-rank")
|
||||
suspend fun getMyFriendRank(): Response<Double>
|
||||
|
||||
/**
|
||||
* 生成提示词
|
||||
*/
|
||||
@POST(BuildConfig.API_COW + "/web/gen/sup-content-v2")
|
||||
suspend fun getPrompts(@Body request: AIIDRequest): Response<List<String>>
|
||||
|
||||
/**
|
||||
* AI回话点赞/点踩
|
||||
*/
|
||||
@POST(BuildConfig.API_PIGEON + "/web/fb/v1")
|
||||
suspend fun aiFeedback(@Body request: AIFeedback): Response<Any>
|
||||
|
||||
/**
|
||||
* 获取RTC
|
||||
*/
|
||||
@POST(BuildConfig.API_COW + "/web/voice-chat/gen-rtc-tk")
|
||||
suspend fun getRTCToken(@Body request: RTCRequest): Response<Token>
|
||||
|
||||
/**
|
||||
* 操作通话
|
||||
*/
|
||||
@POST(BuildConfig.API_COW + "/web/voice-chat/opt")
|
||||
suspend fun voiceChatOpt(@Body request: RTCRequest): Response<Any>
|
||||
|
||||
/**
|
||||
* 获取聊天背景列表
|
||||
*/
|
||||
@POST("/web/chat-background/list")
|
||||
suspend fun getChatBackgroundList(@Body request: AIIDRequest): Response<List<ChatBackground>>
|
||||
|
||||
/**
|
||||
* 获取聊天设置
|
||||
*/
|
||||
@POST("/web/chat-set/get-my")
|
||||
suspend fun getChatSetting(@Body request: ChatSetting): Response<ChatSet>
|
||||
|
||||
/**
|
||||
* 修改聊天设置
|
||||
*/
|
||||
@POST("/web/chat-set/set")
|
||||
suspend fun setChatSetting(@Body request: ChatSet): Response<Any>
|
||||
|
||||
/**
|
||||
* 修改聊天气泡
|
||||
*/
|
||||
@POST("/web/chat-set/set-chat-bubble")
|
||||
suspend fun setChatBubble(@Body request: ChatSetting): Response<Any>
|
||||
|
||||
/**
|
||||
* 修改聊天模型
|
||||
*/
|
||||
@POST("/web/chat-set/set-chat-model")
|
||||
suspend fun setChatModel(@Body request: ChatSetting): Response<Any>
|
||||
|
||||
/**
|
||||
* 修改是否自动播放语音
|
||||
*/
|
||||
@POST("/web/chat-set/auto-play-voice")
|
||||
suspend fun setChatAutoPlay(@Body request: ChatSetting): Response<Any>
|
||||
|
||||
/**
|
||||
* 修改聊天背景图
|
||||
*/
|
||||
@POST("/web/chat-background/set-background")
|
||||
suspend fun setChatBackground(@Body request: ChatSetting): Response<Any>
|
||||
|
||||
/**
|
||||
* 删除聊天背景图
|
||||
*/
|
||||
@POST("/web/chat-background/del")
|
||||
suspend fun deleteChatBackground(@Body request: ChatSetting): Response<Any>
|
||||
|
||||
/**
|
||||
* 展示心动关系开关
|
||||
*/
|
||||
@POST("/web/ai-user/heartbeat-relation-switch")
|
||||
suspend fun relationSwitch(@Body request: AIIsShowDTO): Response<Any>
|
||||
|
||||
/**
|
||||
* 语音转文本
|
||||
*/
|
||||
@POST(BuildConfig.API_COW + "/web/voice/asr-v2")
|
||||
suspend fun voiceASR(@Body request: SimpleDataDTO): Response<VoiceASR>
|
||||
|
||||
/**
|
||||
* 生成语音
|
||||
*/
|
||||
@POST(BuildConfig.API_COW + "/web/voice/tts-v2")
|
||||
suspend fun voiceTTS(@Body request: VoiceTTS): Response<String>
|
||||
|
||||
/**
|
||||
* 获取心动等级
|
||||
*/
|
||||
@POST("/web/ai-user/heartbeat-level")
|
||||
suspend fun getHeartbeatLevel(@Body request: Character): Response<HeartbeatLevelOutput>
|
||||
|
||||
/**
|
||||
* 购买心动值
|
||||
*/
|
||||
@POST("/web/ai-user/buy-heartbeat-val")
|
||||
suspend fun buyHeartbeatVal(@Body request: HeartbeatBuy): Response<Any>
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
package com.remax.visualnovel.api.service
|
||||
|
||||
|
||||
import com.remax.visualnovel.entity.request.AIIDRequest
|
||||
import com.remax.visualnovel.entity.request.Gift
|
||||
import com.remax.visualnovel.entity.request.PageQuery
|
||||
import com.remax.visualnovel.entity.response.AIDict
|
||||
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 retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
|
@ -10,24 +17,24 @@ interface DictService {
|
|||
/**
|
||||
* 获取聊天气泡字典
|
||||
*/
|
||||
/*@POST("/web/chat-set/get-chat-bubble-list")
|
||||
suspend fun getChatBubbleList(@Body request: AIIDRequest): Response<List<ChatBubble>>*/
|
||||
@POST("/web/chat-set/get-chat-bubble-list")
|
||||
suspend fun getChatBubbleList(@Body request: AIIDRequest): Response<List<ChatBubble>>
|
||||
|
||||
/**
|
||||
* AI标签
|
||||
*/
|
||||
/*@POST("/web/get-ai-dict")
|
||||
suspend fun getAIDict(): Response<AIDict>*/
|
||||
@POST("/web/get-ai-dict")
|
||||
suspend fun getAIDict(): Response<AIDict>
|
||||
|
||||
/**
|
||||
* 礼物字典
|
||||
*/
|
||||
/*@POST("/web/gift/dict-list")
|
||||
suspend fun getGiftDict(@Body pageQuery: PageQuery = PageQuery(1).apply { page.ps = 100 }): Response<Pageable<Gift>>*/
|
||||
@POST("/web/gift/dict-list")
|
||||
suspend fun getGiftDict(@Body pageQuery: PageQuery = PageQuery(1).apply { page.ps = 100 }): Response<Pageable<Gift>>
|
||||
|
||||
/**
|
||||
* chat模型
|
||||
*/
|
||||
/*@POST("/web/chat-model/dict-list")
|
||||
suspend fun getAIChatModel(): Response<List<ChatModel>>*/
|
||||
@POST("/web/chat-model/dict-list")
|
||||
suspend fun getAIChatModel(): Response<List<ChatModel>>
|
||||
}
|
||||
|
|
@ -1,34 +1,40 @@
|
|||
package com.remax.visualnovel.api.service
|
||||
|
||||
import com.remax.visualnovel.BuildConfig
|
||||
import com.remax.visualnovel.entity.request.AIListRequest
|
||||
import com.remax.visualnovel.entity.request.PageQuery
|
||||
import com.remax.visualnovel.entity.request.SendGift
|
||||
import com.remax.visualnovel.entity.response.MessageListOutput
|
||||
import com.remax.visualnovel.entity.response.MessageStatOutput
|
||||
import com.remax.visualnovel.entity.response.Pageable
|
||||
import com.remax.visualnovel.entity.response.base.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface MessageService {
|
||||
|
||||
// /**
|
||||
// * 删除会话
|
||||
// */
|
||||
// @POST(BuildConfig.API_COW + "/web/ai-message/del")
|
||||
// suspend fun deleteConversation(@Body request: AIListRequest): Response<Any>
|
||||
//
|
||||
// /**
|
||||
// * 送礼物
|
||||
// */
|
||||
// @POST("/web/ai-user-gift/send")
|
||||
// suspend fun sendGift(@Body dto: SendGift): Response<Any>
|
||||
//
|
||||
// /**
|
||||
// * 未读消息统计
|
||||
// */
|
||||
// @POST(BuildConfig.API_PIGEON + "/web/message/stat")
|
||||
// suspend fun getMessageStat(): Response<MessageStatOutput>
|
||||
//
|
||||
// /**
|
||||
// * 系统通知列表
|
||||
// */
|
||||
// @POST(BuildConfig.API_PIGEON + "/web/message/list")
|
||||
// suspend fun getMessageList(@Body dto: PageQuery): Response<Pageable<MessageListOutput>>
|
||||
/**
|
||||
* 删除会话
|
||||
*/
|
||||
@POST(BuildConfig.API_COW + "/web/ai-message/del")
|
||||
suspend fun deleteConversation(@Body request: AIListRequest): Response<Any>
|
||||
|
||||
/**
|
||||
* 送礼物
|
||||
*/
|
||||
@POST("/web/ai-user-gift/send")
|
||||
suspend fun sendGift(@Body dto: SendGift): Response<Any>
|
||||
|
||||
/**
|
||||
* 未读消息统计
|
||||
*/
|
||||
@POST(BuildConfig.API_PIGEON + "/web/message/stat")
|
||||
suspend fun getMessageStat(): Response<MessageStatOutput>
|
||||
|
||||
/**
|
||||
* 系统通知列表
|
||||
*/
|
||||
@POST(BuildConfig.API_PIGEON + "/web/message/list")
|
||||
suspend fun getMessageList(@Body dto: PageQuery): Response<Pageable<MessageListOutput>>
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.remax.visualnovel.api.service
|
||||
|
||||
import com.remax.visualnovel.BuildConfig
|
||||
import com.remax.visualnovel.entity.request.ImgCheckDTO
|
||||
import com.remax.visualnovel.entity.request.S3TypeDTO
|
||||
import com.remax.visualnovel.entity.request.SimpleContentDTO
|
||||
import com.remax.visualnovel.entity.response.BucketBean
|
||||
import com.remax.visualnovel.entity.response.base.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* OSS文件上传
|
||||
*
|
||||
*/
|
||||
interface OssService {
|
||||
/**
|
||||
* 获取aws s3 bucket信息
|
||||
*/
|
||||
@POST(BuildConfig.API_SHARK + "/web/file/sts-tk")
|
||||
suspend fun getS3Bucket(@Body dto: S3TypeDTO): Response<BucketBean>
|
||||
|
||||
/**
|
||||
* 图片鉴黄
|
||||
*/
|
||||
@POST(BuildConfig.API_SHARK + "/web/file/check")
|
||||
suspend fun checkS3Img(
|
||||
@Body imgCheckDTO: ImgCheckDTO
|
||||
): Response<Any>
|
||||
|
||||
/**
|
||||
* 关键字校验
|
||||
*/
|
||||
@POST("/web/check_text")
|
||||
suspend fun checkText(
|
||||
@Body simpleContentDTO: SimpleContentDTO
|
||||
): Response<Any>
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.remax.visualnovel.api.service
|
||||
|
||||
import com.remax.visualnovel.BuildConfig
|
||||
import com.remax.visualnovel.entity.request.ChargeOrderDTO
|
||||
import com.remax.visualnovel.entity.request.ChargeProductDTO
|
||||
import com.remax.visualnovel.entity.request.ChargeProductInfo
|
||||
import com.remax.visualnovel.entity.request.SearchPage
|
||||
import com.remax.visualnovel.entity.request.SubPriceDTO
|
||||
import com.remax.visualnovel.entity.request.ValidateTransactionDTO
|
||||
import com.remax.visualnovel.entity.response.ChargeOrder
|
||||
import com.remax.visualnovel.entity.response.Membership
|
||||
import com.remax.visualnovel.entity.response.SubPrice
|
||||
import com.remax.visualnovel.entity.response.Transaction
|
||||
import com.remax.visualnovel.entity.response.UserSubInfo
|
||||
import com.remax.visualnovel.entity.response.Wallet
|
||||
import com.remax.visualnovel.entity.response.base.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface PayService {
|
||||
/**
|
||||
* 获取我的流水
|
||||
*/
|
||||
@POST(BuildConfig.API_LION + "/web/pay/account/bill-list")
|
||||
suspend fun getTransactionList(@Body request: SearchPage): Response<Transaction>
|
||||
|
||||
/**
|
||||
* 获取我的钱包
|
||||
*/
|
||||
@POST(BuildConfig.API_LION + "/web/pay/account/wallet")
|
||||
suspend fun getMyWallet(): Response<Wallet>
|
||||
|
||||
/**
|
||||
* 获取充值产品
|
||||
*/
|
||||
@POST(BuildConfig.API_LION + "/web/pay/config/charge-product-list")
|
||||
suspend fun getChargeProducts(
|
||||
@Body dto: ChargeProductDTO = ChargeProductDTO()
|
||||
): Response<ChargeProductInfo>
|
||||
|
||||
|
||||
/**
|
||||
* 获取vip订阅价格列表
|
||||
*/
|
||||
@POST(BuildConfig.API_LION + "/web/pay/config/sub-product-list")
|
||||
suspend fun getSubPriceList(
|
||||
@Body subPriceDTO: SubPriceDTO = SubPriceDTO()
|
||||
): Response<List<SubPrice>>
|
||||
|
||||
/**
|
||||
* 会员特权列表
|
||||
*/
|
||||
@POST(BuildConfig.API_LION + "/web/member/detail")
|
||||
suspend fun getVipPrivilegeList(): Response<Membership>
|
||||
|
||||
/**
|
||||
* 创建一个订单
|
||||
*/
|
||||
@POST(BuildConfig.API_LION +"/web/pay/trade/pre-charge-google")
|
||||
suspend fun createOrder(
|
||||
@Body dto: ChargeOrderDTO
|
||||
): Response<ChargeOrder>
|
||||
|
||||
/**
|
||||
* 验证支付是否成功
|
||||
*/
|
||||
@POST(BuildConfig.API_LION +"/web/pay/webhooks/google/v2")
|
||||
suspend fun validateTranslation(
|
||||
@Body dto: ValidateTransactionDTO
|
||||
): Response<Any>
|
||||
|
||||
/**
|
||||
* 验证订阅是否成功
|
||||
*/
|
||||
@POST(BuildConfig.API_LION +"/web/pay/subscribe/upload-google-receipt")
|
||||
suspend fun uploadGoogleReceipt(
|
||||
@Body dto: ValidateTransactionDTO
|
||||
): Response<String>
|
||||
|
||||
/**
|
||||
* 订阅/升级VIP前查询订阅信息
|
||||
*/
|
||||
@POST(BuildConfig.API_LION +"/web/pay/appStore/getUserSubscription")
|
||||
suspend fun checkSubInfo(): Response<UserSubInfo>
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -2,10 +2,14 @@ package com.remax.visualnovel.app.di
|
|||
|
||||
|
||||
import com.remax.visualnovel.api.factory.ServiceFactory
|
||||
import com.remax.visualnovel.api.service.AIService
|
||||
import com.remax.visualnovel.api.service.BookService
|
||||
import com.remax.visualnovel.api.service.ChatService
|
||||
import com.remax.visualnovel.api.service.DictService
|
||||
import com.remax.visualnovel.api.service.LoginService
|
||||
import com.remax.visualnovel.api.service.MessageService
|
||||
import com.remax.visualnovel.api.service.OssService
|
||||
import com.remax.visualnovel.api.service.PayService
|
||||
import com.remax.visualnovel.api.service.UserService
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -40,6 +44,22 @@ object ApiServiceModule {
|
|||
@Provides
|
||||
fun bookService() = create<BookService>()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun aiService() = create<AIService>()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun ossService() = create<OssService>()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun payService() = create<PayService>()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun chatService() = create<ChatService>()
|
||||
|
||||
|
||||
private inline fun <reified T> create(): T {
|
||||
return ServiceFactory.createService()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,271 @@
|
|||
package com.remax.visualnovel.app.viewmodel.base
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import com.amazonaws.auth.BasicSessionCredentials
|
||||
import com.amazonaws.mobileconnectors.s3.transferutility.TransferListener
|
||||
import com.amazonaws.mobileconnectors.s3.transferutility.TransferState
|
||||
import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility
|
||||
import com.amazonaws.services.s3.AmazonS3Client
|
||||
import com.amazonaws.services.s3.S3ClientOptions
|
||||
import com.amazonaws.services.s3.model.ObjectMetadata
|
||||
import com.remax.visualnovel.R
|
||||
import com.remax.visualnovel.app.base.app.CommonApplicationProxy
|
||||
import com.remax.visualnovel.constant.StatusCode
|
||||
import com.remax.visualnovel.entity.request.ImgCheckDTO
|
||||
import com.remax.visualnovel.entity.response.BucketBean
|
||||
import com.remax.visualnovel.entity.response.base.ApiFailedResponse
|
||||
import com.remax.visualnovel.entity.response.base.ApiSuccessResponse
|
||||
import com.remax.visualnovel.entity.response.base.Response
|
||||
import com.remax.visualnovel.extension.resumeWithActive
|
||||
import com.remax.visualnovel.repository.api.OssRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/11/9
|
||||
*
|
||||
* oss上传相关
|
||||
*/
|
||||
@HiltViewModel
|
||||
open class OssViewModel @Inject constructor() : UserViewModel() {
|
||||
|
||||
@Inject
|
||||
lateinit var ossRepository: OssRepository
|
||||
|
||||
data class LoadFileData(
|
||||
var isSuccess: Boolean,
|
||||
val isViolation: Boolean = false,
|
||||
val errorMsg: String = "",
|
||||
var urlPath: String = "",
|
||||
var filePath: String = "",
|
||||
var width: Int = 0,
|
||||
var height: Int = 0
|
||||
)
|
||||
|
||||
data class FileUpLoadRes(
|
||||
val loadFileData: LoadFileData,
|
||||
val fileOption: FileOption? = null,
|
||||
)
|
||||
|
||||
data class FileOption(
|
||||
val path: String,
|
||||
val urlPath: String,
|
||||
val ossType: String,
|
||||
val width: Int,
|
||||
val height: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 请求oss的token
|
||||
*/
|
||||
suspend fun getBucketToken(postfix: String, ossType: String): Response<BucketBean> {
|
||||
return ossRepository.getS3Bucket(ossType, postfix)
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂起函数上传图片
|
||||
* @param filePath String
|
||||
* @param ossType String
|
||||
* @param isImg Boolean
|
||||
* @param checkNSFW Boolean 是否鉴黄
|
||||
* @param checkRealPerson Boolean 是否鉴定真人
|
||||
* @param checkKid Boolean 是否鉴定儿童
|
||||
* @return Response<LoadFileData> 封装成服务器返回一致类型处理
|
||||
*/
|
||||
suspend fun ossUploadFile(
|
||||
filePath: String,
|
||||
ossType: String,
|
||||
isImg: Boolean = true,
|
||||
checkNSFW: Boolean = true,
|
||||
checkRealPerson: Boolean = false,
|
||||
checkKid: Boolean = false,
|
||||
token: BucketBean? = null
|
||||
): Response<LoadFileData> {
|
||||
/**
|
||||
* 获取S3 STS Token对象
|
||||
*/
|
||||
var s3BucketRes = token
|
||||
if (s3BucketRes == null) {
|
||||
val postfix = if (filePath.isNotEmpty()) filePath.substring(filePath.lastIndexOf(".") + 1) else "png"
|
||||
val getTokenRes = getBucketToken(postfix, ossType)
|
||||
//请求s3 token失败
|
||||
if (!getTokenRes.isApiSuccess) {
|
||||
return ApiFailedResponse(
|
||||
errorMsg = getTokenRes.errorMsg,
|
||||
errorData = createNormalErrorFileData(filePath).loadFileData
|
||||
)
|
||||
} else {
|
||||
s3BucketRes = getTokenRes.data!!
|
||||
}
|
||||
}
|
||||
val uploadRes = uploadFile(s3BucketRes, isImg, filePath, ossType)
|
||||
//上传图片失败
|
||||
if (!uploadRes.loadFileData.isSuccess) {
|
||||
return ApiFailedResponse(errorMsg = uploadRes.loadFileData.errorMsg, errorData = uploadRes.loadFileData)
|
||||
}
|
||||
//如果不是图片 或者 不需要鉴黄、鉴定真人、鉴定儿童,直接返回成功结果
|
||||
if (!isImg || (!checkNSFW && !checkRealPerson && !checkKid)) {
|
||||
return ApiSuccessResponse(uploadRes.loadFileData)
|
||||
}
|
||||
val fileOption = uploadRes.fileOption!!
|
||||
val checkDTO = ImgCheckDTO(fileOption.ossType, fileOption.path)
|
||||
//鉴黄
|
||||
val checkNSFWRes = if (checkNSFW) ossRepository.checkS3Img(checkDTO) else ApiSuccessResponse()
|
||||
return when {
|
||||
checkNSFWRes.isApiSuccess -> {
|
||||
ApiSuccessResponse(uploadRes.loadFileData)
|
||||
}
|
||||
|
||||
else -> {
|
||||
ApiFailedResponse(StatusCode.UPLOAD_FILE_VIOLATION.code, checkNSFWRes.errorMsg, uploadRes.loadFileData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装上传失败的实体
|
||||
* @param filePath String 本地图片地址
|
||||
* @return FileUpLoadRes
|
||||
*/
|
||||
private fun createNormalErrorFileData(filePath: String) = FileUpLoadRes(
|
||||
LoadFileData(
|
||||
isSuccess = false,
|
||||
errorMsg = CommonApplicationProxy.application.getString(R.string.upload_error),
|
||||
filePath = filePath
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* 协程处理亚马逊上传图片
|
||||
*
|
||||
* 使用带取消回调的协程,当上传回调时,需要判断协程isActive以防报错崩溃
|
||||
* @param stsToken BucketBean 授权信息
|
||||
* @param isImg Boolean 是否是图片
|
||||
* @param filePath String 本地地址
|
||||
* @param ossType String 上传类型
|
||||
* @return FileUpLoadRes 返回结果封装
|
||||
*/
|
||||
private suspend fun uploadFile(
|
||||
stsToken: BucketBean,
|
||||
isImg: Boolean,
|
||||
filePath: String,
|
||||
ossType: String,
|
||||
) = suspendCancellableCoroutine {
|
||||
it.invokeOnCancellation { _ ->
|
||||
it.resumeWithActive(createNormalErrorFileData(filePath))
|
||||
}
|
||||
|
||||
val awsCreds = BasicSessionCredentials(
|
||||
stsToken.accessKeyId,
|
||||
stsToken.accessKeySecret,
|
||||
stsToken.securityToken
|
||||
)
|
||||
val uploadClient = AmazonS3Client(
|
||||
awsCreds,
|
||||
com.amazonaws.regions.Region.getRegion(stsToken.region)
|
||||
).apply {
|
||||
setS3ClientOptions(
|
||||
S3ClientOptions.builder()
|
||||
.setAccelerateModeEnabled(false)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
val transferUtility = TransferUtility.builder()
|
||||
.s3Client(uploadClient)
|
||||
.context(CommonApplicationProxy.application)
|
||||
.build()
|
||||
|
||||
val fileName = filePath.substring(filePath.lastIndexOf("/") + 1)
|
||||
val path = if (stsToken.path.endsWith("*")) {
|
||||
stsToken.path.replace("*", fileName)
|
||||
} else {
|
||||
stsToken.path
|
||||
}
|
||||
val urlPath = if (stsToken.urlPath.endsWith("*")) {
|
||||
stsToken.urlPath.replace("*", fileName)
|
||||
} else {
|
||||
stsToken.urlPath
|
||||
}
|
||||
Timber.d("oss上传 - AmazonS3 Token path:$path urlPath:$urlPath")
|
||||
|
||||
val obj = ObjectMetadata()
|
||||
obj.addUserMetadata("x-amz-tagging", "temp=1")
|
||||
|
||||
val transferListener = object :
|
||||
TransferListener {
|
||||
override fun onStateChanged(id: Int, state: TransferState?) {
|
||||
Timber.d("oss上传 - AmazonS3 onStateChanged:$state")
|
||||
Timber.d("oss上传 - 协程状态 isActive: ${it.isActive} isCancelled: ${it.isCancelled} isCompleted: ${it.isCompleted}")
|
||||
when (state) {
|
||||
TransferState.COMPLETED -> {
|
||||
//此方法是上传图片完成后再打标签
|
||||
// uploadClient.setObjectTagging(SetObjectTaggingRequest(stsToken.bucket, stsToken.path, ObjectTagging(listOf(Tag("temp", "1")))))
|
||||
if (it.isActive) {
|
||||
if (isImg) {
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFile(filePath, options)
|
||||
val wid = options.outWidth
|
||||
val hei = options.outHeight
|
||||
val res = FileUpLoadRes(
|
||||
LoadFileData(
|
||||
isSuccess = true,
|
||||
urlPath = urlPath,
|
||||
width = wid,
|
||||
height = hei,
|
||||
filePath = filePath
|
||||
),
|
||||
FileOption(path, urlPath, ossType, wid, hei)
|
||||
)
|
||||
it.resumeWithActive(res)
|
||||
} else {
|
||||
val res = FileUpLoadRes(
|
||||
LoadFileData(
|
||||
isSuccess = true,
|
||||
urlPath = urlPath,
|
||||
filePath = filePath
|
||||
)
|
||||
)
|
||||
it.resumeWithActive(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TransferState.FAILED, TransferState.CANCELED -> {
|
||||
it.resumeWithActive(createNormalErrorFileData(filePath))
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {
|
||||
Timber.d("oss上传 - AmazonS3 onProgressChanged - bytesTotal:${bytesTotal} - bytesCurrent:${bytesCurrent}")
|
||||
}
|
||||
|
||||
override fun onError(id: Int, ex: Exception?) {
|
||||
Timber.d("oss上传 - AmazonS3 onError:${ex?.localizedMessage} - id:$id")
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
filePath.isNotEmpty() -> {
|
||||
val file = File(filePath)
|
||||
if (!file.exists()) {
|
||||
it.cancel()
|
||||
}
|
||||
Timber.d("oss上传 - 上传文件大小 ${file.length() / 1024}")
|
||||
transferUtility.upload(stsToken.bucket, path, file, obj)
|
||||
.setTransferListener(transferListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkText(content: String): Response<Any> = ossRepository.checkText(content)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.remax.visualnovel.entity.imbean
|
||||
|
||||
import com.remax.visualnovel.entity.imbean.voice.IMVoice
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/19
|
||||
*/
|
||||
data class IMAIInMessage(
|
||||
override var message: V2NIMMessage?,
|
||||
val imVoice: IMVoice
|
||||
) : IMMessageWrapper(type = IN_TEXT_TYPE)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.remax.visualnovel.entity.imbean
|
||||
|
||||
import com.remax.visualnovel.entity.response.Character
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/19
|
||||
*/
|
||||
data class IMBaseInfoMessage(
|
||||
val character: Character?
|
||||
) : IMMessageWrapper(type = BASE_INFO)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.remax.visualnovel.entity.imbean
|
||||
|
||||
import com.remax.visualnovel.entity.imbean.raw.CustomCallData
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/19
|
||||
*/
|
||||
data class IMCallMessage(
|
||||
override var message: V2NIMMessage?,
|
||||
val call: CustomCallData?
|
||||
) : IMMessageWrapper(type = OUT_CALL_TYPE)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.remax.visualnovel.entity.imbean
|
||||
|
||||
import com.remax.visualnovel.entity.imbean.raw.CustomGiftData
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/19
|
||||
*/
|
||||
data class IMGiftMessage(
|
||||
override var message: V2NIMMessage?,
|
||||
val gift: CustomGiftData?
|
||||
) : IMMessageWrapper(type = OUT_GIFT_TYPE)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.remax.visualnovel.entity.imbean
|
||||
|
||||
import com.remax.visualnovel.entity.imbean.raw.CustomAlbumData
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/19
|
||||
*/
|
||||
data class IMInImageMessage(
|
||||
override var message: V2NIMMessage?,
|
||||
val albumData: CustomAlbumData?
|
||||
) : IMMessageWrapper(type = IN_IMAGE_TYPE)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.remax.visualnovel.entity.imbean
|
||||
|
||||
import com.remax.visualnovel.entity.imbean.raw.CustomLevelChangeData
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/19
|
||||
*/
|
||||
data class IMLevelMessage(
|
||||
override var message: V2NIMMessage?,
|
||||
val level: CustomLevelChangeData?
|
||||
) : IMMessageWrapper(type = HEART_BEAT_CHANGED_TYPE)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.remax.visualnovel.entity.imbean
|
||||
|
||||
import com.remax.visualnovel.manager.nim.FetchResult
|
||||
import com.remax.visualnovel.manager.nim.LoadStatus
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
|
||||
|
||||
|
||||
/**
|
||||
* Created by HJW on 2020/9/28
|
||||
*/
|
||||
open class IMMessageWrapper(
|
||||
open var message: V2NIMMessage? = null,
|
||||
var type: Int = OUT_TEXT_TYPE,
|
||||
) {
|
||||
/**
|
||||
* 表示该消息后是否要加一条AI输入中的消息
|
||||
*/
|
||||
var aiIsSending: Boolean = true
|
||||
|
||||
var fetchType: FetchResult.FetchType = FetchResult.FetchType.Init
|
||||
var loadStatus: LoadStatus = LoadStatus.Success
|
||||
|
||||
companion object {
|
||||
const val BASE_INFO = 0
|
||||
|
||||
// ai回复中
|
||||
const val INPUT_ING = 1
|
||||
|
||||
const val OUT_TEXT_TYPE = 2
|
||||
const val IN_TEXT_TYPE = 3
|
||||
|
||||
const val OUT_IMAGE_TYPE = 4
|
||||
const val IN_IMAGE_TYPE = 5
|
||||
|
||||
const val OUT_GIFT_TYPE = 6
|
||||
|
||||
const val OUT_CALL_TYPE = 7
|
||||
|
||||
/**
|
||||
* 心动等级升级/降级
|
||||
*/
|
||||
const val HEART_BEAT_CHANGED_TYPE = 8
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.remax.visualnovel.entity.imbean
|
||||
|
||||
import com.remax.visualnovel.entity.imbean.raw.CustomRawData
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/19
|
||||
*/
|
||||
data class IMOutImageMessage(
|
||||
override var message: V2NIMMessage?,
|
||||
val customRawData: CustomRawData?
|
||||
) : IMMessageWrapper(type = OUT_IMAGE_TYPE)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.remax.visualnovel.entity.imbean
|
||||
|
||||
import com.netease.nimlib.sdk.v2.conversation.model.V2NIMConversation
|
||||
import com.netease.nimlib.sdk.v2.utils.V2NIMConversationIdUtil
|
||||
|
||||
/**
|
||||
* Created by HJW on 2020/10/9
|
||||
*/
|
||||
data class RecentContactWrapper(
|
||||
var recentContact: V2NIMConversation
|
||||
) {
|
||||
val aiId: String
|
||||
get() {
|
||||
val targetId = V2NIMConversationIdUtil.conversationTargetId(recentContact.conversationId)
|
||||
|
||||
return targetId.substring(0, targetId.indexOf("@"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.remax.visualnovel.entity.imbean.voice
|
||||
|
||||
import com.remax.visualnovel.entity.response.base.BaseVoice
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/28
|
||||
*/
|
||||
data class IMVoice(
|
||||
val code: String,
|
||||
var filePath: String? = null,
|
||||
var url: String? = null,
|
||||
var autoPlay: Boolean = false
|
||||
) : BaseVoice() {
|
||||
|
||||
override fun id(): String {
|
||||
return code
|
||||
}
|
||||
|
||||
override fun url(): String {
|
||||
return url ?: ""
|
||||
}
|
||||
|
||||
override fun filePathName(): String {
|
||||
return filePath ?: ""
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/7/29
|
||||
*/
|
||||
data class AIGenerate(
|
||||
val nickname: String? = null,
|
||||
val birthday: String? = null,
|
||||
val sex: Int? = null,
|
||||
val introduction: String? = null,
|
||||
val characterCode: String? = null,
|
||||
val tagCode: String? = null,
|
||||
val roleCode: String? = null,
|
||||
val content: String? = null,
|
||||
val ptType: String? = null,
|
||||
val figure: String? = null, //ai的人物基础信息(背景、性格、身份)【生成个人简介时使用】
|
||||
val dialogue: String? = null, // ai对话风格(角色的聊天方式、对话语气)【生成个人简介时使用】
|
||||
) {
|
||||
companion object {
|
||||
//AI一键生成人物基础信息 AI自行创作
|
||||
const val GEN_PROFILE_BY_NON = "GEN_PROFILE_BY_NON"
|
||||
|
||||
//AI一键生成人物基础信息 AI根据用户输入进行创作
|
||||
const val GEN_PROFILE_BY_CONTENT = "GEN_PROFILE_BY_CONTENT"
|
||||
|
||||
//AI一键生成对话风格 AI自行创作
|
||||
const val GEN_DIALOG_STYLE_BY_NON = "GEN_DIALOG_STYLE_BY_NON"
|
||||
|
||||
//AI一键生成对话风格 AI根据用户输入进行创作
|
||||
const val GEN_DIALOG_STYLE_BY_CONTENT = "GEN_DIALOG_STYLE_BY_CONTENT"
|
||||
|
||||
//AI一键生成开场白 AI自行创作
|
||||
const val GEN_PROLOGUE_BY_NON = "GEN_PROLOGUE_BY_NON"
|
||||
|
||||
//AI一键生成开场白 AI根据用户输入进行创作
|
||||
const val GEN_PROLOGUE_BY_CONTENT = "GEN_PROLOGUE_BY_CONTENT"
|
||||
|
||||
//AI一键生成人物简介 AI总结
|
||||
const val GEN_INTRODUCTION = "GEN_INTRODUCTION"
|
||||
|
||||
//图生文-参考图生成prompt
|
||||
const val GEN_AI_IMAGE_DESC = "GEN_AI_IMAGE_DESC_BY_NON"
|
||||
|
||||
//编辑AI或相册图片生成时,合并新老形象描述
|
||||
const val MERGE_NEW_OLD_IMAGE_DESC = "MERGE_NEW_OLD_IMAGE_DESC"
|
||||
|
||||
//文生图-生成6组不同的prompt
|
||||
const val TEXT_TO_IMAGE_PROMPT_V2 = "TEXT_TO_IMAGE_PROMPT_V2"
|
||||
|
||||
//图生文-参考图生成prompt
|
||||
const val IMAGE_REFERENCE = "IMAGE_REFERENCE"
|
||||
|
||||
//文生图-生成6组不同的prompt
|
||||
const val TXT_TO_IMAGE_PROMPT = "TXT_TO_IMAGE_PROMPT"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/7/30
|
||||
*/
|
||||
data class AIGenerateImage(
|
||||
val type: String? = null,
|
||||
val aiId: String? = null,
|
||||
val imageStylePrompt: String? = null,
|
||||
val content: String? = null,
|
||||
val imageReferenceUrl: String? = null,
|
||||
val batchNo: String? = null,
|
||||
var hl: Boolean? = null,
|
||||
) {
|
||||
companion object {
|
||||
//创建ai形象
|
||||
const val CREATE_AI_IMAGE = "CREATE_AI_IMAGE"
|
||||
|
||||
//编辑ai形象
|
||||
const val EDIT_AI_IMAGE = "EDIT_AI_IMAGE"
|
||||
|
||||
//相册
|
||||
const val ALBUM = "ALBUM"
|
||||
|
||||
//背景
|
||||
const val BACKGROUND = "BACKGROUND"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/16
|
||||
*/
|
||||
data class AIHeadImgRequest (
|
||||
val aiId:String,
|
||||
val userHead:String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/24
|
||||
*/
|
||||
open class AIIDRequest(
|
||||
open val aiId: String
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/22
|
||||
*/
|
||||
data class AIIsShowDTO(
|
||||
val aiId: String,
|
||||
val isShow: Int //默认关闭 0:关闭 1:打开
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/24
|
||||
*/
|
||||
data class AIListRequest(
|
||||
val aiIdList: List<String>
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2020/11/19
|
||||
*/
|
||||
data class AlbumDTO(
|
||||
val albumId: Long? = null,
|
||||
val likedStatus: String? = null,
|
||||
val liked: Boolean? = null,
|
||||
val userId: String? = null,
|
||||
var height: Int? = null,
|
||||
var url: String? = null,
|
||||
var width: Int? = null,
|
||||
var unlockPrice: Long? = null,
|
||||
val aiId: String? = null,
|
||||
)
|
||||
|
||||
data class AlbumCreate(
|
||||
val aiId: String?,
|
||||
val images: List<AlbumDTO>
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/10
|
||||
*/
|
||||
data class CardRequest(
|
||||
val aiId: String?,
|
||||
val lk: Boolean
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/7/19
|
||||
*/
|
||||
data class ChargeOrderDTO(
|
||||
val chargeAmount: Long,
|
||||
val productId: String,
|
||||
val version: Int = 1,
|
||||
)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/7/19
|
||||
*/
|
||||
data class ChargeProduct(
|
||||
val id: Int,
|
||||
var selected: Boolean = false,
|
||||
var hot: Boolean?,
|
||||
var localCurrencyCode: String = "USD",
|
||||
var local: String?=null,
|
||||
val payAmount: String,
|
||||
|
||||
/**
|
||||
* 充值到账的BUFF金额
|
||||
*/
|
||||
val chargeAmount: Long,
|
||||
|
||||
/**
|
||||
* 赠送的总金额
|
||||
*/
|
||||
val giftAmount: Long,
|
||||
|
||||
/**
|
||||
* 商品ID
|
||||
*/
|
||||
val productId: String
|
||||
)
|
||||
|
||||
|
||||
data class ChargeProductInfo(
|
||||
val productList: List<ChargeProduct>,
|
||||
val countdown: Long? = null
|
||||
)
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
import com.remax.visualnovel.constant.AppConstant
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/7/19
|
||||
*/
|
||||
data class ChargeProductDTO(
|
||||
val platform: String = AppConstant.ANDROID,
|
||||
val version: Int = 1
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/29
|
||||
*/
|
||||
data class ChatAlbum(
|
||||
val aiId: String,
|
||||
val albumId: Long?,
|
||||
val unlockPrice: Long?,
|
||||
val messageServerId: String? = null
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/23
|
||||
*/
|
||||
data class ChatSetting(
|
||||
val aiId: String?,
|
||||
/**
|
||||
* 修改聊天气泡/模型
|
||||
*/
|
||||
val code: String? = null,
|
||||
/**
|
||||
* 修改聊天背景
|
||||
*/
|
||||
val backgroundId: Int? = null,
|
||||
val backgroundImg: String? = null,
|
||||
/**
|
||||
* 自动播放语音开关
|
||||
*/
|
||||
val isAutoPlayVoice: Boolean?= null,
|
||||
)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/9
|
||||
*/
|
||||
data class ClassificationRequest(
|
||||
/**
|
||||
* 情感性格code
|
||||
*/
|
||||
var characterCodeList: List<String>?=null,
|
||||
|
||||
/**
|
||||
* 需要排除的aiId列表
|
||||
*/
|
||||
var exList: MutableList<String> = mutableListOf<String>(),
|
||||
|
||||
/**
|
||||
* 页码
|
||||
*/
|
||||
var pn: Int = 1,
|
||||
|
||||
/**
|
||||
* 年龄
|
||||
*/
|
||||
var age: String? = null,
|
||||
|
||||
/**
|
||||
* 性别:单选
|
||||
*/
|
||||
var sex: Int? = null,
|
||||
|
||||
/**
|
||||
* 角色code列表
|
||||
*/
|
||||
var roleCodeList: List<String>?=null,
|
||||
|
||||
/**
|
||||
* 标签code列表
|
||||
*/
|
||||
var tagCodeList: List<String>?=null,
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
val ps: Int = PageQuery.DEFAULT_PAGE_SIZE,
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/18
|
||||
*/
|
||||
|
||||
@Parcelize
|
||||
data class Gift(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val desc: String?,
|
||||
val icon: String?,
|
||||
val startVal: Double?,
|
||||
val heartbeatLevel: String?, // 发送该礼物需要的心动等级
|
||||
val price: Long,
|
||||
var getNum: Int?,
|
||||
var isMemberGift: Boolean? = null,
|
||||
var select: Boolean = false
|
||||
) : Parcelable
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/22
|
||||
*/
|
||||
data class HeartbeatBuy(
|
||||
val aiId: String?,
|
||||
val heartbeatVal: Double?,
|
||||
)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.remax.visualnovel.entity.response.HeartbeatLevelEnum
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/20
|
||||
*/
|
||||
@Parcelize
|
||||
data class HeartbeatRelation(
|
||||
val aiHeadImg: String, //AI头像
|
||||
val userHeadImg: String, // 用户头像
|
||||
var heartbeatLevel: String?, //心动等级类型
|
||||
var heartbeatLevelName: String?, //心动等级名称
|
||||
var heartbeatLevelNum: Int?, //心动等级
|
||||
val dayCount: Int, //相识天数
|
||||
val price: Long?, //心动值单价
|
||||
var heartbeatVal: Double?, //心动值
|
||||
val subtractHeartbeatVal: Double?, //已扣减心动值
|
||||
var heartbeatScore: Float?, //心动分
|
||||
var isShow: Boolean?,
|
||||
var aiId: String? = null
|
||||
) : Parcelable {
|
||||
|
||||
val currHeartbeatEnum: HeartbeatLevelEnum?
|
||||
get() = HeartbeatLevelEnum.entries.find { it.levelName == heartbeatLevel }
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2020/10/21
|
||||
*/
|
||||
data class ImgCheckDTO(
|
||||
val bizTypeEnum: String,
|
||||
val fileFullPath: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.remax.visualnovel.entity.request;
|
||||
|
||||
/**
|
||||
* Created by Eric on 2020/9/9
|
||||
*/
|
||||
public class PageQuery {
|
||||
|
||||
public static final int DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
public Page page = new Page();
|
||||
|
||||
public PageQuery(int pn) {
|
||||
this.page.pn = pn;
|
||||
}
|
||||
|
||||
public static class Page {
|
||||
|
||||
public int pn = 1;
|
||||
|
||||
public int ps = DEFAULT_PAGE_SIZE;
|
||||
|
||||
public Page() {
|
||||
|
||||
}
|
||||
|
||||
public Page(int pn) {
|
||||
this.pn = pn;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2023/8/31
|
||||
*/
|
||||
data class QueryAlbumDTO(
|
||||
val aiId: String,
|
||||
val userId:String?,
|
||||
val page: PageQuery.Page
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/25
|
||||
*/
|
||||
data class RTCRequest(
|
||||
val roomId: String,
|
||||
/**
|
||||
* 操作类型 开启通话:START,打断:INTERRUPT,结束通话:STOP
|
||||
*/
|
||||
val optType: String? = null,
|
||||
val duration: Long? = null,
|
||||
/**
|
||||
* 任务id
|
||||
*/
|
||||
val taskId: String? = null,
|
||||
val targetId: String? = null
|
||||
) : AIIDRequest(targetId ?: "") {
|
||||
companion object {
|
||||
const val START = "START"
|
||||
const val INTERRUPT = "INTERRUPT"
|
||||
const val STOP = "STOP"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/3/7
|
||||
*/
|
||||
data class S3TypeDTO(
|
||||
val bizTypeEnum: String,
|
||||
val suffix: String
|
||||
) {
|
||||
companion object {
|
||||
const val ROLE = "ROLE"
|
||||
const val ALBUM = "ALBUM"
|
||||
const val HEAD_IMAGE = "HEAD_IMAGE"
|
||||
const val SOUND = "SOUND"
|
||||
const val SOUND_PATH = "SOUND_PATH"
|
||||
const val IM_IMAGE = "IM_IMG"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/27
|
||||
*/
|
||||
data class SearchPage(
|
||||
val page: PageQuery.Page,
|
||||
val nickname: String? = null,
|
||||
val type: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/23
|
||||
*/
|
||||
data class SendGift(
|
||||
val aiId: String,
|
||||
val num: Int,
|
||||
val giftId: Int,
|
||||
val scene: String = IM,
|
||||
) {
|
||||
companion object {
|
||||
const val IM = "IM"
|
||||
const val HOME = "HOME"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
data class SimpleContentDTO(
|
||||
val content: String
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/17
|
||||
*/
|
||||
data class SimpleCountDTO(
|
||||
val count: Int
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/21
|
||||
*/
|
||||
data class SimpleDataDTO(
|
||||
var aiId: String? = null,
|
||||
val data: String? = null,
|
||||
val url: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
data class SimpleTypeDTO(
|
||||
val type: String
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
import com.remax.visualnovel.constant.AppConstant
|
||||
|
||||
/**
|
||||
* Created by HJW on 2021/8/27
|
||||
*/
|
||||
data class SubPriceDTO(
|
||||
val platform: String = AppConstant.ANDROID,
|
||||
val version: String = "2"
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/6/28
|
||||
*/
|
||||
data class ValidateTransactionDTO(
|
||||
val productId: String?,
|
||||
/**
|
||||
* purchase token
|
||||
*/
|
||||
val receipt: String,
|
||||
var orderId: String? = null,
|
||||
var currency: String? = null,
|
||||
var price: Double? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.remax.visualnovel.entity.request
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/28
|
||||
*/
|
||||
data class VoiceTTS(
|
||||
|
||||
var aiId: String? = null,
|
||||
/**
|
||||
* 音量(Volume)。值范围为 [-12, 12]。默认:0
|
||||
*/
|
||||
var pitchRate: String = DEFAULT,
|
||||
|
||||
/**
|
||||
* 语速,范围 [-50,100],100代表2.0倍速,-50代表0.5倍速
|
||||
*/
|
||||
var speechRate: String = DEFAULT,
|
||||
|
||||
/**
|
||||
* 文本内容
|
||||
*/
|
||||
var text: String?,
|
||||
|
||||
/**
|
||||
* 语音类型
|
||||
*/
|
||||
var voiceType: String?
|
||||
) {
|
||||
companion object {
|
||||
const val DEFAULT = "0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/12
|
||||
*/
|
||||
|
||||
data class Transaction(
|
||||
val pageList: Pageable<AccountBuffBill>
|
||||
)
|
||||
|
||||
data class TransactionGift(
|
||||
val giftId: Int? = null
|
||||
)
|
||||
|
||||
data class AccountBuffBill(
|
||||
val amount: Long,
|
||||
val bizNum: String,
|
||||
val bizType: String,
|
||||
val buffType: String,
|
||||
val item: String,
|
||||
val inOrOut: String? = null,
|
||||
val time: Long,
|
||||
val extend: String? = null,
|
||||
val toWithdrawableIncomeTime: Long,
|
||||
val tradeNo: String,
|
||||
) {
|
||||
|
||||
val isIn: Boolean
|
||||
get() = inOrOut == IN
|
||||
|
||||
companion object {
|
||||
const val BALANCE = "BALANCE"
|
||||
const val INCOME = "INCOME"
|
||||
|
||||
const val IN = "IN"
|
||||
const val OUT = "OUT"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/8
|
||||
*/
|
||||
data class AdvertiseOutput(
|
||||
/**
|
||||
* 使用端点(WEB/ANDROID/IOS)
|
||||
* endpoint
|
||||
*/
|
||||
val endpoint: String? = null,
|
||||
|
||||
/**
|
||||
* 扩展字段
|
||||
* ext
|
||||
*/
|
||||
val ext: String? = null,
|
||||
|
||||
/**
|
||||
* 广告配图
|
||||
* icon
|
||||
*/
|
||||
val icon: String? = null,
|
||||
|
||||
/**
|
||||
* 是否弹窗(1.是,0.否)
|
||||
* is_global
|
||||
*/
|
||||
val isGlobal: Long? = null,
|
||||
|
||||
/**
|
||||
* 跳转连接
|
||||
* jump_link
|
||||
*/
|
||||
val jumpLink: String? = null,
|
||||
|
||||
/**
|
||||
* 广告名称
|
||||
* name
|
||||
*/
|
||||
val name: String? = null,
|
||||
|
||||
/**
|
||||
* 展示结束时间
|
||||
* show_end_time
|
||||
*/
|
||||
val showEndTime: String? = null,
|
||||
|
||||
/**
|
||||
* 展示开始时间
|
||||
* show_start_time
|
||||
*/
|
||||
val showStartTime: String? = null,
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* sort
|
||||
*/
|
||||
val sort: Long? = null
|
||||
) {
|
||||
var type: String? = null
|
||||
|
||||
companion object {
|
||||
const val SIGN = "SIGN"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/16
|
||||
*/
|
||||
data class AlbumCreateCountOutput(
|
||||
/**
|
||||
* 购买创作次数
|
||||
*/
|
||||
val buyNum: Int,
|
||||
val usedBuyNum: Int,
|
||||
|
||||
/**
|
||||
* 免费创作次数
|
||||
*/
|
||||
val freeNum: Int,
|
||||
val usedFreeNum: Int,
|
||||
|
||||
/**
|
||||
* 会员赠送创作次数
|
||||
*/
|
||||
val memberNum: Int,
|
||||
val usedMemberNum: Int
|
||||
) {
|
||||
val hasFree: Boolean
|
||||
get() = usedFreeNum < freeNum
|
||||
|
||||
val hasVipTime: Boolean
|
||||
get() = usedMemberNum < memberNum
|
||||
|
||||
val hasNum: Boolean
|
||||
get() = usedBuyNum < buyNum
|
||||
|
||||
val canCreate: Boolean
|
||||
get() = hasFree || hasVipTime || hasNum
|
||||
|
||||
val canUseCount: Int
|
||||
get() = (buyNum - usedBuyNum) + (memberNum - usedMemberNum) + (freeNum - usedFreeNum)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2020/10/20
|
||||
*/
|
||||
data class BucketBean(
|
||||
val expiration: String,
|
||||
val region: String,
|
||||
val requestId: String,
|
||||
val accessKeyId: String,
|
||||
val accessKeySecret: String,
|
||||
val bucket: String,
|
||||
val endPoint: String,
|
||||
val path: String,
|
||||
val urlPath: String,
|
||||
val securityToken: String,
|
||||
var tempTime: Long,
|
||||
var type: String
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.remax.visualnovel.entity.request.HeartbeatRelation
|
||||
import com.remax.visualnovel.extension.calculateAge
|
||||
import com.remax.visualnovel.extension.getNimAccountId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
|
@ -49,8 +50,8 @@ data class Character(
|
|||
var isHaveChatted: Boolean? = null, //是否聊过天
|
||||
var isDelChatted: Boolean? = null, //是否删除过会话
|
||||
var isAutoPlayVoice: Int? = null, //自动播放语音开关 1:开 0:关
|
||||
//var aiUserHeartbeatRelation: HeartbeatRelation? = null,
|
||||
//var chatBubble: ChatBubble? = null,
|
||||
var aiUserHeartbeatRelation: HeartbeatRelation? = null,
|
||||
var chatBubble: ChatBubble? = null,
|
||||
|
||||
//排行榜使用
|
||||
var rankNo: Int? = null,
|
||||
|
|
@ -65,7 +66,7 @@ data class Character(
|
|||
var role: String? = null,
|
||||
var tag: String? = null,
|
||||
var isSecret: Boolean? = null,
|
||||
//var albumList: List<Album>? = null,
|
||||
var albumList: List<Album>? = null,
|
||||
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.remax.visualnovel.entity.response;
|
||||
|
||||
|
||||
public class ChargeOrder {
|
||||
public String tradeNo;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/18
|
||||
*/
|
||||
@Parcelize
|
||||
data class ChatBubble(
|
||||
/**
|
||||
* code
|
||||
*/
|
||||
val code: String,
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
val id: Long,
|
||||
|
||||
/**
|
||||
* 图片url
|
||||
*/
|
||||
val imgUrl: String?,
|
||||
|
||||
/**
|
||||
* 当前用户是否解锁 false:未解锁,true:解锁
|
||||
*/
|
||||
val isUnlock: Boolean? = null,
|
||||
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
val name: String,
|
||||
|
||||
/**
|
||||
* 解锁心动等级 类型为HEARTBEAT_LEVEL时才有用
|
||||
*/
|
||||
val unlockHeartbeatLevel: String? = null,
|
||||
|
||||
/**
|
||||
* 解锁类型 MEMBER:会员 HEARTBEAT_LEVEL:心动等级
|
||||
*/
|
||||
val unlockType: String? = null,
|
||||
var isDefault: Boolean,
|
||||
var select: Boolean = false
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
const val MEMBER = "MEMBER"
|
||||
const val HEARTBEAT_LEVEL = "HEARTBEAT_LEVEL"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/18
|
||||
*/
|
||||
data class ChatModel(
|
||||
/**
|
||||
* 对话模型code
|
||||
*/
|
||||
val code: String? = null,
|
||||
|
||||
/**
|
||||
* 对话模型描述
|
||||
*/
|
||||
val description: String? = null,
|
||||
|
||||
/**
|
||||
* 对话模型名称
|
||||
*/
|
||||
val name: String? = null,
|
||||
|
||||
/**
|
||||
* 问号图标内容
|
||||
*/
|
||||
val questionMark: String? = null,
|
||||
|
||||
/**
|
||||
* 文本价格
|
||||
*/
|
||||
val textPrice: Long? = null,
|
||||
|
||||
/**
|
||||
* 语音聊天价格
|
||||
*/
|
||||
val voiceChatPrice: Long? = null,
|
||||
|
||||
/**
|
||||
* 发送和听语音价格
|
||||
*/
|
||||
val voicePrice: Long? = null,
|
||||
|
||||
)
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/23
|
||||
*/
|
||||
@Parcelize
|
||||
data class ChatSet(
|
||||
/**
|
||||
* ai的Id
|
||||
*/
|
||||
val aiId: String,
|
||||
|
||||
/**
|
||||
* 聊天背景图片
|
||||
*/
|
||||
val backgroundImg: String?,
|
||||
val isDefaultBackground: Boolean?,
|
||||
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
var birthday: Long?,
|
||||
|
||||
/**
|
||||
* 聊天气泡code
|
||||
*/
|
||||
val bubbleCode: String?,
|
||||
|
||||
/**
|
||||
* 聊天气泡名称
|
||||
*/
|
||||
val bubbleName: String?,
|
||||
|
||||
/**
|
||||
* 自动播放语音开关 1:开 0:关
|
||||
*/
|
||||
val isAutoPlayVoice: Int?,
|
||||
|
||||
/**
|
||||
* 对话模型code
|
||||
*/
|
||||
var modelCode: String?,
|
||||
|
||||
/**
|
||||
* 对话模型名称
|
||||
*/
|
||||
var modelName: String?,
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
var nickname: String?,
|
||||
|
||||
/**
|
||||
* 0,男;1,女;2,自定义
|
||||
*/
|
||||
val sex: Int?,
|
||||
|
||||
/**
|
||||
* 我是谁
|
||||
*/
|
||||
var whoAmI: String?
|
||||
): Parcelable
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/7/29
|
||||
*/
|
||||
open class ContentRes(
|
||||
val content: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/8
|
||||
*/
|
||||
data class ExploreInfo(
|
||||
/**
|
||||
* AI总心动值榜单top3
|
||||
*/
|
||||
val aiChatRankTop3List: List<Character>? = null,
|
||||
|
||||
/**
|
||||
* AI总心动值榜单top3
|
||||
*/
|
||||
val aiGiftRankTop3List: List<Character>? = null,
|
||||
|
||||
/**
|
||||
* AI总心动值榜单top3
|
||||
*/
|
||||
val aiHeartbeatRankTop3List: List<Character>? = null,
|
||||
|
||||
/**
|
||||
* 广告列表
|
||||
*/
|
||||
val outputList: List<AdvertiseOutput>? = null
|
||||
)
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/14
|
||||
*/
|
||||
data class Friends(
|
||||
val aiId: String,
|
||||
/**
|
||||
* 出生日期
|
||||
*/
|
||||
val birthday: Long,
|
||||
|
||||
/**
|
||||
* 性格名称
|
||||
*/
|
||||
val characterName: String,
|
||||
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
val headImg: String,
|
||||
|
||||
/**
|
||||
* 心动等级
|
||||
*/
|
||||
val heartbeatLevel: String,
|
||||
|
||||
/**
|
||||
* 心动等级数字
|
||||
*/
|
||||
val heartbeatLevelNum: Int,
|
||||
|
||||
/**
|
||||
* 心动值
|
||||
*/
|
||||
val heartbeatVal: Double,
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
val nickname: String,
|
||||
|
||||
/**
|
||||
* 角色名称
|
||||
*/
|
||||
val roleName: String,
|
||||
|
||||
/**
|
||||
* 0,男;1,女;2,自定义
|
||||
*/
|
||||
val sex: Int,
|
||||
|
||||
/**
|
||||
* 标签名称
|
||||
*/
|
||||
val tagName: String,
|
||||
|
||||
/**
|
||||
* ai所属用户id
|
||||
*/
|
||||
val userId: String,
|
||||
|
||||
val isShow: Boolean
|
||||
)
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.remax.visualnovel.R
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/21
|
||||
*/
|
||||
data class HeartbeatLevel(
|
||||
/**
|
||||
* 心动等级code
|
||||
*/
|
||||
val code: String,
|
||||
/**
|
||||
* 心动等级图标
|
||||
*/
|
||||
val imgUrl: String,
|
||||
/**
|
||||
* 用户是否解锁
|
||||
*/
|
||||
val isUnlock: Boolean,
|
||||
/**
|
||||
* 心动等级名称
|
||||
*/
|
||||
val name: String,
|
||||
val startVal: Double
|
||||
)
|
||||
|
||||
enum class HeartbeatLevelEnum(
|
||||
val levelName: String,
|
||||
val level: Int,
|
||||
val levelContent: String,
|
||||
val startVal: Double,
|
||||
@StringRes val tagName: Int
|
||||
) {
|
||||
|
||||
// 初识勋章
|
||||
LEVEL_1("LEVEL_1", 1, "Lv.1", 0.50, R.string.meet),
|
||||
|
||||
// 发图功能
|
||||
LEVEL_2("LEVEL_2", 2, "Lv.2", 3.50, R.string.meet),
|
||||
|
||||
// 朋友勋章
|
||||
LEVEL_3("LEVEL_3", 3, "Lv.3", 12.00, R.string.friend),
|
||||
|
||||
// 语音通话
|
||||
LEVEL_4("LEVEL_4", 4, "Lv.4", 30.00, R.string.friend),
|
||||
|
||||
// 暧昧勋章
|
||||
LEVEL_5("LEVEL_5", 5, "Lv.5", 90.00, R.string.flirting),
|
||||
|
||||
// 专属礼物
|
||||
LEVEL_6("LEVEL_6", 6, "Lv.6", 270.00, R.string.flirting),
|
||||
|
||||
// 恋人勋章
|
||||
LEVEL_7("LEVEL_7", 7, "Lv.7", 540.00, R.string.couple),
|
||||
|
||||
// 专属聊天气泡
|
||||
LEVEL_8("LEVEL_8", 8, "Lv.8", 990.00, R.string.couple),
|
||||
|
||||
// 结婚勋章
|
||||
LEVEL_9("LEVEL_9", 9, "Lv.9", 1778.00, R.string.married),
|
||||
|
||||
// 自定义形象
|
||||
LEVEL_10("LEVEL_10", 10, "Lv.10", 2957.00, R.string.married),
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
import com.remax.visualnovel.entity.request.HeartbeatRelation
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/21
|
||||
*/
|
||||
data class HeartbeatLevelOutput(
|
||||
/**
|
||||
* 当前用户与AI的心动关系
|
||||
*/
|
||||
val aiUserHeartbeatRelation: HeartbeatRelation,
|
||||
|
||||
/**
|
||||
* 心动等级字典列表
|
||||
*/
|
||||
val heartbeatLeveLDictList: List<HeartbeatLevel>
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/10
|
||||
*/
|
||||
data class MeetSdOutput(
|
||||
/**
|
||||
* 是否能够调用绑定
|
||||
*/
|
||||
val bd: Boolean? = null,
|
||||
/**
|
||||
* 是否能够调用爱慕者推荐
|
||||
*/
|
||||
val rc: Boolean? = null
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/26
|
||||
*/
|
||||
data class MessageListOutput(
|
||||
|
||||
/**
|
||||
* 导致消息发送的业务ID
|
||||
*/
|
||||
val bizId: String,
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
val content: String,
|
||||
|
||||
/**
|
||||
* 消息时间
|
||||
*/
|
||||
val createTime: Long,
|
||||
|
||||
/**
|
||||
* 消息扩展内容
|
||||
*/
|
||||
val extras: String? = null,
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
val id: Long? = null,
|
||||
|
||||
/**
|
||||
* 发送人用户ID
|
||||
*/
|
||||
val sendUserId: Long? = null,
|
||||
|
||||
/**
|
||||
* 消息状态(0未读、1已读)
|
||||
*/
|
||||
val status: Int,
|
||||
|
||||
/**
|
||||
* 消息标题
|
||||
*/
|
||||
val title: String,
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
val type: Int
|
||||
) {
|
||||
companion object {
|
||||
const val UNREAD = 0
|
||||
}
|
||||
|
||||
data class VIPRenewExtra(
|
||||
val expTime: Long
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/26
|
||||
*/
|
||||
data class MessageStatOutput(
|
||||
/**
|
||||
* 最新未读消息内容
|
||||
*/
|
||||
val latestContent: String,
|
||||
|
||||
/**
|
||||
* 最新未读消息时间
|
||||
*/
|
||||
val latestTime: Long,
|
||||
|
||||
/**
|
||||
* 未读数量
|
||||
*/
|
||||
val unRead: Int
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/19
|
||||
*/
|
||||
data class NimBean(
|
||||
val accountId: String,
|
||||
val token: String,
|
||||
val appKey: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.remax.visualnovel.entity.response;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Eric on 2020/9/9
|
||||
*/
|
||||
public class Pageable<T> {
|
||||
|
||||
public static final int DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
public List<T> datas = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 每页条数
|
||||
*/
|
||||
public int ps = DEFAULT_PAGE_SIZE;
|
||||
|
||||
/**
|
||||
* 页码
|
||||
*/
|
||||
public int pn = 1;
|
||||
|
||||
/**
|
||||
* 总数
|
||||
*/
|
||||
public int tc;
|
||||
|
||||
/**
|
||||
* 订单搜索总价
|
||||
*/
|
||||
public long amount;
|
||||
|
||||
/**
|
||||
* 是否还有更多数据?
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean hasMore() {
|
||||
return pn * ps < tc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义每页数量,是否还有更多
|
||||
*/
|
||||
public boolean isEnd(int ps) {
|
||||
return this.pn * ps >= tc;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Created by HJW on 2020/12/2
|
||||
* vip商品列表
|
||||
*/
|
||||
|
||||
@Parcelize
|
||||
data class SubPrice(
|
||||
val chargeAmount: Long,//赠送的BUFF
|
||||
val memberType: String,
|
||||
val discount: String,
|
||||
val payAmount: Long,
|
||||
val monthlyPrice: Long,
|
||||
val period: String,//MONTH QUARTER YEAR
|
||||
val productId: String,
|
||||
var isChecked: Boolean,
|
||||
var billingPeriod: String? = null,
|
||||
var formattedPrice: String? = null,
|
||||
var priceCurrencyCode: String? = null,
|
||||
var priceAmountMicros: Long? = null,
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
const val P1M = "P1M"
|
||||
const val P3M = "P3M"
|
||||
const val P1Y = "P1Y"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/25
|
||||
*/
|
||||
data class Token(
|
||||
val token: String
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2021/8/21
|
||||
*/
|
||||
data class UserSubInfo(
|
||||
var platform: String? = null,
|
||||
var purchaseToken: String? = null,
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
val createTime: Long? = null,
|
||||
|
||||
/**
|
||||
* 编辑时间
|
||||
*/
|
||||
val editTime: Long? = null,
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
val expTime: Long? = null,
|
||||
/**
|
||||
* 会员类型
|
||||
*/
|
||||
val memberType: String? = null,
|
||||
/**
|
||||
* 产品ID 或者说 订阅计划ID
|
||||
*/
|
||||
val productId: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/6/1
|
||||
*/
|
||||
data class VipItemPrivilege(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val desc: String,
|
||||
val img: String,
|
||||
val code: String
|
||||
) {
|
||||
companion object {
|
||||
val privileges =
|
||||
listOf(ADD_CRUSH_COIN, ADD_CREATE_AI, ADD_ALBUM_CREATE, AUTO_PLAY_VOICE, CUSTOM_CHAT_BUBBLE, SPECIAL_GIFT)
|
||||
|
||||
//增加coin
|
||||
const val ADD_CRUSH_COIN = "ADD_CRUSH_COIN"
|
||||
|
||||
//增加创建ai个数
|
||||
const val ADD_CREATE_AI = "ADD_CREATE_AI"
|
||||
|
||||
//增加相册创建数
|
||||
const val ADD_ALBUM_CREATE = "ADD_ALBUM_CREATE"
|
||||
|
||||
//增加自动播放
|
||||
const val AUTO_PLAY_VOICE = "AUTO_PLAY_VOICE"
|
||||
|
||||
//增加自定义气泡
|
||||
const val CUSTOM_CHAT_BUBBLE = "CUSTOM_CHAT_BUBBLE"
|
||||
|
||||
//增加特殊礼物
|
||||
const val SPECIAL_GIFT = "SPECIAL_GIFT"
|
||||
}
|
||||
}
|
||||
|
||||
data class Membership(
|
||||
/**
|
||||
* 用户会员权限列表
|
||||
*/
|
||||
val memberPrivList: List<VipItemPrivilege>? = null,
|
||||
/**
|
||||
* 用户会员信息
|
||||
*/
|
||||
val userMemberInfo: UserSubInfo? = null
|
||||
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/28
|
||||
*/
|
||||
data class VoiceASR(
|
||||
val content:String,
|
||||
val duration:Long,
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.remax.visualnovel.entity.response
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Created by HJW on 2024/8/5
|
||||
*/
|
||||
@Parcelize
|
||||
data class Wallet(
|
||||
/**
|
||||
* 现页面上显示的Balance为可用于付款的金额,实际为后加上的charge字段(用户充值金额)
|
||||
*/
|
||||
var balance: Long,
|
||||
/**
|
||||
* 现页面上显示的Income为用户收入金额,实际为之前定义的balance等字段
|
||||
*/
|
||||
val income: Long?,
|
||||
val withdrawable: Long?, //可提现
|
||||
/**
|
||||
* 待入账收入
|
||||
*/
|
||||
val awaitingIncome: Long?, //待入账
|
||||
) : Parcelable
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.remax.visualnovel.event.model
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/24
|
||||
*/
|
||||
data class OnAILiked(
|
||||
val aiId: String?,
|
||||
val liked: Boolean?
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.remax.visualnovel.event.modular
|
||||
|
||||
import com.remax.visualnovel.entity.request.AIIsShowDTO
|
||||
import com.remax.visualnovel.event.model.OnAILiked
|
||||
import com.pengxr.modular.eventbus.facade.annotation.EventGroup
|
||||
|
||||
/**
|
||||
* Created by HJW on 2023/5/18
|
||||
* 当前用户相关的事件
|
||||
*/
|
||||
@EventGroup(moduleName = "AI", autoClear = true)
|
||||
interface UserAIEvents {
|
||||
|
||||
/**
|
||||
* 用户的AI角色发生了变更
|
||||
* 包括: 创建AI,编辑AI,编辑AI相册,删除AI,修改AI默认图等等
|
||||
*/
|
||||
fun onAICharacterChanges(): String?
|
||||
|
||||
/**
|
||||
* AI修改了默认图
|
||||
*/
|
||||
fun onAIHomeImageChanges(): String
|
||||
|
||||
/**
|
||||
* AI的心动开关改变
|
||||
*/
|
||||
fun onAIHeartIsOpenChanged(): AIIsShowDTO
|
||||
|
||||
/**
|
||||
* AI点赞状态变更
|
||||
*/
|
||||
fun onAILikedChanged(): OnAILiked
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.remax.visualnovel.event.modular
|
||||
|
||||
import com.remax.visualnovel.entity.response.Wallet
|
||||
import com.pengxr.modular.eventbus.facade.annotation.EventGroup
|
||||
|
||||
/**
|
||||
* Created by HJW on 2023/5/18
|
||||
* 用户钱包相关的事件,比如充值
|
||||
*/
|
||||
@EventGroup(moduleName = "wallet", autoClear = true)
|
||||
interface WalletEvents {
|
||||
|
||||
/**
|
||||
* 充值成功
|
||||
*/
|
||||
fun chargeSucceeded()
|
||||
|
||||
/**
|
||||
* 钱包余额更新成功
|
||||
*/
|
||||
fun buffBalanceUpdateSucceeded(): Wallet?
|
||||
|
||||
/**
|
||||
* google订阅成功
|
||||
*/
|
||||
fun onGoogleSubSucceeded(): String?
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.remax.visualnovel.manager.gift
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.remax.visualnovel.api.factory.ServiceFactory
|
||||
import com.remax.visualnovel.entity.request.Gift
|
||||
import com.remax.visualnovel.repository.api.DictRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/9/17
|
||||
*/
|
||||
object GiftManager : DefaultLifecycleObserver {
|
||||
|
||||
var gifts: List<Gift>? = null
|
||||
private set(value) {
|
||||
value?.firstOrNull()?.select = true
|
||||
field = value
|
||||
}
|
||||
|
||||
private val dictRepository by lazy {
|
||||
DictRepository(ServiceFactory.createService())
|
||||
}
|
||||
|
||||
fun initSelect(){
|
||||
gifts?.forEachIndexed { index, gift ->
|
||||
gift.select = index == 0
|
||||
}
|
||||
}
|
||||
|
||||
fun getGift() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
dictRepository.getGiftDict().transformResult({
|
||||
gifts = it?.datas
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
if (gifts == null) {
|
||||
getGift()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,18 @@
|
|||
package com.remax.visualnovel.manager.login
|
||||
|
||||
import com.pengxr.modular.eventbus.generated.events.EventDefineOfUserEvents
|
||||
import com.remax.visualnovel.api.factory.ServiceFactory
|
||||
import com.remax.visualnovel.entity.response.User
|
||||
import com.remax.visualnovel.event.model.OnLoginEvent
|
||||
import com.remax.visualnovel.event.model.tab.MainTab
|
||||
import com.remax.visualnovel.manager.nim.NimManager
|
||||
import com.remax.visualnovel.repository.api.MessageRepository
|
||||
import com.remax.visualnovel.utils.Routers
|
||||
import com.remax.visualnovel.ui.main.MainActivity
|
||||
import com.remax.visualnovel.ui.wallet.manager.WalletManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/7/11
|
||||
|
|
@ -20,6 +27,7 @@ object LoginManager {
|
|||
set(value) {
|
||||
loginInfoSave.putUser(value)
|
||||
field = value
|
||||
WalletManager.refreshWallet()
|
||||
}
|
||||
|
||||
var token: String? = null
|
||||
|
|
@ -51,6 +59,7 @@ object LoginManager {
|
|||
token = null
|
||||
EventDefineOfUserEvents.onLoginStatusChanged().post(OnLoginEvent(OnLoginEvent.LOGOUT))
|
||||
MainActivity.start(MainTab.TAB_BOOKS)
|
||||
NimManager.logout()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,8 +82,16 @@ object LoginManager {
|
|||
this.token = token
|
||||
}
|
||||
|
||||
var contactUnreadCount: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
EventDefineOfUserEvents.onUserUnReadChanged().post(null)
|
||||
}
|
||||
get() {
|
||||
return field + if (NimManager.isLogin) NimManager.totalUnreadCount else 0
|
||||
}
|
||||
|
||||
/*private val messageRepository by lazy {
|
||||
private val messageRepository by lazy {
|
||||
MessageRepository(ServiceFactory.createService())
|
||||
}
|
||||
|
||||
|
|
@ -82,5 +99,5 @@ object LoginManager {
|
|||
CoroutineScope(Dispatchers.Main).launch {
|
||||
messageRepository.getMessageStat()
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.remax.visualnovel.manager.nim
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/20
|
||||
*/
|
||||
class FetchResult<T>(var loadStatus: LoadStatus?) {
|
||||
var type = FetchType.Init
|
||||
var typeIndex = 0
|
||||
var data: T? = null
|
||||
var error: ErrorMsg? = null
|
||||
var extraInfo: Any? = null
|
||||
|
||||
constructor(loadStatus: LoadStatus?, data: T?) : this(loadStatus) {
|
||||
this.data = data
|
||||
}
|
||||
|
||||
constructor(type: FetchType) : this(LoadStatus.Success) {
|
||||
this.type = type
|
||||
}
|
||||
|
||||
constructor(data: T?) : this(LoadStatus.Success) {
|
||||
this.data = data
|
||||
}
|
||||
|
||||
constructor(type: FetchType, data: T?) : this(LoadStatus.Success) {
|
||||
this.type = type
|
||||
this.data = data
|
||||
}
|
||||
|
||||
constructor(code: Int, msg: String?) : this(LoadStatus.Error) {
|
||||
error = ErrorMsg(code, msg)
|
||||
}
|
||||
|
||||
fun setError(code: Int, msg: String?) {
|
||||
loadStatus = LoadStatus.Error
|
||||
error = ErrorMsg(code, msg)
|
||||
}
|
||||
|
||||
fun setError(code: Int, msgRes: Int) {
|
||||
loadStatus = LoadStatus.Error
|
||||
error = ErrorMsg(code, msgRes)
|
||||
}
|
||||
|
||||
fun setFetchType(type: FetchType) {
|
||||
this.type = type
|
||||
}
|
||||
|
||||
fun isSuccess(): Boolean {
|
||||
return loadStatus == LoadStatus.Success
|
||||
}
|
||||
|
||||
fun setStatus(loadStatus: LoadStatus?) {
|
||||
this.loadStatus = loadStatus
|
||||
}
|
||||
|
||||
fun errorMsg(): ErrorMsg? {
|
||||
return error
|
||||
}
|
||||
|
||||
fun getErrorMsg(context: Context): String? {
|
||||
if (error != null) {
|
||||
if (!error?.msg.isNullOrBlank()) {
|
||||
return error?.msg
|
||||
}
|
||||
if (error?.res != null) {
|
||||
return context.resources.getString(error!!.res)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
enum class FetchType {
|
||||
Init, Add, Update, Remind, Remove
|
||||
}
|
||||
|
||||
class ErrorMsg {
|
||||
var code: Int
|
||||
var res = 0
|
||||
var msg: String? = null
|
||||
|
||||
constructor(code: Int, errorRes: Int) {
|
||||
this.code = code
|
||||
res = errorRes
|
||||
}
|
||||
|
||||
constructor(code: Int, msg: String?) {
|
||||
this.code = code
|
||||
this.msg = msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.remax.visualnovel.manager.nim
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/20
|
||||
*/
|
||||
enum class LoadStatus {
|
||||
|
||||
Loading, Error, Success, Finish
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,518 @@
|
|||
package com.remax.visualnovel.manager.nim
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.remax.visualnovel.R
|
||||
import com.remax.visualnovel.app.base.app.CommonApplicationProxy
|
||||
import com.remax.visualnovel.entity.imbean.RecentContactWrapper
|
||||
import com.remax.visualnovel.entity.response.NimBean
|
||||
import com.remax.visualnovel.extension.resumeWithActive
|
||||
import com.remax.visualnovel.extension.toast
|
||||
import com.remax.visualnovel.manager.login.LoginManager
|
||||
import com.remax.visualnovel.ui.main.MainActivity
|
||||
import com.remax.visualnovel.ui.wallet.manager.WalletManager
|
||||
import com.google.gson.Gson
|
||||
import com.netease.nimlib.sdk.NIMClient
|
||||
import com.netease.nimlib.sdk.NotificationFoldStyle
|
||||
import com.netease.nimlib.sdk.Observer
|
||||
import com.netease.nimlib.sdk.RequestCallback
|
||||
import com.netease.nimlib.sdk.StatusBarNotificationConfig
|
||||
import com.netease.nimlib.sdk.msg.MsgService
|
||||
import com.netease.nimlib.sdk.msg.constant.MsgStatusEnum
|
||||
import com.netease.nimlib.sdk.msg.constant.NotificationExtraTypeEnum
|
||||
import com.netease.nimlib.sdk.msg.constant.SessionTypeEnum
|
||||
import com.netease.nimlib.sdk.msg.model.IMMessage
|
||||
import com.netease.nimlib.sdk.v2.V2NIMError
|
||||
import com.netease.nimlib.sdk.v2.auth.V2NIMLoginListener
|
||||
import com.netease.nimlib.sdk.v2.auth.V2NIMLoginService
|
||||
import com.netease.nimlib.sdk.v2.auth.enums.V2NIMLoginClientChange
|
||||
import com.netease.nimlib.sdk.v2.auth.enums.V2NIMLoginStatus
|
||||
import com.netease.nimlib.sdk.v2.auth.model.V2NIMKickedOfflineDetail
|
||||
import com.netease.nimlib.sdk.v2.auth.model.V2NIMLoginClient
|
||||
import com.netease.nimlib.sdk.v2.auth.option.V2NIMLoginOption
|
||||
import com.netease.nimlib.sdk.v2.conversation.V2NIMConversationListener
|
||||
import com.netease.nimlib.sdk.v2.conversation.V2NIMConversationService
|
||||
import com.netease.nimlib.sdk.v2.conversation.enums.V2NIMConversationType
|
||||
import com.netease.nimlib.sdk.v2.conversation.model.V2NIMConversation
|
||||
import com.netease.nimlib.sdk.v2.conversation.params.V2NIMConversationFilter
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessageService
|
||||
import com.netease.nimlib.sdk.v2.message.config.V2NIMMessageConfig
|
||||
import com.netease.nimlib.sdk.v2.message.params.V2NIMSendMessageParams
|
||||
import com.netease.nimlib.sdk.v2.user.V2NIMUserService
|
||||
import com.netease.nimlib.sdk.v2.utils.V2NIMConversationIdUtil
|
||||
import com.pengxr.modular.eventbus.generated.events.EventDefineOfUserEvents
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
/**
|
||||
* Created by HJW on 2020/9/28
|
||||
*/
|
||||
object NimManager : DefaultLifecycleObserver {
|
||||
//没钱发送消息
|
||||
const val SEND_IM_INSUFFICIENT_BALANCE = 20000
|
||||
|
||||
//发消息等级不足
|
||||
const val SEND_IM_LEVEL_ERROR = 20001
|
||||
|
||||
var account = ""
|
||||
private set
|
||||
|
||||
fun log(content: String) {
|
||||
Timber.i("云信Manager $content")
|
||||
}
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
addLoginListener(true)
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
_conversationSyncLiveData.value = false
|
||||
_updateLiveData.value = null
|
||||
_deleteLiveData.value = null
|
||||
_addLiveData.value = null
|
||||
addLoginListener(false)
|
||||
addConversationListener(false)
|
||||
}
|
||||
|
||||
private val loginListener = object : V2NIMLoginListener {
|
||||
override fun onLoginStatus(status: V2NIMLoginStatus?) {
|
||||
when (status) {
|
||||
V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGINED -> {
|
||||
log("已登录")
|
||||
addConversationListener(true)
|
||||
}
|
||||
|
||||
V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGOUT -> {
|
||||
log("已登出")
|
||||
addConversationListener(false)
|
||||
}
|
||||
|
||||
else -> {
|
||||
log("登录状态回调 $status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoginFailed(error: V2NIMError?) {
|
||||
addConversationListener(false)
|
||||
log("onLoginFailed ${error?.code} ${error?.desc}")
|
||||
}
|
||||
|
||||
override fun onKickedOffline(detail: V2NIMKickedOfflineDetail?) {
|
||||
LoginManager.logout()
|
||||
}
|
||||
|
||||
override fun onLoginClientChanged(
|
||||
change: V2NIMLoginClientChange?,
|
||||
clients: List<V2NIMLoginClient?>?
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private val v2NIMConversationService by lazy {
|
||||
NIMClient.getService(V2NIMConversationService::class.java)
|
||||
}
|
||||
|
||||
private val v2NIMLoginService by lazy {
|
||||
NIMClient.getService(V2NIMLoginService::class.java)
|
||||
}
|
||||
|
||||
private val v2MessageService by lazy {
|
||||
NIMClient.getService(V2NIMMessageService::class.java)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 监听云信登录状态
|
||||
*/
|
||||
fun addLoginListener(register: Boolean) {
|
||||
if (register) {
|
||||
v2NIMLoginService.addLoginListener(loginListener)
|
||||
} else {
|
||||
v2NIMLoginService.removeLoginListener(loginListener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册监听会话列表
|
||||
*/
|
||||
private fun addConversationListener(register: Boolean) {
|
||||
if (register) {
|
||||
v2NIMConversationService.addConversationListener(conversationListener)
|
||||
} else {
|
||||
v2NIMConversationService.removeConversationListener(conversationListener)
|
||||
}
|
||||
}
|
||||
|
||||
private val _conversationSyncLiveData = MutableLiveData<Boolean>(false)
|
||||
val conversationSyncLiveData: LiveData<Boolean> = _conversationSyncLiveData
|
||||
|
||||
// 会话变化LiveData,用于通知会话信息变化变更
|
||||
private val _updateLiveData = MutableLiveData<List<RecentContactWrapper>?>()
|
||||
val updateLiveData: LiveData<List<RecentContactWrapper>?> = _updateLiveData
|
||||
|
||||
// 删除会话LiveData,用于通知会话删除结果
|
||||
private val _deleteLiveData = MutableLiveData<List<String>?>()
|
||||
val deleteLiveData: LiveData<List<String>?> = _deleteLiveData
|
||||
|
||||
// 创建会话
|
||||
private val _addLiveData = MutableLiveData<RecentContactWrapper?>()
|
||||
val addLiveData: LiveData<RecentContactWrapper?> = _addLiveData
|
||||
|
||||
private val conversationListener = object : V2NIMConversationListener {
|
||||
/**
|
||||
* 数据同步开始回调。
|
||||
*/
|
||||
override fun onSyncStarted() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据同步结束回调。如果数据同步已开始,建议在数据同步结束后再进行其他会话操作。
|
||||
*/
|
||||
override fun onSyncFinished() {
|
||||
log("可以开始拉会话列表")
|
||||
EventDefineOfUserEvents.onUserUnReadChanged().post(null)
|
||||
MainScope().launch {
|
||||
_conversationSyncLiveData.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据同步失败回调。
|
||||
*/
|
||||
override fun onSyncFailed(error: V2NIMError?) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话成功创建回调。
|
||||
*/
|
||||
override fun onConversationCreated(conversation: V2NIMConversation) {
|
||||
log("会话成功创建 $conversation")
|
||||
_addLiveData.value = RecentContactWrapper(conversation)
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动删除会话回调。
|
||||
*/
|
||||
override fun onConversationDeleted(conversationIds: List<String>) {
|
||||
log("会话被删除 $conversationIds")
|
||||
_deleteLiveData.value = conversationIds
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话变更回调。当置顶会话、会话有新消息、主动更新会话成功时会触发该回调。
|
||||
*/
|
||||
override fun onConversationChanged(conversationList: List<V2NIMConversation>) {
|
||||
log("会话变更回调 $conversationList")
|
||||
_updateLiveData.value = conversationList.map { RecentContactWrapper(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息总未读数变更回调。
|
||||
*/
|
||||
override fun onTotalUnreadCountChanged(unreadCount: Int) {
|
||||
log("会话消息总未读数变更 $unreadCount")
|
||||
EventDefineOfUserEvents.onUserUnReadChanged().post(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤后的未读数变更回调。调用 subscribeUnreadCountByFilter 方法订阅监听后,当会话过滤后的未读数变化时会返回该回调。
|
||||
*/
|
||||
override fun onUnreadCountChangedByFilter(
|
||||
filter: V2NIMConversationFilter?,
|
||||
unreadCount: Int
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一账号多端登录后的会话已读时间戳标记的回调。
|
||||
*/
|
||||
override fun onConversationReadTimeUpdated(conversationId: String?, readTime: Long) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录云信
|
||||
*/
|
||||
fun login(nimBean: NimBean) {
|
||||
v2NIMLoginService.login(
|
||||
nimBean.accountId, nimBean.token, V2NIMLoginOption().apply {
|
||||
retryCount = 3
|
||||
}, {
|
||||
log("login调用成功")
|
||||
account = nimBean.accountId
|
||||
loginSuccess()
|
||||
},
|
||||
{ error ->
|
||||
val code = error.code
|
||||
val desc = error.desc
|
||||
log("login调用失败 onFailed errorCode:$code desc:$desc")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fun logout() {
|
||||
// 请勿在 Activity 的 `onDestroy` 中调用 `logout` 方法
|
||||
v2NIMLoginService.logout({
|
||||
log("logout调用成功")
|
||||
LoginManager.contactUnreadCount = 0
|
||||
}, { error ->
|
||||
val code = error.code
|
||||
val desc = error.desc
|
||||
log("logout调用失败 onFailed errorCode:$code desc:$desc")
|
||||
})
|
||||
}
|
||||
|
||||
private var offset = 0L
|
||||
|
||||
/**
|
||||
* 分页获取所有会话列表
|
||||
*/
|
||||
data class Conversation(
|
||||
val list: List<V2NIMConversation>,
|
||||
val hasMore: Boolean
|
||||
)
|
||||
|
||||
var allConversation: MutableList<RecentContactWrapper>? = null
|
||||
|
||||
private var queryConversationStart = false
|
||||
|
||||
fun getUserList(accountIds: List<String?>?) {
|
||||
NIMClient.getService(V2NIMUserService::class.java).getUserList(accountIds?.filterNotNull(), {
|
||||
log("获取用户信息成功 $accountIds")
|
||||
}) {
|
||||
log("获取用户信息失败 onFailed errorCode:${it.code} desc:$${it.desc}")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getConversationList(isRefresh: Boolean) =
|
||||
suspendCancellableCoroutine { coroutine ->
|
||||
if (queryConversationStart) {
|
||||
log("queryConversation,has Started return")
|
||||
}
|
||||
queryConversationStart = true
|
||||
coroutine.invokeOnCancellation {
|
||||
coroutine.resumeWithActive(Conversation(emptyList(), false))
|
||||
queryConversationStart = false
|
||||
}
|
||||
if (isRefresh) {
|
||||
allConversation = null
|
||||
offset = 0L
|
||||
}
|
||||
val pageLimit = 100
|
||||
v2NIMConversationService.getConversationList(offset, pageLimit, {
|
||||
offset = it.offset
|
||||
val conversationList = it.conversationList
|
||||
val hasMore = conversationList.size == pageLimit
|
||||
log("拉到的会话列表 $conversationList")
|
||||
queryConversationStart = false
|
||||
coroutine.resumeWithActive(Conversation(conversationList, hasMore))
|
||||
}) {
|
||||
queryConversationStart = false
|
||||
log("拉取会话列表失败 onFailed errorCode:${it.code} desc:$${it.desc}")
|
||||
coroutine.resumeWithActive(Conversation(emptyList(), true))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话
|
||||
* @param conversationId 会话 ID
|
||||
*/
|
||||
fun getConversation(conversationId: String, success: () -> Unit, error: (errorCode: Int) -> Unit) {
|
||||
v2NIMConversationService.getConversation(conversationId, { conversation ->
|
||||
success.invoke()
|
||||
log("获取会话${conversationId}成功: $conversation")
|
||||
}) {
|
||||
error.invoke(it.code)
|
||||
log("获取会话${conversationId}失败 onFailed errorCode:${it.code} desc:$${it.desc}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
*/
|
||||
fun createConversation(accountId: String?, success: () -> Unit, error: (errorCode: Int) -> Unit) {
|
||||
val conversationId = V2NIMConversationIdUtil.p2pConversationId(accountId)
|
||||
v2NIMConversationService.createConversation(conversationId, { conversation ->
|
||||
log("创建会话成功: $conversation")
|
||||
success.invoke()
|
||||
}) {
|
||||
error.invoke(it.code)
|
||||
log("创建${conversationId}会话失败accountId:$accountId onFailed errorCode:${it.code} desc:$${it.desc}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录连接状态
|
||||
* IM 登录连接状态表示当前登录的 NIM SDK 实例与网易云信服务端的长连接状态,也可以理解为用户客户端和网易云信服务端的网络连接状态。
|
||||
* V2NIM_CONNECT_STATUS_DISCONNECTED(0) SDK 未连接服务端
|
||||
* V2NIM_CONNECT_STATUS_CONNECTED(1) SDK 已连接服务端
|
||||
* V2NIM_CONNECT_STATUS_CONNECTING(2) SDK 正在与服务端连接
|
||||
* V2NIM_CONNECT_STATUS_WAITING(3) SDK 正在等待与服务端重连
|
||||
*/
|
||||
fun getConnectStatus() = v2NIMLoginService.connectStatus
|
||||
|
||||
val isLogin: Boolean
|
||||
get() = v2NIMLoginService.loginStatus == V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGINED
|
||||
|
||||
private fun loginSuccess() {
|
||||
log("loginSuccess 当前状态:${v2NIMLoginService.loginStatus}")
|
||||
when (v2NIMLoginService.loginStatus) {
|
||||
V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGINED -> {
|
||||
addConversationListener(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//是否需要开启通知栏消息提醒, MessageAlter已经被去掉,这里需要直接设置为开启
|
||||
val config = StatusBarNotificationConfig().apply {
|
||||
notificationExtraType = NotificationExtraTypeEnum.MESSAGE
|
||||
notificationSmallIconId = R.mipmap.book_archive
|
||||
notificationEntrance = MainActivity::class.java
|
||||
notificationFoldStyle = NotificationFoldStyle.CONTACT
|
||||
ring = false
|
||||
vibrate = false
|
||||
downTimeToggle = false
|
||||
}
|
||||
NIMClient.updateStatusBarNotificationConfig(config)
|
||||
NIMClient.toggleNotification(true)
|
||||
setPushAlter(true)
|
||||
updateMyIMUserInfo()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话总未读数
|
||||
*/
|
||||
val totalUnreadCount: Int
|
||||
get() {
|
||||
val unreadCount = v2NIMConversationService.totalUnreadCount
|
||||
log("会话消息总未读数 $unreadCount")
|
||||
return unreadCount
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 标记会话已读
|
||||
* 并触发 onTotalUnreadCountChanged、onConversationChanged 和 onUnreadCountChangedByFilter 回调,同步数据库和缓存。
|
||||
*/
|
||||
fun clearUnreadCountByIds(conversationId: String) {
|
||||
v2NIMConversationService.clearUnreadCountByIds(listOf(conversationId), {
|
||||
log("标记会话已读成功 ${Gson().toJson(it)}")
|
||||
}) {
|
||||
log("标记会话已读失败 onFailed errorCode:${it.code} desc:$${it.desc}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 未读数清零
|
||||
*/
|
||||
fun clearTotalUnreadCount() {
|
||||
v2NIMConversationService.clearTotalUnreadCount({
|
||||
log("未读数清零成功")
|
||||
}) {
|
||||
log("未读数清零失败 onFailed errorCode:${it.code} desc:$${it.desc}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyIMUserInfo() {
|
||||
// val user = getUserInfo(account)
|
||||
// try {
|
||||
// user?.extensionMap?.let { extension ->
|
||||
//
|
||||
// val fields = kotlin.collections.HashMap<UserInfoFieldEnum, Any>().apply {
|
||||
// put(UserInfoFieldEnum.EXTEND, Gson().toJson(extension))
|
||||
// }
|
||||
// NIMClient.getService(UserService::class.java).updateUserInfo(fields)
|
||||
// .setCallback(object : RequestCallbackWrapper<Void>() {
|
||||
// override fun onResult(code: Int, result: Void?, exception: Throwable?) {
|
||||
// Timber.d("updateUserInfo code: $code}")
|
||||
// Timber.d("updateUserInfo res: ${user.extension}")
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// } catch (e: Exception) {
|
||||
// Timber.d("NIMClient-updateUserInfo-error : ${e.localizedMessage}")
|
||||
// }
|
||||
}
|
||||
|
||||
private val _messageStatus = MutableLiveData<IMMessage>()
|
||||
val messageStatus: LiveData<IMMessage> = _messageStatus
|
||||
|
||||
private val messageStatusObserver: Observer<IMMessage> = Observer<IMMessage> {
|
||||
Timber.d("observeMsgStatus:${Gson().toJson(it)}")
|
||||
if (it.status == MsgStatusEnum.success) {
|
||||
// 1、根据sessionId判断是否是自己的消息
|
||||
// 2、更改内存中消息的状态
|
||||
// 3、刷新界面
|
||||
_messageStatus.value = it
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun v2SendMessage(v2Message: V2NIMMessage, accountId: String?, errorCallback: ((Int) -> Unit)? = null) {
|
||||
val conversationId =
|
||||
V2NIMConversationIdUtil.conversationId(accountId, V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P)
|
||||
|
||||
val messageConfig = V2NIMMessageConfig.V2NIMMessageConfigBuilder
|
||||
.builder()
|
||||
.build()
|
||||
val sendMessageParams = V2NIMSendMessageParams.V2NIMSendMessageParamsBuilder
|
||||
.builder()
|
||||
.withMessageConfig(messageConfig)
|
||||
.build()
|
||||
v2MessageService.sendMessage(v2Message, conversationId, sendMessageParams, { result ->
|
||||
val message = result.message
|
||||
log("发送消息成功: $message")
|
||||
}, { failure ->
|
||||
log("发送消息, code: " + failure.code + ", message: " + failure.desc)
|
||||
when (failure.code) {
|
||||
SEND_IM_INSUFFICIENT_BALANCE -> {
|
||||
CommonApplicationProxy.application.toast(R.string.insufficient_balance)
|
||||
WalletManager.refreshWallet()
|
||||
}
|
||||
}
|
||||
errorCallback?.invoke(failure.code)
|
||||
}) { progress ->
|
||||
log("发送消息进度: $progress")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开、关闭推送服务
|
||||
*/
|
||||
fun setPushAlter(isOpen: Boolean, callback: RequestCallback<Void>? = null) {
|
||||
|
||||
}
|
||||
|
||||
fun setChattingAccount(account: String) {
|
||||
// 进入聊天界面,建议放在onResume中。表示来自account的消息无需进行消息提醒。
|
||||
NIMClient.getService(MsgService::class.java).setChattingAccount(account, SessionTypeEnum.P2P)
|
||||
}
|
||||
|
||||
fun setChattingAccountAll() {
|
||||
// 进入最近联系人列表界面,建议放在onResume中。表示所有消息无需进行消息提醒。
|
||||
NIMClient.getService(MsgService::class.java)
|
||||
.setChattingAccount(MsgService.MSG_CHATTING_ACCOUNT_ALL, SessionTypeEnum.None)
|
||||
}
|
||||
|
||||
fun setChattingAccountNone() {
|
||||
// 退出聊天界面或离开最近联系人列表界面,建议放在onPause中。表示所有消息都可以进行消息提醒。
|
||||
NIMClient.getService(MsgService::class.java)
|
||||
.setChattingAccount(MsgService.MSG_CHATTING_ACCOUNT_NONE, SessionTypeEnum.None)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,476 @@
|
|||
package com.remax.visualnovel.manager.pay
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams
|
||||
import com.android.billingclient.api.BillingClient
|
||||
import com.android.billingclient.api.BillingClientStateListener
|
||||
import com.android.billingclient.api.BillingFlowParams
|
||||
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
|
||||
import com.android.billingclient.api.BillingResult
|
||||
import com.android.billingclient.api.ConsumeParams
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||
import com.android.billingclient.api.QueryProductDetailsParams
|
||||
import com.android.billingclient.api.QueryPurchasesParams
|
||||
import com.remax.visualnovel.R
|
||||
import com.remax.visualnovel.api.factory.ServiceFactory
|
||||
import com.remax.visualnovel.app.base.BaseBindingActivity
|
||||
import com.remax.visualnovel.app.base.app.CommonApplicationProxy
|
||||
import com.remax.visualnovel.constant.StatusCode
|
||||
import com.remax.visualnovel.entity.request.ChargeProduct
|
||||
import com.remax.visualnovel.entity.request.ValidateTransactionDTO
|
||||
import com.remax.visualnovel.entity.response.SubPrice
|
||||
import com.remax.visualnovel.entity.response.base.parseData
|
||||
import com.remax.visualnovel.extension.launchFlow
|
||||
import com.remax.visualnovel.repository.api.PayRepository
|
||||
import com.remax.visualnovel.ui.wallet.manager.WalletManager
|
||||
import com.remax.visualnovel.utils.TimeUtils
|
||||
import com.remax.visualnovel.utils.analytics.AnalyticsUtils
|
||||
import com.google.gson.Gson
|
||||
import com.pengxr.modular.eventbus.generated.events.EventDefineOfWalletEvents
|
||||
import com.remax.visualnovel.configs.NovelApplication
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/6/28
|
||||
*/
|
||||
object GooglePayManager : PurchasesUpdatedListener {
|
||||
|
||||
private val payRepository by lazy {
|
||||
PayRepository(ServiceFactory.createService())
|
||||
}
|
||||
|
||||
private var isConnected = false
|
||||
|
||||
private var billingClient: BillingClient? = null
|
||||
|
||||
private var queryProductTime = 0L
|
||||
|
||||
var productDetails = listOf<ProductDetails?>()
|
||||
private set
|
||||
var subProductDetails: MutableList<ProductDetails> = arrayListOf()
|
||||
private set
|
||||
|
||||
private val mHandler: Handler = object : Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
if (msg.what == 100) {
|
||||
queryPurchases()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startConnection(callback: () -> Unit, errorCallback: (() -> Unit)? = null) {
|
||||
if (billingClient == null) {
|
||||
billingClient =
|
||||
BillingClient.newBuilder(CommonApplicationProxy.application).enablePendingPurchases().setListener(this)
|
||||
.build()
|
||||
}
|
||||
if (isConnected && billingClient?.isReady == true) {
|
||||
Timber.d("Google Play正常连接")
|
||||
callback.invoke()
|
||||
} else {
|
||||
billingClient?.startConnection(object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
isConnected = true
|
||||
callback.invoke()
|
||||
} else {
|
||||
isConnected = false
|
||||
errorCallback?.invoke()
|
||||
Timber.e("GooglePay连接失败")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
isConnected = false
|
||||
// Try to restart the connection on the next request to
|
||||
// Google Play by calling the startConnection() method.
|
||||
Timber.e("GooglePay断开连接")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查本地商品是否为空
|
||||
*/
|
||||
fun checkProductDetails() {
|
||||
startConnection({
|
||||
/**
|
||||
* 如果距离上次查询时间超过10分钟,则清空重新查询,防止商品token过期
|
||||
*/
|
||||
if (System.currentTimeMillis() - queryProductTime > 10 * TimeUtils.ONE_MINUTE) {
|
||||
queryProductTime = System.currentTimeMillis()
|
||||
queryAllProductDetails()
|
||||
} else {
|
||||
if (productDetails.isEmpty()) {
|
||||
queryAllProductDetails(isSubs = false)
|
||||
}
|
||||
if (subProductDetails.isEmpty()) {
|
||||
queryAllProductDetails(isConsume = false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private var productList = arrayListOf<ChargeProduct>()
|
||||
private var subList = arrayListOf<SubPrice>()
|
||||
|
||||
private fun queryProductDetails() {
|
||||
val immutableList = arrayListOf<QueryProductDetailsParams.Product>()
|
||||
productList.forEach { product ->
|
||||
immutableList.add(
|
||||
QueryProductDetailsParams.Product.newBuilder()
|
||||
.setProductId(product.productId)
|
||||
.setProductType(BillingClient.ProductType.INAPP)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (immutableList.isNotEmpty()) {
|
||||
val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(immutableList)
|
||||
.build()
|
||||
billingClient?.queryProductDetailsAsync(
|
||||
queryProductDetailsParams
|
||||
) { billingResult, productDetailsList ->
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
productDetailsList.forEach {
|
||||
Timber.d("GooglePay 查询一次性商品本地化 : $it")
|
||||
}
|
||||
productDetails = productDetailsList
|
||||
queryPurchases()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun querySubProductDetails() {
|
||||
val immutableList = arrayListOf<QueryProductDetailsParams.Product>()
|
||||
subList.forEach { product ->
|
||||
immutableList.add(
|
||||
QueryProductDetailsParams.Product.newBuilder()
|
||||
.setProductId(product.productId)
|
||||
.setProductType(BillingClient.ProductType.SUBS)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
if (immutableList.isNotEmpty()) {
|
||||
val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(immutableList)
|
||||
.build()
|
||||
billingClient?.queryProductDetailsAsync(
|
||||
queryProductDetailsParams
|
||||
) { billingResult, productDetailsList ->
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
productDetailsList.forEach {
|
||||
Timber.d("GooglePay 查询订阅商品本地化 : $it")
|
||||
}
|
||||
subProductDetails.clear()
|
||||
subProductDetails.addAll(productDetailsList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有商品
|
||||
*/
|
||||
fun queryAllProductDetails(isConsume: Boolean = true, isSubs: Boolean = true) {
|
||||
if (isConsume) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
launchFlow({
|
||||
payRepository.getProducts()
|
||||
}).collect {
|
||||
it.parseData({
|
||||
onSuccess = { productInfo ->
|
||||
productList.clear()
|
||||
productList.addAll(productInfo?.productList ?: emptyList())
|
||||
queryProductDetails()
|
||||
}
|
||||
|
||||
onFailed = { _, errorMsg ->
|
||||
Timber.e("GooglePay google支付单例查询充值接口报错 msg:${errorMsg}")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSubs) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
launchFlow({
|
||||
payRepository.getSubPriceList()
|
||||
}).collect {
|
||||
it.parseData({
|
||||
onSuccess = { group ->
|
||||
subList.clear()
|
||||
subList.addAll(group?: emptyList())
|
||||
querySubProductDetails()
|
||||
}
|
||||
|
||||
onFailed = { _, errorMsg ->
|
||||
Timber.e("GooglePay google支付单例查询订阅商品接口报错 msg:${errorMsg}")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起支付
|
||||
*/
|
||||
fun pay(
|
||||
activity: Activity,
|
||||
productId: String,
|
||||
isSub: Boolean = false,
|
||||
oldPurchaseToken: String = "",
|
||||
tradeNo: String = "",
|
||||
errorCallback: (() -> Unit)? = null
|
||||
) {
|
||||
startConnection({
|
||||
queryProductDetails(
|
||||
activity,
|
||||
productId,
|
||||
if (isSub) BillingClient.ProductType.SUBS else BillingClient.ProductType.INAPP,
|
||||
oldPurchaseToken,
|
||||
tradeNo,
|
||||
errorCallback
|
||||
)
|
||||
}) {
|
||||
errorCallback?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询商品
|
||||
*/
|
||||
private fun queryProductDetails(
|
||||
activity: Activity,
|
||||
productId: String,
|
||||
type: String,
|
||||
oldPurchaseToken: String = "",
|
||||
tradeNo: String = "",
|
||||
errorCallback: (() -> Unit)? = null
|
||||
) {
|
||||
val queryProductDetailsParams =
|
||||
QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(
|
||||
arrayListOf(
|
||||
QueryProductDetailsParams.Product.newBuilder()
|
||||
.setProductId(productId)
|
||||
.setProductType(type)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
.build()
|
||||
|
||||
billingClient?.queryProductDetailsAsync(
|
||||
queryProductDetailsParams
|
||||
) { billingResult, productDetailsList ->
|
||||
// check billingResult
|
||||
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||
Timber.e("GooglePay queryProductDetailsAsync onFailed")
|
||||
errorCallback?.invoke()
|
||||
} else {
|
||||
/**
|
||||
* 启动购买流程
|
||||
*/
|
||||
productDetailsList.forEach { productDetails ->
|
||||
val flowParamsBuilder = BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
// retrieve a value for "productDetails" by calling queryProductDetailsAsync()
|
||||
.setProductDetails(productDetails)
|
||||
if (type == BillingClient.ProductType.SUBS) {
|
||||
val offerToken = productDetails?.subscriptionOfferDetails?.get(0)?.offerToken ?: ""
|
||||
flowParamsBuilder.setOfferToken(offerToken)
|
||||
}
|
||||
val flowParams = flowParamsBuilder.build()
|
||||
val billingFlowParamsBuilder =
|
||||
BillingFlowParams.newBuilder().setProductDetailsParamsList(arrayListOf(flowParams))
|
||||
if (type == BillingClient.ProductType.SUBS && oldPurchaseToken.isNotEmpty()) {
|
||||
billingFlowParamsBuilder.setSubscriptionUpdateParams(
|
||||
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
|
||||
// purchaseToken can be found in Purchase#getPurchaseToken
|
||||
.setOldPurchaseToken(oldPurchaseToken)
|
||||
.setSubscriptionReplacementMode(ReplacementMode.DEFERRED)
|
||||
// .setReplaceProrationMode(BillingFlowParams.ProrationMode.DEFERRED)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
val billingFlowParams = billingFlowParamsBuilder
|
||||
.setObfuscatedAccountId(tradeNo)
|
||||
.setObfuscatedProfileId(tradeNo)
|
||||
.build()
|
||||
// Launch the billing flow
|
||||
val billingFlowParamsResult = billingClient?.launchBillingFlow(activity, billingFlowParams)
|
||||
if (billingFlowParamsResult?.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||
Timber.e("GooglePay launchBillingFlow onFailed")
|
||||
errorCallback?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将购买操作的结果传送给实现 PurchasesUpdatedListener 接口的监听器
|
||||
*/
|
||||
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
|
||||
Timber.d("GooglePay 购买操作结果回调 billingResult:$billingResult res:${Gson().toJson(purchases)} ")
|
||||
when {
|
||||
billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null -> {
|
||||
queryPurchases()
|
||||
}
|
||||
|
||||
billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED -> {
|
||||
//取消购买
|
||||
}
|
||||
|
||||
else -> {
|
||||
// showError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询购买交易记录
|
||||
*/
|
||||
fun queryPurchases() {
|
||||
startConnection({
|
||||
billingClient?.queryPurchasesAsync(
|
||||
QueryPurchasesParams.newBuilder()
|
||||
.setProductType(BillingClient.ProductType.INAPP)
|
||||
.build()
|
||||
) { billingResult, purchases ->
|
||||
Timber.d("GooglePay 查询一次性消费商品 billingResult:$billingResult res:$purchases} ")
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
if (purchases.isNotEmpty()) {
|
||||
purchases.forEach {
|
||||
handlePurchase(it, BillingClient.ProductType.INAPP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
billingClient?.queryPurchasesAsync(
|
||||
QueryPurchasesParams.newBuilder()
|
||||
.setProductType(BillingClient.ProductType.SUBS)
|
||||
.build()
|
||||
) { billingResult, purchases ->
|
||||
Timber.d("GooglePay 查询订阅商品 billingResult:$billingResult res:${Gson().toJson(purchases)} ")
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases.isNotEmpty()) {
|
||||
for (purchase in purchases) {
|
||||
handlePurchase(purchase, BillingClient.ProductType.SUBS)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消费
|
||||
*/
|
||||
private fun handlePurchase(purchase: Purchase, type: String) {
|
||||
if (!purchase.isAcknowledged) {
|
||||
//订阅
|
||||
if (type == BillingClient.ProductType.SUBS) {
|
||||
val build = AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(purchase.purchaseToken)
|
||||
.build()
|
||||
billingClient?.acknowledgePurchase(build) { billingResult ->
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
//订阅成功
|
||||
EventDefineOfWalletEvents.onGoogleSubSucceeded().post(purchase.purchaseToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
//一次性消费
|
||||
else {
|
||||
val consumeParams = ConsumeParams.newBuilder()
|
||||
.setPurchaseToken(purchase.purchaseToken)
|
||||
.build()
|
||||
billingClient?.consumeAsync(consumeParams) { billingResult, purchaseToken ->
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
//消费成功
|
||||
try {
|
||||
var currency = "USD"
|
||||
var price = 0.00
|
||||
productDetails.find { it?.productId == purchase.products[0] }?.let { productDetails ->
|
||||
currency = productDetails.oneTimePurchaseOfferDetails?.priceCurrencyCode ?: "USD"
|
||||
price =
|
||||
BigDecimal(
|
||||
productDetails.oneTimePurchaseOfferDetails?.priceAmountMicros ?: 0L
|
||||
).divide(
|
||||
BigDecimal(1000000),
|
||||
2,
|
||||
RoundingMode.DOWN
|
||||
).setScale(2, BigDecimal.ROUND_DOWN).toDouble()
|
||||
}
|
||||
val orderId = purchase.orderId
|
||||
val productId = purchase.products[0]
|
||||
Timber.d("GooglePay 本次消费的数据 orderId:$orderId productId:$productId currency:$currency price:$price")
|
||||
validateToken(purchaseToken, productId, orderId, currency, price)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("GooglePay 消费回调报错 msg:${e.localizedMessage}")
|
||||
}
|
||||
} else {
|
||||
//消费失败, 后面查询消费记录后再次消费,否则,就只能等待退款
|
||||
Timber.d("GooglePay consumeAsync failed")
|
||||
mHandler.removeMessages(100)
|
||||
mHandler.sendEmptyMessageDelayed(100, TimeUtils.ONE_MINUTE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端入账,并且查询钱包
|
||||
* 验证支付结果, 如果成功, 则服务器端增加buff
|
||||
*/
|
||||
private fun validateToken(
|
||||
purchaseToken: String,
|
||||
productId: String,
|
||||
orderId: String?,
|
||||
currency: String,
|
||||
price: Double
|
||||
) {
|
||||
val currActivity = NovelApplication.getCurrentActivity()
|
||||
val dto = ValidateTransactionDTO(productId, purchaseToken)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
launchFlow({
|
||||
payRepository.validateTranslation(dto)
|
||||
}, {
|
||||
(currActivity as? BaseBindingActivity<*>)?.showLoading()
|
||||
}) {
|
||||
(currActivity as? BaseBindingActivity<*>)?.hideLoading()
|
||||
}.collect {
|
||||
it.parseData({
|
||||
onSuccess = {
|
||||
(currActivity as? BaseBindingActivity<*>)?.showToast(R.string.charge_succeeded)
|
||||
AnalyticsUtils.logAnalytics("Charge_CheckOut_Success")
|
||||
//充值成功通知
|
||||
EventDefineOfWalletEvents.chargeSucceeded().post(null)
|
||||
WalletManager.refreshWallet()
|
||||
}
|
||||
|
||||
onFailed = { errorCode, _ ->
|
||||
if (errorCode == StatusCode.UNUSED_PURCHASE_TOKEN.code) {
|
||||
WalletManager.refreshWallet()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
package com.remax.visualnovel.repository.api
|
||||
|
||||
import com.remax.visualnovel.R
|
||||
import com.remax.visualnovel.api.service.AIService
|
||||
import com.remax.visualnovel.app.base.app.CommonApplicationProxy
|
||||
import com.remax.visualnovel.entity.request.AIGenerate
|
||||
import com.remax.visualnovel.entity.request.AIGenerateImage
|
||||
import com.remax.visualnovel.entity.request.AIHeadImgRequest
|
||||
import com.remax.visualnovel.entity.request.AIIDRequest
|
||||
import com.remax.visualnovel.entity.request.AlbumCreate
|
||||
import com.remax.visualnovel.entity.request.AlbumDTO
|
||||
import com.remax.visualnovel.entity.request.CardRequest
|
||||
import com.remax.visualnovel.entity.request.ChatAlbum
|
||||
import com.remax.visualnovel.entity.request.ClassificationRequest
|
||||
import com.remax.visualnovel.entity.request.PageQuery
|
||||
import com.remax.visualnovel.entity.request.QueryAlbumDTO
|
||||
import com.remax.visualnovel.entity.request.SimpleCountDTO
|
||||
import com.remax.visualnovel.entity.response.Album
|
||||
import com.remax.visualnovel.entity.response.Character
|
||||
import com.remax.visualnovel.entity.response.base.ApiFailedResponse
|
||||
import com.remax.visualnovel.entity.response.base.Response
|
||||
import com.remax.visualnovel.event.model.OnAILiked
|
||||
import com.remax.visualnovel.extension.toast
|
||||
import com.remax.visualnovel.manager.login.LoginManager
|
||||
import com.remax.visualnovel.repository.api.base.BaseRepository
|
||||
import com.remax.visualnovel.ui.wallet.manager.WalletManager
|
||||
import com.pengxr.modular.eventbus.generated.events.EventDefineOfUserAIEvents
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/11/15
|
||||
*/
|
||||
class AIRepository @Inject constructor(private val aiService: AIService) : BaseRepository() {
|
||||
|
||||
suspend fun cardBind(aiId: String) = executeHttp {
|
||||
aiService.cardBind(AIIDRequest(aiId))
|
||||
}
|
||||
|
||||
suspend fun cardLiked() = executeHttp {
|
||||
aiService.cardLiked()
|
||||
}
|
||||
|
||||
suspend fun reportCard(request: CardRequest) = executeHttp(false) {
|
||||
aiService.reportCard(request)
|
||||
}
|
||||
|
||||
suspend fun getHomeCard(request: ClassificationRequest) = executeHttp {
|
||||
aiService.getHomeCard(request)
|
||||
}
|
||||
|
||||
suspend fun getHomeCardDetail(aiId: String) = executeHttp {
|
||||
aiService.getHomeCardDetail(AIIDRequest(aiId))
|
||||
}
|
||||
|
||||
suspend fun getClassificationList(request: ClassificationRequest) = executeHttp {
|
||||
aiService.getClassificationList(request)
|
||||
}
|
||||
|
||||
suspend fun getHeartbeatRank() = executeHttp {
|
||||
aiService.getHeartbeatRank()
|
||||
}
|
||||
|
||||
suspend fun getGiftRank() = executeHttp {
|
||||
aiService.getGiftRank()
|
||||
}
|
||||
|
||||
suspend fun getChatRank() = executeHttp {
|
||||
aiService.getChatRank()
|
||||
}
|
||||
|
||||
suspend fun getExploreInfo() = executeHttp {
|
||||
aiService.getExploreInfo()
|
||||
}
|
||||
|
||||
|
||||
suspend fun unlockSecret(dto: AIIDRequest): Response<Album> {
|
||||
return if (WalletManager.balance < 5000L) {
|
||||
WalletManager.showChargeDialog()
|
||||
ApiFailedResponse()
|
||||
} else {
|
||||
executeHttp {
|
||||
aiService.unlockSecret(dto)
|
||||
}
|
||||
}.transformResult({
|
||||
WalletManager.refreshWallet()
|
||||
})
|
||||
}
|
||||
|
||||
suspend fun unlockAlbum(dto: ChatAlbum): Response<Album> {
|
||||
return if ((dto.unlockPrice ?: 0) > WalletManager.balance) {
|
||||
WalletManager.showChargeDialog()
|
||||
ApiFailedResponse()
|
||||
} else {
|
||||
executeHttp {
|
||||
aiService.unlockAlbum(dto)
|
||||
}
|
||||
}.transformResult({
|
||||
WalletManager.refreshWallet()
|
||||
})
|
||||
}
|
||||
|
||||
suspend fun setAlbumUnlockPrice(dto: AlbumDTO) = executeHttp {
|
||||
aiService.setAlbumUnlockPrice(dto)
|
||||
}
|
||||
|
||||
suspend fun setAILikeOrCancel(dto: AlbumDTO) = executeHttp {
|
||||
aiService.setAILikeOrCancel(dto)
|
||||
}.transformResult({
|
||||
EventDefineOfUserAIEvents.onAILikedChanged().post(OnAILiked(dto.aiId,dto.liked))
|
||||
})
|
||||
|
||||
suspend fun deleteAICharacter(aiId: String) = executeHttp {
|
||||
aiService.deleteAICharacter(Character(aiId))
|
||||
}.transformResult({
|
||||
CommonApplicationProxy.application.toast(R.string.delete_succeed_toast)
|
||||
EventDefineOfUserAIEvents.onAICharacterChanges().post(aiId)
|
||||
})
|
||||
|
||||
suspend fun setAlbumDefault(aiId: String, albumId: Long) = executeHttp {
|
||||
aiService.setAlbumDefault(AlbumDTO(aiId = aiId, albumId = albumId))
|
||||
}.transformResult({
|
||||
EventDefineOfUserAIEvents.onAIHomeImageChanges().post(aiId)
|
||||
})
|
||||
|
||||
suspend fun editAIAvatar(request: AIHeadImgRequest) = executeHttp {
|
||||
aiService.editAIAvatar(request)
|
||||
}
|
||||
|
||||
suspend fun createOrEditAICharacter(request: Character) = executeHttp {
|
||||
aiService.createOrEditAICharacter(request)
|
||||
}.transformResult({
|
||||
EventDefineOfUserAIEvents.onAICharacterChanges().post(it?.aiId)
|
||||
})
|
||||
|
||||
suspend fun generateAICharacter(request: AIGenerate) = executeHttp {
|
||||
aiService.generateAICharacter(request)
|
||||
}
|
||||
|
||||
suspend fun getAICharacter(aiId: String) = executeHttp {
|
||||
aiService.getAICharacter(Character(aiId = aiId))
|
||||
}
|
||||
|
||||
suspend fun getAICharacterProfile(aiId: String) = executeHttp {
|
||||
aiService.getAICharacterProfile(Character(aiId = aiId))
|
||||
}
|
||||
|
||||
suspend fun getAICharacterStat(aiId: String) = executeHttp {
|
||||
aiService.getAICharacterStat(Character(aiId = aiId))
|
||||
}
|
||||
|
||||
suspend fun addAlbum(request: AlbumCreate) = executeHttp {
|
||||
aiService.addAlbum(request)
|
||||
}
|
||||
|
||||
suspend fun getAlbumCreateCount() = executeHttp {
|
||||
aiService.getAlbumCreateCount()
|
||||
}
|
||||
|
||||
suspend fun buyAlbumCreateCount(count: Int) = executeHttp {
|
||||
aiService.buyAlbumCreateCount(SimpleCountDTO(count))
|
||||
}.transformResult({
|
||||
WalletManager.refreshWallet()
|
||||
})
|
||||
|
||||
suspend fun addChatBackground(request: AlbumCreate) = executeHttp {
|
||||
aiService.addChatBackground(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* 喜欢或取消喜欢相片
|
||||
*/
|
||||
suspend fun setLikeOrDislike(albumId: Long, isLike: Boolean) = executeHttp {
|
||||
aiService.setLikeOrDislike(AlbumDTO(albumId, if (isLike) Album.CANCELED else Album.LIKED))
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除相片
|
||||
*/
|
||||
suspend fun deleteAlbum(albumId: Long) = executeHttp {
|
||||
aiService.deleteAlbum(AlbumDTO(albumId, userId = LoginManager.user?.userId))
|
||||
}
|
||||
|
||||
suspend fun getAlbumList(dto: QueryAlbumDTO) = executeHttp {
|
||||
aiService.getAlbumList(dto)
|
||||
}.transformResult({
|
||||
it?.datas?.forEach { item ->
|
||||
item.userId = dto.userId
|
||||
item.aiId = dto.aiId
|
||||
}
|
||||
})
|
||||
|
||||
suspend fun getUserGiftList(aiId: String) = executeHttp {
|
||||
aiService.getUserGiftList(QueryAlbumDTO(aiId, null, PageQuery.Page(1).apply { this.ps = 100 }))
|
||||
}
|
||||
|
||||
suspend fun generateImageBatch(request: AIGenerateImage) = executeHttp {
|
||||
aiService.generateImageBatch(request.apply {
|
||||
hl = this.aiId != null
|
||||
})
|
||||
}.transformResult({
|
||||
when(request.type){
|
||||
/**
|
||||
* 生成背景每次都要扣钱
|
||||
*/
|
||||
AIGenerateImage.BACKGROUND -> WalletManager.refreshWallet()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* AI一键生成-删除图片生成任务
|
||||
*/
|
||||
suspend fun generateImageBatchDel(batchNo: String) = executeHttp {
|
||||
aiService.generateImageBatchDel(AIGenerateImage(batchNo = batchNo))
|
||||
}
|
||||
|
||||
/**
|
||||
* AI一键生成-轮询查询图片生成结果
|
||||
*/
|
||||
suspend fun generateImageBatchQuery(batchNo: String) = executeHttp {
|
||||
aiService.generateImageBatchQuery(AIGenerateImage(batchNo = batchNo))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相册 分页
|
||||
*/
|
||||
// suspend fun getAlbumList(userId: Int, custId: String?, id: Int, imgOrder: Int) = executeHttp {
|
||||
// userService.getAlbumList(QueryAlbumDTO(userId, custId, id, imgOrder))
|
||||
// }
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
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.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.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)
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
suspend fun getHeartbeatLevel(aiId: String) = executeHttp {
|
||||
chatService.getHeartbeatLevel(Character(aiId = aiId))
|
||||
}
|
||||
|
||||
suspend fun buyHeartbeatVal(request: HeartbeatBuy) = executeHttp {
|
||||
chatService.buyHeartbeatVal(request)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ 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 javax.inject.Inject
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ import javax.inject.Inject
|
|||
*/
|
||||
class DictRepository @Inject constructor(private val dictService: DictService) : BaseRepository() {
|
||||
|
||||
/*suspend fun getChatBubbleList(aiId: String) = executeHttp {
|
||||
suspend fun getChatBubbleList(aiId: String) = executeHttp {
|
||||
dictService.getChatBubbleList(AIIDRequest(aiId))
|
||||
}
|
||||
|
||||
|
|
@ -20,6 +21,6 @@ class DictRepository @Inject constructor(private val dictService: DictService) :
|
|||
|
||||
suspend fun getGiftDict() = executeHttp(false) { dictService.getGiftDict() }
|
||||
|
||||
suspend fun getAIChatModel() = executeHttp { dictService.getAIChatModel() }*/
|
||||
suspend fun getAIChatModel() = executeHttp { dictService.getAIChatModel() }
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +1,34 @@
|
|||
package com.remax.visualnovel.repository.api
|
||||
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.google.gson.Gson
|
||||
import com.netease.nimlib.sdk.NIMClient
|
||||
import com.netease.nimlib.sdk.v2.conversation.V2NIMConversationService
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessageListener
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessageService
|
||||
import com.netease.nimlib.sdk.v2.message.enums.V2NIMMessageQueryDirection
|
||||
import com.netease.nimlib.sdk.v2.message.option.V2NIMCloudMessageListOption
|
||||
import com.remax.visualnovel.api.service.MessageService
|
||||
import com.remax.visualnovel.entity.imbean.IMMessageWrapper
|
||||
import com.remax.visualnovel.entity.request.AIListRequest
|
||||
import com.remax.visualnovel.entity.request.Gift
|
||||
import com.remax.visualnovel.entity.request.PageQuery
|
||||
import com.remax.visualnovel.entity.request.SendGift
|
||||
import com.remax.visualnovel.entity.response.Character
|
||||
import com.remax.visualnovel.entity.response.base.ApiFailedResponse
|
||||
import com.remax.visualnovel.entity.response.base.Response
|
||||
import com.remax.visualnovel.extension.resumeWithActive
|
||||
import com.remax.visualnovel.manager.login.LoginManager
|
||||
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.base.BaseRepository
|
||||
import com.remax.visualnovel.repository.ext.convertMessage
|
||||
import com.remax.visualnovel.ui.wallet.manager.WalletManager
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
|
|
@ -11,5 +37,135 @@ import javax.inject.Inject
|
|||
*/
|
||||
class MessageRepository @Inject constructor(private val messageService: MessageService) : BaseRepository() {
|
||||
|
||||
private val v2NIMConversationService by lazy {
|
||||
NIMClient.getService(V2NIMConversationService::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除会话
|
||||
*/
|
||||
fun deleteConversationListByIds(ids: List<String>, callback: () -> Unit) {
|
||||
v2NIMConversationService.deleteConversationListByIds(ids, true, {
|
||||
NimManager.log("删除会话成功 ${Gson().toJson(it)}")
|
||||
callback.invoke()
|
||||
}) {
|
||||
NimManager.log("删除会话失败 ${Gson().toJson(it)}")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteConversation(aiIdList: List<String>) = executeHttp {
|
||||
messageService.deleteConversation(AIListRequest(aiIdList))
|
||||
}
|
||||
|
||||
private val messageFetchResult = FetchResult<List<IMMessageWrapper>>(LoadStatus.Finish)
|
||||
|
||||
/**
|
||||
* 分页查询历史消息
|
||||
*/
|
||||
suspend fun getMessageList(conversationId: String, anchorMessage: V2NIMMessage? = null, character: Character?) =
|
||||
suspendCancellableCoroutine { coroutine ->
|
||||
coroutine.invokeOnCancellation {
|
||||
coroutine.resumeWithActive(messageFetchResult)
|
||||
}
|
||||
val optionBuilder = V2NIMCloudMessageListOption.V2NIMCloudMessageListOptionBuilder
|
||||
.builder(conversationId)
|
||||
.withDirection(V2NIMMessageQueryDirection.V2NIM_QUERY_DIRECTION_DESC)
|
||||
|
||||
if (anchorMessage != null) {
|
||||
optionBuilder.withAnchorMessage(anchorMessage)
|
||||
optionBuilder.withEndTime(anchorMessage.createTime)
|
||||
}
|
||||
|
||||
val option = optionBuilder.build()
|
||||
// 此处避免在获取 anchor 消息后被之前消息添加导致ui移位,因此将 anchor 之前消息请求添加到后续的主线程事件队列中
|
||||
Handler(Looper.getMainLooper())
|
||||
.post {
|
||||
// 调用接口
|
||||
v2MessageService.getCloudMessageList(option, { result ->
|
||||
NimManager.log("查询云端消息成功")
|
||||
val messages = result?.messages
|
||||
val anchorMessage = result.anchorMessage
|
||||
NimManager.log("获取到 " + messages?.size + " 条消息")
|
||||
|
||||
val loadStatus = if (messages.isNullOrEmpty()) LoadStatus.Finish else LoadStatus.Success
|
||||
messageFetchResult.loadStatus = loadStatus
|
||||
messageFetchResult.data = messages?.convertMessage(anchorMessage != null, character)
|
||||
messageFetchResult.extraInfo = anchorMessage
|
||||
|
||||
if (anchorMessage != null) {
|
||||
NimManager.log("还有更多消息,下次查询锚点: " + anchorMessage.messageClientId)
|
||||
} else {
|
||||
NimManager.log("没有更多消息了")
|
||||
}
|
||||
|
||||
coroutine.resumeWithActive(messageFetchResult)
|
||||
}) { error ->
|
||||
NimManager.log("查询云端消息失败, code: " + error.code + ", message: " + error.desc)
|
||||
messageFetchResult.setError(error.code, error.desc)
|
||||
messageFetchResult.data = null
|
||||
messageFetchResult.extraInfo = null
|
||||
|
||||
coroutine.resumeWithActive(messageFetchResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val v2MessageService by lazy {
|
||||
NIMClient.getService(V2NIMMessageService::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息监听
|
||||
*/
|
||||
fun setMessageListener(register: Boolean, listener: V2NIMMessageListener) {
|
||||
if (register) {
|
||||
v2MessageService.addMessageListener(listener)
|
||||
} else {
|
||||
v2MessageService.removeMessageListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收方消息已读
|
||||
*/
|
||||
fun sendP2PMessageReceipt(v2Message: V2NIMMessage) {
|
||||
v2MessageService.sendP2PMessageReceipt(v2Message, {
|
||||
NimManager.log("接收方消息已读: $v2Message")
|
||||
}) {
|
||||
NimManager.log("接收方消息已读失败 ${it.code} -- ${it.desc}")
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage(v2Message: V2NIMMessage, accountId: String?, errorCallback: (Int) -> Unit) {
|
||||
NimManager.v2SendMessage(v2Message, accountId, errorCallback)
|
||||
}
|
||||
|
||||
suspend fun sendGift(dto: SendGift, gift: Gift): Response<Any> {
|
||||
val totalPrice = gift.price * dto.num
|
||||
return if (totalPrice > WalletManager.balance) {
|
||||
WalletManager.showChargeDialog()
|
||||
ApiFailedResponse()
|
||||
} else {
|
||||
executeHttp {
|
||||
messageService.sendGift(dto)
|
||||
}.transformResult({
|
||||
WalletManager.refreshWallet()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMessageStat() = executeHttp(false) {
|
||||
messageService.getMessageStat()
|
||||
}.transformResult({
|
||||
LoginManager.contactUnreadCount = it?.unRead ?: 0
|
||||
})
|
||||
|
||||
/**
|
||||
* 系统通知列表
|
||||
*/
|
||||
suspend fun getMessageList(pn: Int) = executeHttp {
|
||||
messageService.getMessageList(PageQuery(pn))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.remax.visualnovel.repository.api
|
||||
|
||||
import com.remax.visualnovel.api.service.OssService
|
||||
import com.remax.visualnovel.entity.request.ImgCheckDTO
|
||||
import com.remax.visualnovel.entity.request.S3TypeDTO
|
||||
import com.remax.visualnovel.entity.request.SimpleContentDTO
|
||||
import com.remax.visualnovel.entity.response.BucketBean
|
||||
import com.remax.visualnovel.entity.response.base.Response
|
||||
import com.remax.visualnovel.repository.api.base.BaseRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/10/27
|
||||
*
|
||||
* 应用相关
|
||||
*/
|
||||
class OssRepository @Inject constructor(
|
||||
private val ossService: OssService
|
||||
) : BaseRepository() {
|
||||
|
||||
/**
|
||||
* 获取aws s3 bucket信息
|
||||
*/
|
||||
suspend fun getS3Bucket(ossType: String, postfix: String): Response<BucketBean> =
|
||||
executeHttp {
|
||||
val bucketDTO = S3TypeDTO(ossType, postfix)
|
||||
ossService.getS3Bucket(bucketDTO)
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片鉴黄
|
||||
*/
|
||||
suspend fun checkS3Img(imgCheckDTO: ImgCheckDTO): Response<Any> =
|
||||
executeHttp {
|
||||
ossService.checkS3Img(imgCheckDTO = imgCheckDTO)
|
||||
}
|
||||
|
||||
suspend fun checkText(content: String): Response<Any> =
|
||||
executeHttp {
|
||||
ossService.checkText(simpleContentDTO = SimpleContentDTO(content))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.remax.visualnovel.repository.api
|
||||
|
||||
import com.remax.visualnovel.api.service.PayService
|
||||
import com.remax.visualnovel.entity.request.ChargeOrderDTO
|
||||
import com.remax.visualnovel.entity.request.ChargeProduct
|
||||
import com.remax.visualnovel.entity.request.ChargeProductInfo
|
||||
import com.remax.visualnovel.entity.request.SearchPage
|
||||
import com.remax.visualnovel.entity.request.ValidateTransactionDTO
|
||||
import com.remax.visualnovel.entity.response.UserSubInfo
|
||||
import com.remax.visualnovel.entity.response.Wallet
|
||||
import com.remax.visualnovel.entity.response.base.Response
|
||||
import com.remax.visualnovel.repository.api.base.BaseRepository
|
||||
import com.pengxr.modular.eventbus.generated.events.EventDefineOfWalletEvents
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by HJW on 2022/11/8
|
||||
*
|
||||
* 支付相关
|
||||
*/
|
||||
class PayRepository @Inject constructor(
|
||||
private val accountService: PayService,
|
||||
) : BaseRepository() {
|
||||
|
||||
suspend fun getVipPrivilegeList() = executeHttp {
|
||||
accountService.getVipPrivilegeList()
|
||||
}
|
||||
|
||||
suspend fun getTransactionList(request: SearchPage) = executeHttp {
|
||||
accountService.getTransactionList(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的钱包
|
||||
*/
|
||||
suspend fun getMyWallet(): Response<Wallet> = executeHttp(false) {
|
||||
accountService.getMyWallet()
|
||||
}.transformResult({
|
||||
EventDefineOfWalletEvents.buffBalanceUpdateSucceeded().post(it)
|
||||
})
|
||||
|
||||
/**
|
||||
* 验证支付是否成功
|
||||
*/
|
||||
suspend fun validateTranslation(dto: ValidateTransactionDTO): Response<Any> =
|
||||
executeHttp {
|
||||
accountService.validateTranslation(dto = dto)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取充值产品
|
||||
*/
|
||||
suspend fun getProducts(): Response<ChargeProductInfo> =
|
||||
executeHttp(false) {
|
||||
accountService.getChargeProducts()
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个google pay订单
|
||||
*/
|
||||
suspend fun createOrder(product: ChargeProduct) = executeHttp {
|
||||
accountService.createOrder(dto = ChargeOrderDTO(product.chargeAmount, product.productId))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取vip订阅价格列表
|
||||
*/
|
||||
suspend fun getSubPriceList() = executeHttp(false) {
|
||||
accountService.getSubPriceList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证订阅是否成功
|
||||
*/
|
||||
suspend fun uploadGoogleReceipt(dto: ValidateTransactionDTO): Response<String> = executeHttp {
|
||||
accountService.uploadGoogleReceipt(dto = dto)
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅/升级VIP前查询订阅信息
|
||||
*/
|
||||
suspend fun checkSubInfo(): Response<UserSubInfo> = executeHttp {
|
||||
accountService.checkSubInfo()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package com.remax.visualnovel.repository.ext
|
||||
|
||||
import com.remax.visualnovel.entity.imbean.IMAIInMessage
|
||||
import com.remax.visualnovel.entity.imbean.IMBaseInfoMessage
|
||||
import com.remax.visualnovel.entity.imbean.IMCallMessage
|
||||
import com.remax.visualnovel.entity.imbean.IMGiftMessage
|
||||
import com.remax.visualnovel.entity.imbean.IMInImageMessage
|
||||
import com.remax.visualnovel.entity.imbean.IMLevelMessage
|
||||
import com.remax.visualnovel.entity.imbean.IMMessageWrapper
|
||||
import com.remax.visualnovel.entity.imbean.IMOutImageMessage
|
||||
import com.remax.visualnovel.entity.imbean.raw.CustomAlbumData
|
||||
import com.remax.visualnovel.entity.imbean.raw.CustomCallData
|
||||
import com.remax.visualnovel.entity.imbean.raw.CustomGiftData
|
||||
import com.remax.visualnovel.entity.imbean.raw.CustomLevelChangeData
|
||||
import com.remax.visualnovel.entity.imbean.raw.CustomRawData
|
||||
import com.remax.visualnovel.entity.imbean.voice.IMVoice
|
||||
import com.remax.visualnovel.entity.request.SimpleTypeDTO
|
||||
import com.remax.visualnovel.entity.response.Character
|
||||
import com.remax.visualnovel.extension.convertFromJson
|
||||
import com.remax.visualnovel.manager.nim.FetchResult
|
||||
import com.remax.visualnovel.manager.nim.NimManager
|
||||
import com.remax.visualnovel.ui.chat.message.call.manager.RTCManager
|
||||
import com.remax.visualnovel.ui.wallet.manager.WalletManager
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
|
||||
import com.netease.nimlib.sdk.v2.message.V2NIMMessageCreator
|
||||
import com.netease.nimlib.sdk.v2.message.enums.V2NIMMessageSendingState
|
||||
import com.netease.nimlib.sdk.v2.message.enums.V2NIMMessageType
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/26
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* 转化一下消息类
|
||||
* 适配一下多布局
|
||||
*/
|
||||
fun List<V2NIMMessage>?.convertMessage(hasMore: Boolean, character: Character?): List<IMMessageWrapper> {
|
||||
val result = this?.map { v2NIMMessage -> v2NIMMessage.convertMessage(true) }
|
||||
?.filter { messageWrapper -> messageWrapper?.fetchType != FetchResult.FetchType.Remind }
|
||||
?.toMutableList()
|
||||
if (!hasMore) {
|
||||
result?.add(0, IMBaseInfoMessage(character))
|
||||
/**
|
||||
* 没有聊过天需要添加开场白
|
||||
*/
|
||||
if (character?.isDelChatted != true) {
|
||||
val prologueMsg = V2NIMMessageCreator.createTextMessage(character?.dialoguePrologue)
|
||||
val prologue = IMAIInMessage(prologueMsg, IMVoice(prologueMsg.messageClientId ?: ""))
|
||||
result?.add(0, prologue)
|
||||
}
|
||||
}
|
||||
return result?.filterNotNull() ?: emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fromHistory 是否从消息历史解析
|
||||
*/
|
||||
fun V2NIMMessage.convertMessage(fromHistory:Boolean = false): IMMessageWrapper? {
|
||||
var aiIsSending = true
|
||||
var fetchType = when {
|
||||
// 收到的消息总是展示
|
||||
!this.isSelf -> FetchResult.FetchType.Add
|
||||
// 自己发送的消息先loading
|
||||
this.sendingState == V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SENDING -> FetchResult.FetchType.Add
|
||||
// AI只回复发送成功的
|
||||
else -> {
|
||||
aiIsSending = this.sendingState == V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED
|
||||
FetchResult.FetchType.Update
|
||||
}
|
||||
}
|
||||
NimManager.log("消息转换 messageServerId:${this.messageServerId} text:${this.text} attachment.raw:${this.attachment?.raw} serverExtension:${this.serverExtension}")
|
||||
val res = when (messageType) {
|
||||
V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT -> {
|
||||
if (isSelf) IMMessageWrapper(this)
|
||||
else IMAIInMessage(this, IMVoice(this.messageServerId.toString()))
|
||||
}
|
||||
|
||||
V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM -> {
|
||||
val type = attachment?.raw?.convertFromJson<SimpleTypeDTO>()?.type?.uppercase()
|
||||
when (type) {
|
||||
/**
|
||||
* 用户发送给AI的图片消息
|
||||
*/
|
||||
CustomRawData.IMAGE -> {
|
||||
if (isSelf) {
|
||||
IMOutImageMessage(this, attachment?.raw?.convertFromJson<CustomRawData>())
|
||||
} else {
|
||||
IMInImageMessage(this, attachment?.raw?.convertFromJson<CustomAlbumData>())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户发送给AI的礼物消息
|
||||
*/
|
||||
CustomRawData.GIFT -> {
|
||||
if (isSelf) {
|
||||
IMGiftMessage(this, attachment?.raw?.convertFromJson<CustomGiftData>())
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* 心动等级变化
|
||||
*/
|
||||
CustomRawData.HEARTBEAT_LEVEL_UP, CustomRawData.HEARTBEAT_LEVEL_DOWN -> {
|
||||
fetchType = FetchResult.FetchType.Remind
|
||||
aiIsSending = false
|
||||
val msg = IMLevelMessage(this, attachment?.raw?.convertFromJson<CustomLevelChangeData>())
|
||||
RTCManager.sendIMLevelMessage(msg)
|
||||
msg
|
||||
}
|
||||
|
||||
CustomRawData.VOICE_CHAT_EMOTION_SCORE -> {
|
||||
aiIsSending = false
|
||||
fetchType = FetchResult.FetchType.Remind
|
||||
null
|
||||
}
|
||||
|
||||
CustomRawData.INSUFFICIENT_BALANCE -> {
|
||||
aiIsSending = false
|
||||
fetchType = FetchResult.FetchType.Remind
|
||||
WalletManager.refreshWallet()
|
||||
if (!fromHistory){
|
||||
RTCManager.balanceInsufficient()
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* 拨打语音电话
|
||||
* 语音通话消息不需要回话
|
||||
*/
|
||||
CustomRawData.CALL -> {
|
||||
aiIsSending = false
|
||||
IMCallMessage(this, attachment?.raw?.convertFromJson<CustomCallData>())
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
return res?.apply {
|
||||
this.fetchType = fetchType
|
||||
this.aiIsSending = aiIsSending
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
package com.remax.visualnovel.ui.Chat
|
||||
|
||||
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import com.alibaba.android.arouter.facade.annotation.Route
|
||||
import com.alibaba.android.arouter.launcher.ARouter
|
||||
import com.remax.visualnovel.app.base.BaseBindingActivity
|
||||
import com.remax.visualnovel.utils.Routers
|
||||
import com.remax.visualnovel.utils.StatusBarUtils
|
||||
import com.pengxr.modular.eventbus.generated.events.EventDefineOfUserEvents
|
||||
import com.remax.visualnovel.R
|
||||
import com.remax.visualnovel.databinding.ActivityActorChatBinding
|
||||
import com.remax.visualnovel.event.model.OnLoginEvent
|
||||
import com.remax.visualnovel.extension.launchWithRequest
|
||||
import com.remax.visualnovel.ui.main.MainViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
@Route(path = Routers.CHAT)
|
||||
class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
|
||||
|
||||
private val mViewModel by viewModels<MainViewModel>()
|
||||
|
||||
override fun initView() {
|
||||
ARouter.getInstance().inject(this)
|
||||
//setToolbar(R.string.setting)
|
||||
|
||||
StatusBarUtils.setStatusBarAndNavBarIsLight(this, false)
|
||||
StatusBarUtils.setTransparent(this)
|
||||
|
||||
with(binding) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun initData() {
|
||||
|
||||
}
|
||||
|
||||
private val loginObserver = Observer<OnLoginEvent?> {
|
||||
launchWithRequest({
|
||||
//TODO - business handling for login events
|
||||
if (it?.isLogin() == true) {
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
EventDefineOfUserEvents.onLoginStatusChanged().removeObserver(loginObserver)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val ACTOR_ID = "ACTOR_ID"
|
||||
|
||||
fun start(actorId: Int) {
|
||||
ARouter.getInstance()
|
||||
.build(Routers.CHAT)
|
||||
.withInt(ACTOR_ID, actorId)
|
||||
.navigation()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
package com.remax.visualnovel.ui.chat
|
||||
|
||||
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.alibaba.android.arouter.facade.annotation.Route
|
||||
import com.alibaba.android.arouter.launcher.ARouter
|
||||
import com.hjq.permissions.OnPermissionCallback
|
||||
import com.hjq.permissions.XXPermissions
|
||||
import com.hjq.permissions.permission.PermissionLists
|
||||
import com.hjq.permissions.permission.base.IPermission
|
||||
import com.remax.visualnovel.app.base.BaseBindingActivity
|
||||
import com.remax.visualnovel.utils.Routers
|
||||
import com.remax.visualnovel.utils.StatusBarUtils
|
||||
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.event.model.OnLoginEvent
|
||||
import com.remax.visualnovel.extension.countDownCoroutines
|
||||
import com.remax.visualnovel.extension.launchAndLoadingCollect
|
||||
import com.remax.visualnovel.extension.launchWithRequest
|
||||
import com.remax.visualnovel.extension.toast
|
||||
import com.remax.visualnovel.manager.nim.NimManager
|
||||
import com.remax.visualnovel.ui.chat.setting.model.ChatModelDialog
|
||||
import com.remax.visualnovel.ui.chat.ui.HoldToTalkDialog
|
||||
import com.remax.visualnovel.ui.wallet.manager.WalletManager
|
||||
import com.remax.visualnovel.utils.RecordHelper
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Job
|
||||
import timber.log.Timber
|
||||
import kotlin.getValue
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
@Route(path = Routers.CHAT)
|
||||
class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
|
||||
|
||||
private val chatViewModel by viewModels<ChatViewModel>()
|
||||
private val mRecordAssist = RecordAssist()
|
||||
|
||||
|
||||
|
||||
|
||||
override fun initView() {
|
||||
ARouter.getInstance().inject(this)
|
||||
//setToolbar(R.string.setting)
|
||||
|
||||
StatusBarUtils.setStatusBarAndNavBarIsLight(this, false)
|
||||
StatusBarUtils.setTransparent(this)
|
||||
|
||||
with(binding) {
|
||||
initInputPanelEvents()
|
||||
}
|
||||
}
|
||||
|
||||
override fun initData() {
|
||||
|
||||
}
|
||||
|
||||
private val loginObserver = Observer<OnLoginEvent?> {
|
||||
launchWithRequest({
|
||||
//TODO - business handling for login events
|
||||
if (it?.isLogin() == true) {
|
||||
|
||||
} else {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
EventDefineOfUserEvents.onLoginStatusChanged().removeObserver(loginObserver)
|
||||
mRecordAssist.stopTalk()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理发送消息的错误码
|
||||
*/
|
||||
private val sendMsgErrorCallback: (Int) -> Unit = { code ->
|
||||
when (code) {
|
||||
NimManager.SEND_IM_INSUFFICIENT_BALANCE -> {
|
||||
showSelectModelDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initInputPanelEvents() {
|
||||
with(binding.inputPanel) {
|
||||
holdToTalk({
|
||||
mRecordAssist.startRecording()
|
||||
}) {
|
||||
mRecordAssist.stopTalk()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查麦克风权限
|
||||
*/
|
||||
private fun checkRecordAudioPermission(onGranted: () -> Unit, allowGranted: (() -> Unit)? = null) {
|
||||
val permission = PermissionLists.getRecordAudioPermission()
|
||||
if (XXPermissions.isGrantedPermission(this, permission)) {
|
||||
onGranted.invoke()
|
||||
} else {
|
||||
XXPermissions.with(this).permission(permission)
|
||||
.request(object : OnPermissionCallback {
|
||||
override fun onDenied(permissions: MutableList<IPermission>, doNotAskAgain: Boolean) {
|
||||
this@ChatActivity.toast(R.string.no_permission)
|
||||
if (doNotAskAgain) {
|
||||
// 如果是被永久拒绝就跳转到应用权限系统设置页面
|
||||
XXPermissions.startPermissionActivity(this@ChatActivity, permissions)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGranted(permissions: MutableList<IPermission>, allGranted: Boolean) {
|
||||
if (!allGranted) {
|
||||
this@ChatActivity.toast(getString(R.string.no_permission))
|
||||
} else {
|
||||
allowGranted?.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun showSelectModelDialog() {
|
||||
with(chatViewModel) {
|
||||
fun createModelDialog() {
|
||||
val modelDialog = ChatModelDialog(this@ChatActivity)
|
||||
|
||||
modelDialog.build(chatModels ?: emptyList(), chatSet?.modelCode, {
|
||||
|
||||
}) { model ->
|
||||
launchAndLoadingCollect({
|
||||
setChatModel(
|
||||
ChatSetting(aiId, model.code)
|
||||
)
|
||||
}) {
|
||||
onSuccess = { res ->
|
||||
modelDialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
modelDialog.show()
|
||||
}
|
||||
|
||||
if (chatModels.isNullOrEmpty()) {
|
||||
launchAndLoadingCollect({
|
||||
chatViewModel.getChatModels()
|
||||
}) {
|
||||
onSuccess = {
|
||||
createModelDialog()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createModelDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val ACTOR_ID = "ACTOR_ID"
|
||||
|
||||
fun start(actorId: Int) {
|
||||
ARouter.getInstance()
|
||||
.build(Routers.CHAT)
|
||||
.withInt(ACTOR_ID, actorId)
|
||||
.navigation()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
inner class RecordAssist {
|
||||
|
||||
private var recordJob: Job? = null
|
||||
|
||||
private val recordHelper by lazy {
|
||||
RecordHelper()
|
||||
}
|
||||
|
||||
private val holdToTalkDialog by lazy {
|
||||
HoldToTalkDialog(this@ChatActivity).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始按住说话
|
||||
*/
|
||||
fun startRecording() {
|
||||
// TODO - check bill count
|
||||
checkRecordAudioPermission({ startTalk() })
|
||||
/*when {
|
||||
// 没钱就去充值
|
||||
WalletManager.balance < 1000L -> WalletManager.showChargeDialog()
|
||||
// 检查权限
|
||||
else -> {
|
||||
checkRecordAudioPermission({ startTalk() })
|
||||
}
|
||||
|
||||
}*/
|
||||
}
|
||||
|
||||
|
||||
private fun startTalk() {
|
||||
val maxTalkTime = 60
|
||||
val minTalkTime = 1
|
||||
|
||||
var recordingProgress = 0
|
||||
|
||||
holdToTalkDialog.show()
|
||||
recordHelper.startRecording(this@ChatActivity, {
|
||||
recordJob = lifecycleScope.countDownCoroutines(maxTalkTime, {
|
||||
Timber.i("startRecording countDownCoroutines: %d", it)
|
||||
recordingProgress = maxTalkTime - it
|
||||
}, {
|
||||
stopTalk()
|
||||
})
|
||||
|
||||
}, {
|
||||
if (recordingProgress >= minTalkTime) {
|
||||
Timber.i("startRecording onStop: ${recordHelper.getFilename()}")
|
||||
|
||||
launchAndLoadingCollect({
|
||||
chatViewModel.voiceASR(recordHelper.getFilename())
|
||||
}) {
|
||||
onSuccess = {
|
||||
if (!it?.content.isNullOrBlank()) {
|
||||
chatViewModel.sendMsg(it.content, errorCallback = sendMsgErrorCallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
//录音最少1秒
|
||||
showToast(R.string.min_voice_time)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fun stopTalk() {
|
||||
holdToTalkDialog.dismiss()
|
||||
recordJob?.cancel()
|
||||
recordHelper.stopRecording()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.remax.visualnovel.ui.Chat
|
||||
package com.remax.visualnovel.ui.chat
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
|
|
@ -0,0 +1,357 @@
|
|||
package com.remax.visualnovel.ui.chat
|
||||
|
||||
/**
|
||||
* Created by HJW on 2025/8/13
|
||||
*/
|
||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class ChatViewModel @Inject constructor(
|
||||
private val chatRepository: ChatRepository,
|
||||
private val dictRepository: DictRepository,
|
||||
private val messageRepository: MessageRepository,
|
||||
private val aiRepository: AIRepository,
|
||||
) : OssViewModel() {
|
||||
|
||||
var aiId: String = ""
|
||||
|
||||
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 chatModels: List<ChatModel>? = null
|
||||
private set
|
||||
|
||||
suspend fun getChatModels() = dictRepository.getAIChatModel().transformResult({
|
||||
chatModels = it
|
||||
})
|
||||
|
||||
suspend fun setChatModel(request: ChatSetting) = chatRepository.setChatModel(request).transformResult({
|
||||
getChatSetting()
|
||||
})
|
||||
|
||||
var chatSet: ChatSet? = null
|
||||
private set
|
||||
|
||||
suspend fun getChatSetting() = chatRepository.getChatSetting(aiId).transformResult({
|
||||
chatSet = it
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* 云信发送消息,raw代表是自定义消息
|
||||
*/
|
||||
fun sendMsg(msgContent: String, raw: CustomRawData? = null, errorCallback: (Int) -> Unit) {
|
||||
|
||||
val v2Message = if (raw != null)
|
||||
V2NIMMessageCreator.createCustomMessage(msgContent, Gson().toJson(raw))
|
||||
else
|
||||
V2NIMMessageCreator.createTextMessage(msgContent)
|
||||
|
||||
messageRepository.sendMessage(v2Message, character?.nimAccountId) { errorCode ->
|
||||
errorCallback(errorCode)
|
||||
}
|
||||
}
|
||||
|
||||
// 消息发送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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.remax.visualnovel.ui.Chat
|
||||
package com.remax.visualnovel.ui.chat
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import com.dylanc.viewbinding.nonreflection.inflate
|
||||
|
|
@ -79,4 +81,22 @@ class InputPanel @JvmOverloads constructor(context: Context, attrs: AttributeSet
|
|||
}
|
||||
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun holdToTalk(callback: () -> Unit, cancelCallback: () -> Unit) {
|
||||
binding.ivHold2talk.run {
|
||||
setOnTouchListener { v, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
callback.invoke()
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
cancelCallback.invoke()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.remax.visualnovel.ui.Chat
|
||||
package com.remax.visualnovel.ui.chat
|
||||
|
||||
|
||||
import android.animation.Animator
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.remax.visualnovel.ui.chat.message.call.manager
|
||||
import com.remax.visualnovel.extension.convertFromJson
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
|
||||
/**
|
||||
* text 字幕文本。
|
||||
*
|
||||
* language 字幕语言。
|
||||
*
|
||||
* userId 字幕源的 ID。如果字幕源是人类用户,则此值为人类用户的 UserId。如果字幕源是 AI 代理,则此值为 AI 代理的 UserId。
|
||||
*
|
||||
* sequence 整数 字幕序列号。
|
||||
*
|
||||
* definite 布尔值 字幕是否为完整的句段。
|
||||
* * true:是。
|
||||
* * false:否。
|
||||
*
|
||||
* paragraph 布尔值 副标题是否为完整句子。
|
||||
* * true:是。
|
||||
* * false:否。
|
||||
*
|
||||
* roundId 整数 对话的回合 ID。
|
||||
*
|
||||
* 在不同的使用场景下,你可以根据definite、paragraph和sequence字段来决定如何处理字幕。
|
||||
* 实时字幕显示:
|
||||
* 如果paragraph=false且definite= false,则用较新的字幕(序列号更高)替换较旧的字幕。
|
||||
* 如果paragraph=false且definite= true,则开始一个新句子并替换前一个句子。
|
||||
* 如果paragraph= true,则表示一个完整句子的结束。此时继续解析并显示字幕将导致重复显示。
|
||||
*/
|
||||
data class SubtitleMsgData(
|
||||
val definite: Boolean?,
|
||||
val language: String?,
|
||||
val paragraph: Boolean?,
|
||||
val sequence: Int?,
|
||||
val text: String?,
|
||||
val userId: String?
|
||||
)
|
||||
|
||||
|
||||
data class SubtitleData(
|
||||
val data: List<SubtitleMsgData>?,
|
||||
)
|
||||
|
||||
class BinaryMessageHandler() {
|
||||
fun unpack(message: ByteArray, callback: (SubtitleMsgData) -> Unit): Boolean {
|
||||
val kSubtitleHeaderSize = 8
|
||||
if (message.size < kSubtitleHeaderSize) {
|
||||
return false
|
||||
}
|
||||
// Magic number "subv"
|
||||
val magic = ((message[0].toInt() and 0xFF shl 24) or
|
||||
(message[1].toInt() and 0xFF shl 16) or
|
||||
(message[2].toInt() and 0xFF shl 8) or
|
||||
(message[3].toInt() and 0xFF)).toUInt()
|
||||
if (magic != 0x73756276U) {
|
||||
RTCManager.log("unpack magic != 0x73756276U")
|
||||
return false
|
||||
}
|
||||
|
||||
val length = ((message[4].toInt() and 0xFF shl 24) or
|
||||
(message[5].toInt() and 0xFF shl 16) or
|
||||
(message[6].toInt() and 0xFF shl 8) or
|
||||
(message[7].toInt() and 0xFF)).toUInt()
|
||||
|
||||
if (message.size - kSubtitleHeaderSize != length.toInt()) {
|
||||
RTCManager.log("unpack != length.toInt()")
|
||||
return false
|
||||
}
|
||||
|
||||
if (length > 0U) {
|
||||
val subtitleBytes = message.copyOfRange(kSubtitleHeaderSize, message.size)
|
||||
parseData(String(subtitleBytes, StandardCharsets.UTF_8), callback)
|
||||
} else {
|
||||
parseData("", callback)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Parse
|
||||
private fun parseData(msg: String, callback: (SubtitleMsgData) -> Unit) {
|
||||
val subtitles = msg.convertFromJson<SubtitleData>()
|
||||
// 这里可以进一步处理 subtitles 列表,例如打印或存储
|
||||
subtitles?.data?.forEach {
|
||||
RTCManager.log("Parsed subtitles: $it")
|
||||
callback.invoke(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.remax.visualnovel.ui.chat.message.call.manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
/**
|
||||
* 防抖函数,在指定时间内只执行最后一次调用
|
||||
* @param delayMs 防抖时间(毫秒)
|
||||
* @param scope 协程作用域
|
||||
* @param action 要执行的操作
|
||||
*/
|
||||
fun <T1, T2> debounce(
|
||||
delayMs: Long = 0L,
|
||||
scope: CoroutineScope,
|
||||
action: (T1, T2?) -> Unit
|
||||
): (T1, T2?) -> Unit {
|
||||
var debounceJob: Job? = null
|
||||
|
||||
return { p1, p2 ->
|
||||
// 取消之前的任务
|
||||
debounceJob?.cancel()
|
||||
// 创建新的协程任务
|
||||
debounceJob = scope.launch {
|
||||
delay(delayMs)
|
||||
action(p1, p2)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue