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("ABOUT_US", "https://www.xxxxx.ai/about")
|
||||||
buildConfigString("API_FROG", "https://www.test-frog.xxxxx.ai")
|
buildConfigString("API_FROG", "https://www.test-frog.xxxxx.ai")
|
||||||
buildConfigString("EPAL_TERMS_SERVICES", "https://www.xxxxx.ai/policy/tos")
|
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("ABOUT_US", "https://test.xxxxx.ai/about")
|
||||||
buildConfigString("API_FROG", "https://test-frog.xxxxx.ai")
|
buildConfigString("API_FROG", "https://test-frog.xxxxx.ai")
|
||||||
buildConfigString("EPAL_TERMS_SERVICES", "https://test.xxxxx.ai/policy/tos")
|
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.exoplayer)
|
||||||
implementation(Deps.subsamplingScaleImageView)
|
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")))
|
implementation(project(mapOf("path" to ":loadingstateview")))
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.Chat.ChatActivity"
|
android:name=".ui.chat.ChatActivity"
|
||||||
android:exported="false" >
|
android:exported="false" >
|
||||||
</activity>
|
</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
|
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 com.remax.visualnovel.entity.response.base.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
|
@ -10,24 +17,24 @@ interface DictService {
|
||||||
/**
|
/**
|
||||||
* 获取聊天气泡字典
|
* 获取聊天气泡字典
|
||||||
*/
|
*/
|
||||||
/*@POST("/web/chat-set/get-chat-bubble-list")
|
@POST("/web/chat-set/get-chat-bubble-list")
|
||||||
suspend fun getChatBubbleList(@Body request: AIIDRequest): Response<List<ChatBubble>>*/
|
suspend fun getChatBubbleList(@Body request: AIIDRequest): Response<List<ChatBubble>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI标签
|
* AI标签
|
||||||
*/
|
*/
|
||||||
/*@POST("/web/get-ai-dict")
|
@POST("/web/get-ai-dict")
|
||||||
suspend fun getAIDict(): Response<AIDict>*/
|
suspend fun getAIDict(): Response<AIDict>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 礼物字典
|
* 礼物字典
|
||||||
*/
|
*/
|
||||||
/*@POST("/web/gift/dict-list")
|
@POST("/web/gift/dict-list")
|
||||||
suspend fun getGiftDict(@Body pageQuery: PageQuery = PageQuery(1).apply { page.ps = 100 }): Response<Pageable<Gift>>*/
|
suspend fun getGiftDict(@Body pageQuery: PageQuery = PageQuery(1).apply { page.ps = 100 }): Response<Pageable<Gift>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* chat模型
|
* chat模型
|
||||||
*/
|
*/
|
||||||
/*@POST("/web/chat-model/dict-list")
|
@POST("/web/chat-model/dict-list")
|
||||||
suspend fun getAIChatModel(): Response<List<ChatModel>>*/
|
suspend fun getAIChatModel(): Response<List<ChatModel>>
|
||||||
}
|
}
|
||||||
|
|
@ -1,34 +1,40 @@
|
||||||
package com.remax.visualnovel.api.service
|
package com.remax.visualnovel.api.service
|
||||||
|
|
||||||
import com.remax.visualnovel.BuildConfig
|
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 com.remax.visualnovel.entity.response.base.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
|
||||||
interface MessageService {
|
interface MessageService {
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * 删除会话
|
* 删除会话
|
||||||
// */
|
*/
|
||||||
// @POST(BuildConfig.API_COW + "/web/ai-message/del")
|
@POST(BuildConfig.API_COW + "/web/ai-message/del")
|
||||||
// suspend fun deleteConversation(@Body request: AIListRequest): Response<Any>
|
suspend fun deleteConversation(@Body request: AIListRequest): Response<Any>
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 送礼物
|
* 送礼物
|
||||||
// */
|
*/
|
||||||
// @POST("/web/ai-user-gift/send")
|
@POST("/web/ai-user-gift/send")
|
||||||
// suspend fun sendGift(@Body dto: SendGift): Response<Any>
|
suspend fun sendGift(@Body dto: SendGift): Response<Any>
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 未读消息统计
|
* 未读消息统计
|
||||||
// */
|
*/
|
||||||
// @POST(BuildConfig.API_PIGEON + "/web/message/stat")
|
@POST(BuildConfig.API_PIGEON + "/web/message/stat")
|
||||||
// suspend fun getMessageStat(): Response<MessageStatOutput>
|
suspend fun getMessageStat(): Response<MessageStatOutput>
|
||||||
//
|
|
||||||
// /**
|
/**
|
||||||
// * 系统通知列表
|
* 系统通知列表
|
||||||
// */
|
*/
|
||||||
// @POST(BuildConfig.API_PIGEON + "/web/message/list")
|
@POST(BuildConfig.API_PIGEON + "/web/message/list")
|
||||||
// suspend fun getMessageList(@Body dto: PageQuery): Response<Pageable<MessageListOutput>>
|
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.factory.ServiceFactory
|
||||||
|
import com.remax.visualnovel.api.service.AIService
|
||||||
import com.remax.visualnovel.api.service.BookService
|
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.DictService
|
||||||
import com.remax.visualnovel.api.service.LoginService
|
import com.remax.visualnovel.api.service.LoginService
|
||||||
import com.remax.visualnovel.api.service.MessageService
|
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 com.remax.visualnovel.api.service.UserService
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -40,6 +44,22 @@ object ApiServiceModule {
|
||||||
@Provides
|
@Provides
|
||||||
fun bookService() = create<BookService>()
|
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 {
|
private inline fun <reified T> create(): T {
|
||||||
return ServiceFactory.createService()
|
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
|
package com.remax.visualnovel.entity.response
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.remax.visualnovel.entity.request.HeartbeatRelation
|
||||||
import com.remax.visualnovel.extension.calculateAge
|
import com.remax.visualnovel.extension.calculateAge
|
||||||
import com.remax.visualnovel.extension.getNimAccountId
|
import com.remax.visualnovel.extension.getNimAccountId
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
@ -49,8 +50,8 @@ data class Character(
|
||||||
var isHaveChatted: Boolean? = null, //是否聊过天
|
var isHaveChatted: Boolean? = null, //是否聊过天
|
||||||
var isDelChatted: Boolean? = null, //是否删除过会话
|
var isDelChatted: Boolean? = null, //是否删除过会话
|
||||||
var isAutoPlayVoice: Int? = null, //自动播放语音开关 1:开 0:关
|
var isAutoPlayVoice: Int? = null, //自动播放语音开关 1:开 0:关
|
||||||
//var aiUserHeartbeatRelation: HeartbeatRelation? = null,
|
var aiUserHeartbeatRelation: HeartbeatRelation? = null,
|
||||||
//var chatBubble: ChatBubble? = null,
|
var chatBubble: ChatBubble? = null,
|
||||||
|
|
||||||
//排行榜使用
|
//排行榜使用
|
||||||
var rankNo: Int? = null,
|
var rankNo: Int? = null,
|
||||||
|
|
@ -65,7 +66,7 @@ data class Character(
|
||||||
var role: String? = null,
|
var role: String? = null,
|
||||||
var tag: String? = null,
|
var tag: String? = null,
|
||||||
var isSecret: Boolean? = null,
|
var isSecret: Boolean? = null,
|
||||||
//var albumList: List<Album>? = null,
|
var albumList: List<Album>? = null,
|
||||||
|
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
companion object {
|
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
|
package com.remax.visualnovel.manager.login
|
||||||
|
|
||||||
import com.pengxr.modular.eventbus.generated.events.EventDefineOfUserEvents
|
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.entity.response.User
|
||||||
import com.remax.visualnovel.event.model.OnLoginEvent
|
import com.remax.visualnovel.event.model.OnLoginEvent
|
||||||
import com.remax.visualnovel.event.model.tab.MainTab
|
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.utils.Routers
|
||||||
import com.remax.visualnovel.ui.main.MainActivity
|
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
|
* Created by HJW on 2025/7/11
|
||||||
|
|
@ -20,6 +27,7 @@ object LoginManager {
|
||||||
set(value) {
|
set(value) {
|
||||||
loginInfoSave.putUser(value)
|
loginInfoSave.putUser(value)
|
||||||
field = value
|
field = value
|
||||||
|
WalletManager.refreshWallet()
|
||||||
}
|
}
|
||||||
|
|
||||||
var token: String? = null
|
var token: String? = null
|
||||||
|
|
@ -51,6 +59,7 @@ object LoginManager {
|
||||||
token = null
|
token = null
|
||||||
EventDefineOfUserEvents.onLoginStatusChanged().post(OnLoginEvent(OnLoginEvent.LOGOUT))
|
EventDefineOfUserEvents.onLoginStatusChanged().post(OnLoginEvent(OnLoginEvent.LOGOUT))
|
||||||
MainActivity.start(MainTab.TAB_BOOKS)
|
MainActivity.start(MainTab.TAB_BOOKS)
|
||||||
|
NimManager.logout()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,8 +82,16 @@ object LoginManager {
|
||||||
this.token = token
|
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())
|
MessageRepository(ServiceFactory.createService())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,5 +99,5 @@ object LoginManager {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
messageRepository.getMessageStat()
|
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.api.service.DictService
|
||||||
|
import com.remax.visualnovel.entity.request.AIIDRequest
|
||||||
import com.remax.visualnovel.repository.api.base.BaseRepository
|
import com.remax.visualnovel.repository.api.base.BaseRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -10,7 +11,7 @@ import javax.inject.Inject
|
||||||
*/
|
*/
|
||||||
class DictRepository @Inject constructor(private val dictService: DictService) : BaseRepository() {
|
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))
|
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 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
|
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.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.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
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,5 +37,135 @@ import javax.inject.Inject
|
||||||
*/
|
*/
|
||||||
class MessageRepository @Inject constructor(private val messageService: MessageService) : BaseRepository() {
|
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.content.Context
|
||||||
import android.util.AttributeSet
|
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.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.dylanc.viewbinding.nonreflection.inflate
|
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
|
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