diff --git a/VisualNovel/app/build.gradle.kts b/VisualNovel/app/build.gradle.kts index 9a66c87..d574124 100644 --- a/VisualNovel/app/build.gradle.kts +++ b/VisualNovel/app/build.gradle.kts @@ -124,6 +124,13 @@ android { buildConfigString("ABOUT_US", "https://www.xxxxx.ai/about") buildConfigString("API_FROG", "https://www.test-frog.xxxxx.ai") buildConfigString("EPAL_TERMS_SERVICES", "https://www.xxxxx.ai/policy/tos") + buildConfigString("API_SHARK", "https://test-shark.xxxxx.ai") + buildConfigString("API_COW", "https://test-cow.xxxxx.ai") + buildConfigString("API_PIGEON", "https://test-pigeon.xxxx.ai") + buildConfigString("API_LION", "https://test-lion.xxxx.ai") + buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge") + + buildConfigString("RTC_APP_ID", "689ade491323ae01797818e0-XXX-TODO") } @@ -135,6 +142,13 @@ android { buildConfigString("ABOUT_US", "https://test.xxxxx.ai/about") buildConfigString("API_FROG", "https://test-frog.xxxxx.ai") buildConfigString("EPAL_TERMS_SERVICES", "https://test.xxxxx.ai/policy/tos") + buildConfigString("API_SHARK", "https://test-shark.xxxxx.ai") + buildConfigString("API_COW", "https://test-cow.xxxxx.ai") + buildConfigString("API_PIGEON", "https://test-pigeon.xxxx.ai") + buildConfigString("API_LION", "https://test-lion.xxxx.ai") + buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge") + + buildConfigString("RTC_APP_ID", "689ade491323ae01797818e0-XXX-TODO") } } } @@ -274,6 +288,19 @@ dependencies { implementation(Deps.exoplayer) implementation(Deps.subsamplingScaleImageView) + //s3图片上传 oss + implementation(Deps.awsS3) + implementation(Deps.awsCore) + + // 网易 云信 + implementation(Deps.nimBase) + implementation(Deps.nimPush) + + //内购 / 充值 + implementation(Deps.billing) + + // RTC : 实时通信 + implementation(Deps.BytePlusRTC) implementation(project(mapOf("path" to ":loadingstateview"))) diff --git a/VisualNovel/app/src/main/AndroidManifest.xml b/VisualNovel/app/src/main/AndroidManifest.xml index 3a564f6..f247fad 100644 --- a/VisualNovel/app/src/main/AndroidManifest.xml +++ b/VisualNovel/app/src/main/AndroidManifest.xml @@ -73,7 +73,7 @@ diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/AIService.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/AIService.kt new file mode 100644 index 0000000..f135566 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/AIService.kt @@ -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 + + /** + * 卡片被喜欢推荐 + */ + @POST("/web/meet/rc") + suspend fun cardLiked(): Response + + /** + * 卡片上报 + */ + @POST("/web/meet/sd") + suspend fun reportCard(@Body request: CardRequest): Response + + /** + * 获取首页卡片列表 + */ + @POST("/web/home/rm-list") + suspend fun getHomeCard(@Body request: ClassificationRequest): Response> + + + /** + * 获取单个首页卡片 + */ + @POST(" /web/home/meet-detail") + suspend fun getHomeCardDetail(@Body request: AIIDRequest): Response + + /** + * 获取分类列表 + */ + @POST("/web/home/classification-list") + suspend fun getClassificationList(@Body request: ClassificationRequest): Response> + + /** + * 获取榜单 + */ + @POST("/web/rank/heartbeat") + suspend fun getHeartbeatRank(): Response> + + /** + * 获取榜单 + */ + @POST("/web/rank/gift") + suspend fun getGiftRank(): Response> + + /** + * 获取榜单 + */ + @POST("/web/rank/chat") + suspend fun getChatRank(): Response> + + /** + * 获取发现页顶部数据 + */ + @POST("/web/explore/info") + suspend fun getExploreInfo(): Response + + /** + * 解锁加密图片 + */ + @POST("/web/ai-user/unlock-album-img") + suspend fun unlockAlbum(@Body dto: ChatAlbum): Response + + /** + * 解锁秘密爱慕者 + */ + @POST("/web/meet/unlock") + suspend fun unlockSecret(@Body dto: AIIDRequest): Response + + /** + * 设置当前图片价格 + */ + @POST("/web/ai-user/set-album-unlock-price") + suspend fun setAlbumUnlockPrice(@Body dto: AlbumDTO): Response + + /** + * 删除AI角色 + */ + @POST("/web/ai-user/del") + suspend fun deleteAICharacter(@Body request: Character): Response + + /** + * 设置当前默认图片 + */ + @POST("/web/ai-user/set-default-album") + suspend fun setAlbumDefault(@Body dto: AlbumDTO): Response + + @POST("/web/ai-user/create-edit") + suspend fun createOrEditAICharacter(@Body request: Character): Response + + @POST("/web/ai-user/edit-head-img") + suspend fun editAIAvatar(@Body request: AIHeadImgRequest): Response + + + @POST(BuildConfig.API_COW + "/web/gen/user-content-v1") + suspend fun generateAICharacter(@Body request: AIGenerate): Response + + /** + * 编辑时获取我的ai角色信息 + */ + @POST("/web/ai-user/get-my-ai-user/info") + suspend fun getAICharacter(@Body request: Character): Response + + /** + * 访问AI个人主页时获取信息 + */ + @POST("/web/ai-user-search/base-info") + suspend fun getAICharacterProfile(@Body request: Character): Response + + /** + * 访问AI的统计信息 + */ + @POST("/web/ai-user/stat") + suspend fun getAICharacterStat(@Body request: Character): Response + + /** + * 修改点赞状态 + */ + @POST("/web/ai-user/like-or-cancel") + suspend fun setAILikeOrCancel(@Body request: AlbumDTO): Response + + /** + * 喜欢或取消喜欢相片 + */ + @POST("/web/album/like_or_cancel") + suspend fun setLikeOrDislike(@Body dto: AlbumDTO): Response + + /** + * 删除相片 + */ + @POST("/web/ai-user/album-del") + suspend fun deleteAlbum(@Body dto: AlbumDTO): Response + + /** + * 批量添加图片到相册 + */ + @POST("/web/ai-user/batch-add-album") + suspend fun addAlbum(@Body dto: AlbumCreate): Response + + /** + * 获取创作次数 + */ + @POST("/web/user/get-user-create-count") + suspend fun getAlbumCreateCount(): Response + + /** + * 购买创作次数 + */ + @POST("/web/ai/buy-create-image-count") + suspend fun buyAlbumCreateCount(@Body dto: SimpleCountDTO): Response + + /** + * 批量添加图片到聊天背景 + */ + @POST("/web/chat-background/batch-add") + suspend fun addChatBackground(@Body dto: AlbumCreate): Response + + /** + * 获取相册 分页 + */ + @POST("/web/ai-user/album-list") + suspend fun getAlbumList(@Body dto: QueryAlbumDTO): Response> + + /** + * 获取用户礼物 分页 + */ + @POST("/web/ai-user-gift/list") + suspend fun getUserGiftList(@Body dto: QueryAlbumDTO): Response> + + + /** + * AI一键生成-创建生成人物形象图片任务 + */ + @POST(BuildConfig.API_COW + "/web/gen/image-ct") + suspend fun generateImageBatch(@Body request: AIGenerateImage): Response + + /** + * AI一键生成-删除图片生成任务 + */ + @POST(BuildConfig.API_COW + "/web/gen/del") + suspend fun generateImageBatchDel(@Body request: AIGenerateImage): Response + + /** + * AI一键生成-轮询查询图片生成结果 + */ + @POST(BuildConfig.API_COW + "/web/gen/image-pl") + suspend fun generateImageBatchQuery(@Body request: AIGenerateImage): Response> + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/ChatService.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/ChatService.kt new file mode 100644 index 0000000..3743d08 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/ChatService.kt @@ -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 + + /** + * 获取IM中AI的基础信息 + */ + @POST("/web/ai-user-search/im-base-info") + suspend fun getIMAICharacterProfile(@Body request: Character): Response + + /** + * 访问解锁加密图片 + */ + @POST("/web/ai-user/view-unlock-album-img") + suspend fun viewAlbumImg(@Body request: ChatAlbum): Response + + /** + * 关系列表 + */ + @POST("/web/ai-user/heartbeat-relation-list") + suspend fun getMyFriends(@Body request: SearchPage): Response> + + + @POST("/web/ai-user/heartbeat-rank") + suspend fun getMyFriendRank(): Response + + /** + * 生成提示词 + */ + @POST(BuildConfig.API_COW + "/web/gen/sup-content-v2") + suspend fun getPrompts(@Body request: AIIDRequest): Response> + + /** + * AI回话点赞/点踩 + */ + @POST(BuildConfig.API_PIGEON + "/web/fb/v1") + suspend fun aiFeedback(@Body request: AIFeedback): Response + + /** + * 获取RTC + */ + @POST(BuildConfig.API_COW + "/web/voice-chat/gen-rtc-tk") + suspend fun getRTCToken(@Body request: RTCRequest): Response + + /** + * 操作通话 + */ + @POST(BuildConfig.API_COW + "/web/voice-chat/opt") + suspend fun voiceChatOpt(@Body request: RTCRequest): Response + + /** + * 获取聊天背景列表 + */ + @POST("/web/chat-background/list") + suspend fun getChatBackgroundList(@Body request: AIIDRequest): Response> + + /** + * 获取聊天设置 + */ + @POST("/web/chat-set/get-my") + suspend fun getChatSetting(@Body request: ChatSetting): Response + + /** + * 修改聊天设置 + */ + @POST("/web/chat-set/set") + suspend fun setChatSetting(@Body request: ChatSet): Response + + /** + * 修改聊天气泡 + */ + @POST("/web/chat-set/set-chat-bubble") + suspend fun setChatBubble(@Body request: ChatSetting): Response + + /** + * 修改聊天模型 + */ + @POST("/web/chat-set/set-chat-model") + suspend fun setChatModel(@Body request: ChatSetting): Response + + /** + * 修改是否自动播放语音 + */ + @POST("/web/chat-set/auto-play-voice") + suspend fun setChatAutoPlay(@Body request: ChatSetting): Response + + /** + * 修改聊天背景图 + */ + @POST("/web/chat-background/set-background") + suspend fun setChatBackground(@Body request: ChatSetting): Response + + /** + * 删除聊天背景图 + */ + @POST("/web/chat-background/del") + suspend fun deleteChatBackground(@Body request: ChatSetting): Response + + /** + * 展示心动关系开关 + */ + @POST("/web/ai-user/heartbeat-relation-switch") + suspend fun relationSwitch(@Body request: AIIsShowDTO): Response + + /** + * 语音转文本 + */ + @POST(BuildConfig.API_COW + "/web/voice/asr-v2") + suspend fun voiceASR(@Body request: SimpleDataDTO): Response + + /** + * 生成语音 + */ + @POST(BuildConfig.API_COW + "/web/voice/tts-v2") + suspend fun voiceTTS(@Body request: VoiceTTS): Response + + /** + * 获取心动等级 + */ + @POST("/web/ai-user/heartbeat-level") + suspend fun getHeartbeatLevel(@Body request: Character): Response + + /** + * 购买心动值 + */ + @POST("/web/ai-user/buy-heartbeat-val") + suspend fun buyHeartbeatVal(@Body request: HeartbeatBuy): Response + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/DictService.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/DictService.kt index 343dd38..40f60e7 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/DictService.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/DictService.kt @@ -1,6 +1,13 @@ package com.remax.visualnovel.api.service +import com.remax.visualnovel.entity.request.AIIDRequest +import com.remax.visualnovel.entity.request.Gift +import com.remax.visualnovel.entity.request.PageQuery +import com.remax.visualnovel.entity.response.AIDict +import com.remax.visualnovel.entity.response.ChatBubble +import com.remax.visualnovel.entity.response.ChatModel +import com.remax.visualnovel.entity.response.Pageable import com.remax.visualnovel.entity.response.base.Response import retrofit2.http.Body import retrofit2.http.POST @@ -10,24 +17,24 @@ interface DictService { /** * 获取聊天气泡字典 */ - /*@POST("/web/chat-set/get-chat-bubble-list") - suspend fun getChatBubbleList(@Body request: AIIDRequest): Response>*/ + @POST("/web/chat-set/get-chat-bubble-list") + suspend fun getChatBubbleList(@Body request: AIIDRequest): Response> /** * AI标签 */ - /*@POST("/web/get-ai-dict") - suspend fun getAIDict(): Response*/ + @POST("/web/get-ai-dict") + suspend fun getAIDict(): Response /** * 礼物字典 */ - /*@POST("/web/gift/dict-list") - suspend fun getGiftDict(@Body pageQuery: PageQuery = PageQuery(1).apply { page.ps = 100 }): Response>*/ + @POST("/web/gift/dict-list") + suspend fun getGiftDict(@Body pageQuery: PageQuery = PageQuery(1).apply { page.ps = 100 }): Response> /** * chat模型 */ - /*@POST("/web/chat-model/dict-list") - suspend fun getAIChatModel(): Response>*/ + @POST("/web/chat-model/dict-list") + suspend fun getAIChatModel(): Response> } \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/MessageService.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/MessageService.kt index fcba604..fd60258 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/MessageService.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/MessageService.kt @@ -1,34 +1,40 @@ package com.remax.visualnovel.api.service import com.remax.visualnovel.BuildConfig +import com.remax.visualnovel.entity.request.AIListRequest +import com.remax.visualnovel.entity.request.PageQuery +import com.remax.visualnovel.entity.request.SendGift +import com.remax.visualnovel.entity.response.MessageListOutput +import com.remax.visualnovel.entity.response.MessageStatOutput +import com.remax.visualnovel.entity.response.Pageable import com.remax.visualnovel.entity.response.base.Response import retrofit2.http.Body import retrofit2.http.POST interface MessageService { -// /** -// * 删除会话 -// */ -// @POST(BuildConfig.API_COW + "/web/ai-message/del") -// suspend fun deleteConversation(@Body request: AIListRequest): Response -// -// /** -// * 送礼物 -// */ -// @POST("/web/ai-user-gift/send") -// suspend fun sendGift(@Body dto: SendGift): Response -// -// /** -// * 未读消息统计 -// */ -// @POST(BuildConfig.API_PIGEON + "/web/message/stat") -// suspend fun getMessageStat(): Response -// -// /** -// * 系统通知列表 -// */ -// @POST(BuildConfig.API_PIGEON + "/web/message/list") -// suspend fun getMessageList(@Body dto: PageQuery): Response> + /** + * 删除会话 + */ + @POST(BuildConfig.API_COW + "/web/ai-message/del") + suspend fun deleteConversation(@Body request: AIListRequest): Response + + /** + * 送礼物 + */ + @POST("/web/ai-user-gift/send") + suspend fun sendGift(@Body dto: SendGift): Response + + /** + * 未读消息统计 + */ + @POST(BuildConfig.API_PIGEON + "/web/message/stat") + suspend fun getMessageStat(): Response + + /** + * 系统通知列表 + */ + @POST(BuildConfig.API_PIGEON + "/web/message/list") + suspend fun getMessageList(@Body dto: PageQuery): Response> } \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/OssService.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/OssService.kt new file mode 100644 index 0000000..b4a0a98 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/OssService.kt @@ -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 + + /** + * 图片鉴黄 + */ + @POST(BuildConfig.API_SHARK + "/web/file/check") + suspend fun checkS3Img( + @Body imgCheckDTO: ImgCheckDTO + ): Response + + /** + * 关键字校验 + */ + @POST("/web/check_text") + suspend fun checkText( + @Body simpleContentDTO: SimpleContentDTO + ): Response + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/PayService.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/PayService.kt new file mode 100644 index 0000000..5e95e48 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/PayService.kt @@ -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 + + /** + * 获取我的钱包 + */ + @POST(BuildConfig.API_LION + "/web/pay/account/wallet") + suspend fun getMyWallet(): Response + + /** + * 获取充值产品 + */ + @POST(BuildConfig.API_LION + "/web/pay/config/charge-product-list") + suspend fun getChargeProducts( + @Body dto: ChargeProductDTO = ChargeProductDTO() + ): Response + + + /** + * 获取vip订阅价格列表 + */ + @POST(BuildConfig.API_LION + "/web/pay/config/sub-product-list") + suspend fun getSubPriceList( + @Body subPriceDTO: SubPriceDTO = SubPriceDTO() + ): Response> + + /** + * 会员特权列表 + */ + @POST(BuildConfig.API_LION + "/web/member/detail") + suspend fun getVipPrivilegeList(): Response + + /** + * 创建一个订单 + */ + @POST(BuildConfig.API_LION +"/web/pay/trade/pre-charge-google") + suspend fun createOrder( + @Body dto: ChargeOrderDTO + ): Response + + /** + * 验证支付是否成功 + */ + @POST(BuildConfig.API_LION +"/web/pay/webhooks/google/v2") + suspend fun validateTranslation( + @Body dto: ValidateTransactionDTO + ): Response + + /** + * 验证订阅是否成功 + */ + @POST(BuildConfig.API_LION +"/web/pay/subscribe/upload-google-receipt") + suspend fun uploadGoogleReceipt( + @Body dto: ValidateTransactionDTO + ): Response + + /** + * 订阅/升级VIP前查询订阅信息 + */ + @POST(BuildConfig.API_LION +"/web/pay/appStore/getUserSubscription") + suspend fun checkSubInfo(): Response + + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/app/di/ApiServiceModule.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/app/di/ApiServiceModule.kt index 0420e5d..b9c116d 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/app/di/ApiServiceModule.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/app/di/ApiServiceModule.kt @@ -2,10 +2,14 @@ package com.remax.visualnovel.app.di import com.remax.visualnovel.api.factory.ServiceFactory +import com.remax.visualnovel.api.service.AIService import com.remax.visualnovel.api.service.BookService +import com.remax.visualnovel.api.service.ChatService import com.remax.visualnovel.api.service.DictService import com.remax.visualnovel.api.service.LoginService import com.remax.visualnovel.api.service.MessageService +import com.remax.visualnovel.api.service.OssService +import com.remax.visualnovel.api.service.PayService import com.remax.visualnovel.api.service.UserService import dagger.Module import dagger.Provides @@ -40,6 +44,22 @@ object ApiServiceModule { @Provides fun bookService() = create() + @Singleton + @Provides + fun aiService() = create() + + @Singleton + @Provides + fun ossService() = create() + + @Singleton + @Provides + fun payService() = create() + + @Singleton + @Provides + fun chatService() = create() + private inline fun create(): T { return ServiceFactory.createService() diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/app/viewmodel/base/OssViewModel.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/app/viewmodel/base/OssViewModel.kt new file mode 100644 index 0000000..e81ec90 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/app/viewmodel/base/OssViewModel.kt @@ -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 { + 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 封装成服务器返回一致类型处理 + */ + suspend fun ossUploadFile( + filePath: String, + ossType: String, + isImg: Boolean = true, + checkNSFW: Boolean = true, + checkRealPerson: Boolean = false, + checkKid: Boolean = false, + token: BucketBean? = null + ): Response { + /** + * 获取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 = ossRepository.checkText(content) + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMAIInMessage.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMAIInMessage.kt new file mode 100644 index 0000000..7c62f0d --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMAIInMessage.kt @@ -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) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMBaseInfoMessage.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMBaseInfoMessage.kt new file mode 100644 index 0000000..e512bd0 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMBaseInfoMessage.kt @@ -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) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMCallMessage.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMCallMessage.kt new file mode 100644 index 0000000..f13ea8b --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMCallMessage.kt @@ -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) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMGiftMessage.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMGiftMessage.kt new file mode 100644 index 0000000..d57d1bc --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMGiftMessage.kt @@ -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) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMInImageMessage.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMInImageMessage.kt new file mode 100644 index 0000000..22f30fb --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMInImageMessage.kt @@ -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) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMLevelMessage.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMLevelMessage.kt new file mode 100644 index 0000000..1721863 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMLevelMessage.kt @@ -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) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMMessageWrapper.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMMessageWrapper.kt new file mode 100644 index 0000000..daf1e60 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMMessageWrapper.kt @@ -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 + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMOutImageMessage.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMOutImageMessage.kt new file mode 100644 index 0000000..dc8e199 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/IMOutImageMessage.kt @@ -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) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/RecentContactWrapper.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/RecentContactWrapper.kt new file mode 100644 index 0000000..cb34887 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/RecentContactWrapper.kt @@ -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("@")) + } +} + diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/voice/IMVoice.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/voice/IMVoice.kt new file mode 100644 index 0000000..6190590 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/imbean/voice/IMVoice.kt @@ -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 ?: "" + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIGenerate.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIGenerate.kt new file mode 100644 index 0000000..3af0402 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIGenerate.kt @@ -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" + + } +} + diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIGenerateImage.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIGenerateImage.kt new file mode 100644 index 0000000..c0c3fca --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIGenerateImage.kt @@ -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" + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIHeadImgRequest.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIHeadImgRequest.kt new file mode 100644 index 0000000..78e33db --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIHeadImgRequest.kt @@ -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?, +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIIDRequest.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIIDRequest.kt new file mode 100644 index 0000000..339c87d --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIIDRequest.kt @@ -0,0 +1,8 @@ +package com.remax.visualnovel.entity.request + +/** + * Created by HJW on 2025/8/24 + */ +open class AIIDRequest( + open val aiId: String +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIIsShowDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIIsShowDTO.kt new file mode 100644 index 0000000..5eb57af --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIIsShowDTO.kt @@ -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:打开 +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIListRequest.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIListRequest.kt new file mode 100644 index 0000000..f130006 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIListRequest.kt @@ -0,0 +1,8 @@ +package com.remax.visualnovel.entity.request + +/** + * Created by HJW on 2025/8/24 + */ +data class AIListRequest( + val aiIdList: List +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AlbumDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AlbumDTO.kt new file mode 100644 index 0000000..e893b07 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AlbumDTO.kt @@ -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 +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/CardRequest.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/CardRequest.kt new file mode 100644 index 0000000..29707f1 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/CardRequest.kt @@ -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 +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChargeOrderDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChargeOrderDTO.kt new file mode 100644 index 0000000..fa2e843 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChargeOrderDTO.kt @@ -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, +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChargeProduct.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChargeProduct.kt new file mode 100644 index 0000000..04a1a3f --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChargeProduct.kt @@ -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, + val countdown: Long? = null +) + diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChargeProductDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChargeProductDTO.kt new file mode 100644 index 0000000..f09f4fa --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChargeProductDTO.kt @@ -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 +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChatAlbum.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChatAlbum.kt new file mode 100644 index 0000000..7c27d21 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChatAlbum.kt @@ -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 +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChatSetting.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChatSetting.kt new file mode 100644 index 0000000..d2ca539 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ChatSetting.kt @@ -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, +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ClassificationRequest.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ClassificationRequest.kt new file mode 100644 index 0000000..6f308c0 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ClassificationRequest.kt @@ -0,0 +1,46 @@ +package com.remax.visualnovel.entity.request + +/** + * Created by HJW on 2025/9/9 + */ +data class ClassificationRequest( + /** + * 情感性格code + */ + var characterCodeList: List?=null, + + /** + * 需要排除的aiId列表 + */ + var exList: MutableList = mutableListOf(), + + /** + * 页码 + */ + var pn: Int = 1, + + /** + * 年龄 + */ + var age: String? = null, + + /** + * 性别:单选 + */ + var sex: Int? = null, + + /** + * 角色code列表 + */ + var roleCodeList: List?=null, + + /** + * 标签code列表 + */ + var tagCodeList: List?=null, + + /** + * 每页大小 + */ + val ps: Int = PageQuery.DEFAULT_PAGE_SIZE, +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/Gift.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/Gift.kt new file mode 100644 index 0000000..69881f2 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/Gift.kt @@ -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 diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/HeartbeatBuy.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/HeartbeatBuy.kt new file mode 100644 index 0000000..642a58c --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/HeartbeatBuy.kt @@ -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?, +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/HeartbeatRelation.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/HeartbeatRelation.kt new file mode 100644 index 0000000..68aa43a --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/HeartbeatRelation.kt @@ -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 } + +} + diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ImgCheckDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ImgCheckDTO.kt new file mode 100644 index 0000000..cb604f0 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ImgCheckDTO.kt @@ -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, +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/PageQuery.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/PageQuery.java new file mode 100644 index 0000000..3fe9dc3 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/PageQuery.java @@ -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; + } + + + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/QueryAlbumDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/QueryAlbumDTO.kt new file mode 100644 index 0000000..15ee706 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/QueryAlbumDTO.kt @@ -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 +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/RTCRequest.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/RTCRequest.kt new file mode 100644 index 0000000..3c3d5e2 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/RTCRequest.kt @@ -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" + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/S3TypeDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/S3TypeDTO.kt new file mode 100644 index 0000000..e17671c --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/S3TypeDTO.kt @@ -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" + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SearchPage.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SearchPage.kt new file mode 100644 index 0000000..4c169a2 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SearchPage.kt @@ -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, +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SendGift.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SendGift.kt new file mode 100644 index 0000000..5067025 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SendGift.kt @@ -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" + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleContentDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleContentDTO.kt new file mode 100644 index 0000000..dcc05ae --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleContentDTO.kt @@ -0,0 +1,5 @@ +package com.remax.visualnovel.entity.request + +data class SimpleContentDTO( + val content: String +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleCountDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleCountDTO.kt new file mode 100644 index 0000000..5be2aed --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleCountDTO.kt @@ -0,0 +1,8 @@ +package com.remax.visualnovel.entity.request + +/** + * Created by HJW on 2025/9/17 + */ +data class SimpleCountDTO( + val count: Int +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleDataDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleDataDTO.kt new file mode 100644 index 0000000..57fd206 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleDataDTO.kt @@ -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, +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleTypeDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleTypeDTO.kt new file mode 100644 index 0000000..9b68f54 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SimpleTypeDTO.kt @@ -0,0 +1,5 @@ +package com.remax.visualnovel.entity.request + +data class SimpleTypeDTO( + val type: String +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SubPriceDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SubPriceDTO.kt new file mode 100644 index 0000000..48be7fc --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/SubPriceDTO.kt @@ -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" +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ValidateTransactionDTO.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ValidateTransactionDTO.kt new file mode 100644 index 0000000..924a191 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/ValidateTransactionDTO.kt @@ -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, +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/VoiceTTS.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/VoiceTTS.kt new file mode 100644 index 0000000..95ee9f7 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/VoiceTTS.kt @@ -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" + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AccountBuffBill.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AccountBuffBill.kt new file mode 100644 index 0000000..665abcc --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AccountBuffBill.kt @@ -0,0 +1,38 @@ +package com.remax.visualnovel.entity.response + +/** + * Created by HJW on 2025/9/12 + */ + +data class Transaction( + val pageList: Pageable +) + +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" + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AdvertiseOutput.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AdvertiseOutput.kt new file mode 100644 index 0000000..a2ac346 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AdvertiseOutput.kt @@ -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" + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AlbumCreateCountOutput.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AlbumCreateCountOutput.kt new file mode 100644 index 0000000..28ea8be --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AlbumCreateCountOutput.kt @@ -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) +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/BucketBean.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/BucketBean.kt new file mode 100644 index 0000000..5e9f7a1 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/BucketBean.kt @@ -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 +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Character.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Character.kt index 8aae888..6b3d6a0 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Character.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Character.kt @@ -1,6 +1,7 @@ package com.remax.visualnovel.entity.response import android.os.Parcelable +import com.remax.visualnovel.entity.request.HeartbeatRelation import com.remax.visualnovel.extension.calculateAge import com.remax.visualnovel.extension.getNimAccountId import kotlinx.parcelize.Parcelize @@ -49,8 +50,8 @@ data class Character( var isHaveChatted: Boolean? = null, //是否聊过天 var isDelChatted: Boolean? = null, //是否删除过会话 var isAutoPlayVoice: Int? = null, //自动播放语音开关 1:开 0:关 - //var aiUserHeartbeatRelation: HeartbeatRelation? = null, - //var chatBubble: ChatBubble? = null, + var aiUserHeartbeatRelation: HeartbeatRelation? = null, + var chatBubble: ChatBubble? = null, //排行榜使用 var rankNo: Int? = null, @@ -65,7 +66,7 @@ data class Character( var role: String? = null, var tag: String? = null, var isSecret: Boolean? = null, - //var albumList: List? = null, + var albumList: List? = null, ) : Parcelable { companion object { diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChargeOrder.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChargeOrder.java new file mode 100644 index 0000000..59c6074 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChargeOrder.java @@ -0,0 +1,6 @@ +package com.remax.visualnovel.entity.response; + + +public class ChargeOrder { + public String tradeNo; +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatBubble.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatBubble.kt new file mode 100644 index 0000000..9fd0233 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatBubble.kt @@ -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" + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatModel.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatModel.kt new file mode 100644 index 0000000..01f2405 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatModel.kt @@ -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, + +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatSet.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatSet.kt new file mode 100644 index 0000000..b479aaa --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatSet.kt @@ -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 diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ContentRes.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ContentRes.kt new file mode 100644 index 0000000..a4d335a --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ContentRes.kt @@ -0,0 +1,8 @@ +package com.remax.visualnovel.entity.response + +/** + * Created by HJW on 2025/7/29 + */ +open class ContentRes( + val content: String? +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ExploreInfo.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ExploreInfo.kt new file mode 100644 index 0000000..eeafb49 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ExploreInfo.kt @@ -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? = null, + + /** + * AI总心动值榜单top3 + */ + val aiGiftRankTop3List: List? = null, + + /** + * AI总心动值榜单top3 + */ + val aiHeartbeatRankTop3List: List? = null, + + /** + * 广告列表 + */ + val outputList: List? = null +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Friends.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Friends.kt new file mode 100644 index 0000000..3fc0dc8 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Friends.kt @@ -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 +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/HeartbeatLevel.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/HeartbeatLevel.kt new file mode 100644 index 0000000..b043c6d --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/HeartbeatLevel.kt @@ -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), + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/HeartbeatLevelOutput.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/HeartbeatLevelOutput.kt new file mode 100644 index 0000000..20559fa --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/HeartbeatLevelOutput.kt @@ -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 +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/MeetSdOutput.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/MeetSdOutput.kt new file mode 100644 index 0000000..a1dd08f --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/MeetSdOutput.kt @@ -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 +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/MessageListOutput.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/MessageListOutput.kt new file mode 100644 index 0000000..b2de65e --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/MessageListOutput.kt @@ -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 + ) +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/MessageStatOutput.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/MessageStatOutput.kt new file mode 100644 index 0000000..5c4fdf1 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/MessageStatOutput.kt @@ -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 +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/NimBean.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/NimBean.kt new file mode 100644 index 0000000..b517ef4 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/NimBean.kt @@ -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, +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Pageable.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Pageable.java new file mode 100644 index 0000000..5151032 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Pageable.java @@ -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 { + + public static final int DEFAULT_PAGE_SIZE = 20; + + public List 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; + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/SubPrice.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/SubPrice.kt new file mode 100644 index 0000000..654821d --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/SubPrice.kt @@ -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" + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Token.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Token.kt new file mode 100644 index 0000000..f7c9c02 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Token.kt @@ -0,0 +1,8 @@ +package com.remax.visualnovel.entity.response + +/** + * Created by HJW on 2025/8/25 + */ +data class Token( + val token: String +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/UserSubInfo.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/UserSubInfo.kt new file mode 100644 index 0000000..3c47d61 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/UserSubInfo.kt @@ -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, +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/VipItemPrivilege.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/VipItemPrivilege.kt new file mode 100644 index 0000000..2350f06 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/VipItemPrivilege.kt @@ -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? = null, + /** + * 用户会员信息 + */ + val userMemberInfo: UserSubInfo? = null + +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/VoiceASR.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/VoiceASR.kt new file mode 100644 index 0000000..39f0dc8 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/VoiceASR.kt @@ -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, +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Wallet.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Wallet.kt new file mode 100644 index 0000000..f5e6b6f --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Wallet.kt @@ -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 + diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/event/model/OnAILiked.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/event/model/OnAILiked.kt new file mode 100644 index 0000000..e7f53dc --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/event/model/OnAILiked.kt @@ -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? +) \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/event/modular/UserAIEvents.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/event/modular/UserAIEvents.kt new file mode 100644 index 0000000..37c76b8 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/event/modular/UserAIEvents.kt @@ -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 +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/event/modular/WalletEvents.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/event/modular/WalletEvents.kt new file mode 100644 index 0000000..34fa373 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/event/modular/WalletEvents.kt @@ -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? +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/gift/GiftManager.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/gift/GiftManager.kt new file mode 100644 index 0000000..acba02c --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/gift/GiftManager.kt @@ -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? = 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() + } + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/login/LoginManager.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/login/LoginManager.kt index 468b199..de44bfe 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/login/LoginManager.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/login/LoginManager.kt @@ -1,11 +1,18 @@ package com.remax.visualnovel.manager.login import com.pengxr.modular.eventbus.generated.events.EventDefineOfUserEvents +import com.remax.visualnovel.api.factory.ServiceFactory import com.remax.visualnovel.entity.response.User import com.remax.visualnovel.event.model.OnLoginEvent import com.remax.visualnovel.event.model.tab.MainTab +import com.remax.visualnovel.manager.nim.NimManager +import com.remax.visualnovel.repository.api.MessageRepository import com.remax.visualnovel.utils.Routers import com.remax.visualnovel.ui.main.MainActivity +import com.remax.visualnovel.ui.wallet.manager.WalletManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch /** * Created by HJW on 2025/7/11 @@ -20,6 +27,7 @@ object LoginManager { set(value) { loginInfoSave.putUser(value) field = value + WalletManager.refreshWallet() } var token: String? = null @@ -51,6 +59,7 @@ object LoginManager { token = null EventDefineOfUserEvents.onLoginStatusChanged().post(OnLoginEvent(OnLoginEvent.LOGOUT)) MainActivity.start(MainTab.TAB_BOOKS) + NimManager.logout() } /** @@ -73,8 +82,16 @@ object LoginManager { this.token = token } + var contactUnreadCount: Int = 0 + set(value) { + field = value + EventDefineOfUserEvents.onUserUnReadChanged().post(null) + } + get() { + return field + if (NimManager.isLogin) NimManager.totalUnreadCount else 0 + } - /*private val messageRepository by lazy { + private val messageRepository by lazy { MessageRepository(ServiceFactory.createService()) } @@ -82,5 +99,5 @@ object LoginManager { CoroutineScope(Dispatchers.Main).launch { messageRepository.getMessageStat() } - }*/ + } } \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/nim/FetchResult.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/nim/FetchResult.kt new file mode 100644 index 0000000..44289f5 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/nim/FetchResult.kt @@ -0,0 +1,93 @@ +package com.remax.visualnovel.manager.nim + +import android.content.Context + +/** + * Created by HJW on 2025/8/20 + */ +class FetchResult(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 + } + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/nim/LoadStatus.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/nim/LoadStatus.kt new file mode 100644 index 0000000..35584d3 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/nim/LoadStatus.kt @@ -0,0 +1,11 @@ +package com.remax.visualnovel.manager.nim + +/** + * Created by HJW on 2025/8/20 + */ +enum class LoadStatus { + + Loading, Error, Success, Finish + +} + diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/nim/NimManager.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/nim/NimManager.kt new file mode 100644 index 0000000..b65b74a --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/nim/NimManager.kt @@ -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? + ) { + + } + } + + 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(false) + val conversationSyncLiveData: LiveData = _conversationSyncLiveData + + // 会话变化LiveData,用于通知会话信息变化变更 + private val _updateLiveData = MutableLiveData?>() + val updateLiveData: LiveData?> = _updateLiveData + + // 删除会话LiveData,用于通知会话删除结果 + private val _deleteLiveData = MutableLiveData?>() + val deleteLiveData: LiveData?> = _deleteLiveData + + // 创建会话 + private val _addLiveData = MutableLiveData() + val addLiveData: LiveData = _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) { + log("会话被删除 $conversationIds") + _deleteLiveData.value = conversationIds + } + + /** + * 会话变更回调。当置顶会话、会话有新消息、主动更新会话成功时会触发该回调。 + */ + override fun onConversationChanged(conversationList: List) { + 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, + val hasMore: Boolean + ) + + var allConversation: MutableList? = null + + private var queryConversationStart = false + + fun getUserList(accountIds: List?) { + 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().apply { +// put(UserInfoFieldEnum.EXTEND, Gson().toJson(extension)) +// } +// NIMClient.getService(UserService::class.java).updateUserInfo(fields) +// .setCallback(object : RequestCallbackWrapper() { +// 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() + val messageStatus: LiveData = _messageStatus + + private val messageStatusObserver: Observer = Observer { + 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? = 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) + } + +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/pay/GooglePayManager.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/pay/GooglePayManager.kt new file mode 100644 index 0000000..d5c27b5 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/manager/pay/GooglePayManager.kt @@ -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() + private set + var subProductDetails: MutableList = 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() + private var subList = arrayListOf() + + private fun queryProductDetails() { + val immutableList = arrayListOf() + 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() + 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?) { + 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() + } + } + }) + } + } + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/AIRepository.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/AIRepository.kt new file mode 100644 index 0000000..86903cf --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/AIRepository.kt @@ -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 { + return if (WalletManager.balance < 5000L) { + WalletManager.showChargeDialog() + ApiFailedResponse() + } else { + executeHttp { + aiService.unlockSecret(dto) + } + }.transformResult({ + WalletManager.refreshWallet() + }) + } + + suspend fun unlockAlbum(dto: ChatAlbum): Response { + 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)) +// } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ChatRepository.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ChatRepository.kt new file mode 100644 index 0000000..4c35748 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ChatRepository.kt @@ -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) + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/DictRepository.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/DictRepository.kt index 3dacf11..f8a4b9c 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/DictRepository.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/DictRepository.kt @@ -2,6 +2,7 @@ package com.remax.visualnovel.repository.api import com.remax.visualnovel.api.service.DictService +import com.remax.visualnovel.entity.request.AIIDRequest import com.remax.visualnovel.repository.api.base.BaseRepository import javax.inject.Inject @@ -10,7 +11,7 @@ import javax.inject.Inject */ class DictRepository @Inject constructor(private val dictService: DictService) : BaseRepository() { - /*suspend fun getChatBubbleList(aiId: String) = executeHttp { + suspend fun getChatBubbleList(aiId: String) = executeHttp { dictService.getChatBubbleList(AIIDRequest(aiId)) } @@ -20,6 +21,6 @@ class DictRepository @Inject constructor(private val dictService: DictService) : suspend fun getGiftDict() = executeHttp(false) { dictService.getGiftDict() } - suspend fun getAIChatModel() = executeHttp { dictService.getAIChatModel() }*/ + suspend fun getAIChatModel() = executeHttp { dictService.getAIChatModel() } } \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/MessageRepository.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/MessageRepository.kt index 5c68926..2101984 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/MessageRepository.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/MessageRepository.kt @@ -1,8 +1,34 @@ package com.remax.visualnovel.repository.api +import android.os.Handler +import android.os.Looper +import com.google.gson.Gson +import com.netease.nimlib.sdk.NIMClient +import com.netease.nimlib.sdk.v2.conversation.V2NIMConversationService +import com.netease.nimlib.sdk.v2.message.V2NIMMessage +import com.netease.nimlib.sdk.v2.message.V2NIMMessageListener +import com.netease.nimlib.sdk.v2.message.V2NIMMessageService +import com.netease.nimlib.sdk.v2.message.enums.V2NIMMessageQueryDirection +import com.netease.nimlib.sdk.v2.message.option.V2NIMCloudMessageListOption import com.remax.visualnovel.api.service.MessageService +import com.remax.visualnovel.entity.imbean.IMMessageWrapper +import com.remax.visualnovel.entity.request.AIListRequest +import com.remax.visualnovel.entity.request.Gift +import com.remax.visualnovel.entity.request.PageQuery +import com.remax.visualnovel.entity.request.SendGift +import com.remax.visualnovel.entity.response.Character +import com.remax.visualnovel.entity.response.base.ApiFailedResponse +import com.remax.visualnovel.entity.response.base.Response +import com.remax.visualnovel.extension.resumeWithActive +import com.remax.visualnovel.manager.login.LoginManager +import com.remax.visualnovel.manager.nim.FetchResult +import com.remax.visualnovel.manager.nim.LoadStatus +import com.remax.visualnovel.manager.nim.NimManager import com.remax.visualnovel.repository.api.base.BaseRepository +import com.remax.visualnovel.repository.ext.convertMessage +import com.remax.visualnovel.ui.wallet.manager.WalletManager +import kotlinx.coroutines.suspendCancellableCoroutine import javax.inject.Inject @@ -11,5 +37,135 @@ import javax.inject.Inject */ class MessageRepository @Inject constructor(private val messageService: MessageService) : BaseRepository() { + private val v2NIMConversationService by lazy { + NIMClient.getService(V2NIMConversationService::class.java) + } + + /** + * 批量删除会话 + */ + fun deleteConversationListByIds(ids: List, callback: () -> Unit) { + v2NIMConversationService.deleteConversationListByIds(ids, true, { + NimManager.log("删除会话成功 ${Gson().toJson(it)}") + callback.invoke() + }) { + NimManager.log("删除会话失败 ${Gson().toJson(it)}") + } + } + + suspend fun deleteConversation(aiIdList: List) = executeHttp { + messageService.deleteConversation(AIListRequest(aiIdList)) + } + + private val messageFetchResult = FetchResult>(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 { + 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)) + } + } diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/OssRepository.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/OssRepository.kt new file mode 100644 index 0000000..000a45a --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/OssRepository.kt @@ -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 = + executeHttp { + val bucketDTO = S3TypeDTO(ossType, postfix) + ossService.getS3Bucket(bucketDTO) + } + + /** + * 图片鉴黄 + */ + suspend fun checkS3Img(imgCheckDTO: ImgCheckDTO): Response = + executeHttp { + ossService.checkS3Img(imgCheckDTO = imgCheckDTO) + } + + suspend fun checkText(content: String): Response = + executeHttp { + ossService.checkText(simpleContentDTO = SimpleContentDTO(content)) + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/PayRepository.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/PayRepository.kt new file mode 100644 index 0000000..7937daf --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/PayRepository.kt @@ -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 = executeHttp(false) { + accountService.getMyWallet() + }.transformResult({ + EventDefineOfWalletEvents.buffBalanceUpdateSucceeded().post(it) + }) + + /** + * 验证支付是否成功 + */ + suspend fun validateTranslation(dto: ValidateTransactionDTO): Response = + executeHttp { + accountService.validateTranslation(dto = dto) + } + + /** + * 获取充值产品 + */ + suspend fun getProducts(): Response = + 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 = executeHttp { + accountService.uploadGoogleReceipt(dto = dto) + } + + /** + * 订阅/升级VIP前查询订阅信息 + */ + suspend fun checkSubInfo(): Response = executeHttp { + accountService.checkSubInfo() + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/ext/MessageExt.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/ext/MessageExt.kt new file mode 100644 index 0000000..387b85e --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/ext/MessageExt.kt @@ -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?.convertMessage(hasMore: Boolean, character: Character?): List { + 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()?.type?.uppercase() + when (type) { + /** + * 用户发送给AI的图片消息 + */ + CustomRawData.IMAGE -> { + if (isSelf) { + IMOutImageMessage(this, attachment?.raw?.convertFromJson()) + } else { + IMInImageMessage(this, attachment?.raw?.convertFromJson()) + } + } + + /** + * 用户发送给AI的礼物消息 + */ + CustomRawData.GIFT -> { + if (isSelf) { + IMGiftMessage(this, attachment?.raw?.convertFromJson()) + } else null + } + + /** + * 心动等级变化 + */ + CustomRawData.HEARTBEAT_LEVEL_UP, CustomRawData.HEARTBEAT_LEVEL_DOWN -> { + fetchType = FetchResult.FetchType.Remind + aiIsSending = false + val msg = IMLevelMessage(this, attachment?.raw?.convertFromJson()) + 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()) + } + + else -> null + } + } + + else -> null + } + + return res?.apply { + this.fetchType = fetchType + this.aiIsSending = aiIsSending + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/ChatActivity.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/ChatActivity.kt deleted file mode 100644 index a55f1b0..0000000 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/ChatActivity.kt +++ /dev/null @@ -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() { - - private val mViewModel by viewModels() - - 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 { - 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() - } - - } - - -} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatActivity.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatActivity.kt new file mode 100644 index 0000000..c34228b --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatActivity.kt @@ -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() { + + private val chatViewModel by viewModels() + 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 { + 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, doNotAskAgain: Boolean) { + this@ChatActivity.toast(R.string.no_permission) + if (doNotAskAgain) { + // 如果是被永久拒绝就跳转到应用权限系统设置页面 + XXPermissions.startPermissionActivity(this@ChatActivity, permissions) + } + } + + override fun onGranted(permissions: MutableList, 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() + } + + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/ChatEditView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatEditView.kt similarity index 93% rename from VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/ChatEditView.kt rename to VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatEditView.kt index ff1d44a..95e2bf8 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/ChatEditView.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatEditView.kt @@ -1,4 +1,4 @@ -package com.remax.visualnovel.ui.Chat +package com.remax.visualnovel.ui.chat import android.content.Context import android.util.AttributeSet diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatViewModel.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatViewModel.kt new file mode 100644 index 0000000..ef9eb50 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatViewModel.kt @@ -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>() + 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? = 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() + val sendScoreLiveData: LiveData = _sendScoreLiveData + + /** + * 接受方添加消息接收回调 + */ + private val messageListener = object : V2NIMMessageListener { + /** + * 消息接收 + */ + override fun onReceiveMessages(messages: List) { + NimManager.log("消息接收回调 onReceiveMessages") + NimManager.clearUnreadCountByIds(conversationId) + messages.firstOrNull()?.let { message -> + if (message.conversationId == conversationId) { + /** + * message中的分数 + * 本地实时加减一下 + * AI发送的文本消息和语音通话时,都会有serverExtension:{"score":0.1}的计算 + */ + message.serverExtension?.convertFromJson()?.let { + _sendScoreLiveData.value = it.score + RTCManager.sendIMScoreMessage(it.score) + } + + val messageRecFetchResult = FetchResult(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?) { + } + + override fun onReceiveTeamMessageReadReceipts(readReceipts: List?) { + } + + override fun onMessageRevokeNotifications(revokeNotifications: List?) { + } + + override fun onMessagePinNotification(pinNotification: V2NIMMessagePinNotification?) { + } + + override fun onMessageQuickCommentNotification(quickCommentNotification: V2NIMMessageQuickCommentNotification?) { + } + + override fun onMessageDeletedNotifications(messageDeletedNotifications: List?) { + } + + override fun onClearHistoryNotifications(clearHistoryNotifications: List?) { + } + + /** + * 本端发送消息状态回调 + */ + 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) { + NimManager.log("收到更新的消息 $messages") + messages.firstOrNull()?.let { message -> + NimManager.log("收到更新的消息 conversationId:${message.conversationId}") + if (message.conversationId == conversationId) { + val messageRecFetchResult = FetchResult(LoadStatus.Success) + messageRecFetchResult.data = message.convertMessage() + messageRecFetchResult.type = FetchResult.FetchType.Update + messageRecFetchResult.typeIndex = -1 + _sendMessageLiveData.value = messageRecFetchResult + } + } + } + } + + fun updateMsg(message: V2NIMMessage) { + val messageRecFetchResult = FetchResult(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?>() + val sendMessageLiveData: LiveData?> = _sendMessageLiveData + + private val sendMessageFetchResult by lazy { + FetchResult(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> { + 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 { + 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 { + return messageRepository.sendGift(request, gift) + } + + var refreshPrompts = true + private set + + suspend fun getPrompts(): Response> { + 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 { + 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) + } + } + } + +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/InputPanel.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/InputPanel.kt similarity index 78% rename from VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/InputPanel.kt rename to VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/InputPanel.kt index 093ce9e..c3c7624 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/InputPanel.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/InputPanel.kt @@ -1,7 +1,9 @@ -package com.remax.visualnovel.ui.Chat +package com.remax.visualnovel.ui.chat +import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet +import android.view.MotionEvent import android.widget.FrameLayout import android.widget.Toast import com.dylanc.viewbinding.nonreflection.inflate @@ -79,4 +81,22 @@ class InputPanel @JvmOverloads constructor(context: Context, attrs: AttributeSet } + @SuppressLint("ClickableViewAccessibility") + fun holdToTalk(callback: () -> Unit, cancelCallback: () -> Unit) { + binding.ivHold2talk.run { + setOnTouchListener { v, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + callback.invoke() + } + + MotionEvent.ACTION_UP -> { + cancelCallback.invoke() + } + } + true + } + } + } + } \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/PopMenuIconView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/PopMenuIconView.kt similarity index 99% rename from VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/PopMenuIconView.kt rename to VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/PopMenuIconView.kt index d0f6ed8..ef47ab4 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/Chat/PopMenuIconView.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/PopMenuIconView.kt @@ -1,4 +1,4 @@ -package com.remax.visualnovel.ui.Chat +package com.remax.visualnovel.ui.chat import android.animation.Animator diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/call/manager/BinaryMessageHandler.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/call/manager/BinaryMessageHandler.kt new file mode 100644 index 0000000..4a989e1 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/call/manager/BinaryMessageHandler.kt @@ -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?, +) + +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() + // 这里可以进一步处理 subtitles 列表,例如打印或存储 + subtitles?.data?.forEach { + RTCManager.log("Parsed subtitles: $it") + callback.invoke(it) + } + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/call/manager/RTCExt.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/call/manager/RTCExt.kt new file mode 100644 index 0000000..2feebf3 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/call/manager/RTCExt.kt @@ -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 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) + } + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/call/manager/RTCManager.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/call/manager/RTCManager.kt new file mode 100644 index 0000000..a926328 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/call/manager/RTCManager.kt @@ -0,0 +1,353 @@ +package com.remax.visualnovel.ui.chat.message.call.manager + + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.remax.visualnovel.BuildConfig +import com.remax.visualnovel.app.base.app.CommonApplicationProxy +import com.remax.visualnovel.entity.imbean.IMLevelMessage +import com.remax.visualnovel.entity.imbean.raw.CustomCallData +import com.remax.visualnovel.entity.imbean.raw.CustomRawData +import com.remax.visualnovel.entity.request.HeartbeatRelation +import com.remax.visualnovel.manager.nim.NimManager +import com.remax.visualnovel.utils.TimeUtils +import com.google.gson.Gson +import com.netease.nimlib.sdk.v2.message.V2NIMMessageCreator +import com.ss.bytertc.engine.RTCRoom +import com.ss.bytertc.engine.RTCRoomConfig +import com.ss.bytertc.engine.RTCVideo +import com.ss.bytertc.engine.UserInfo +import com.ss.bytertc.engine.data.AudioPropertiesConfig +import com.ss.bytertc.engine.data.LocalAudioPropertiesInfo +import com.ss.bytertc.engine.data.LocalAudioStreamError +import com.ss.bytertc.engine.data.LocalAudioStreamState +import com.ss.bytertc.engine.data.RemoteAudioPropertiesInfo +import com.ss.bytertc.engine.data.RemoteAudioState +import com.ss.bytertc.engine.data.RemoteAudioStateChangeReason +import com.ss.bytertc.engine.data.RemoteStreamKey +import com.ss.bytertc.engine.data.StreamSycnInfoConfig +import com.ss.bytertc.engine.handler.IRTCRoomEventHandler +import com.ss.bytertc.engine.handler.IRTCVideoEventHandler +import com.ss.bytertc.engine.type.AudioProfileType +import com.ss.bytertc.engine.type.AudioScenarioType +import com.ss.bytertc.engine.type.ChannelProfile +import com.ss.bytertc.engine.type.MediaStreamType +import com.ss.bytertc.engine.type.NetworkQualityStats +import com.ss.bytertc.engine.utils.LogUtil +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import java.nio.ByteBuffer + +/** + * Created by HJW on 2025/8/25 + */ +object RTCManager : DefaultLifecycleObserver { + + private var mRTCVideo: RTCVideo? = null + private var mRTCRoom: RTCRoom? = null + + override fun onDestroy(owner: LifecycleOwner) { + leaveRoom() + destroyEngine() + } + + fun log(content: String?) { + Timber.i("RTCManager $content") + } + + var onAudioPropertiesReport: ((isLocal: Boolean, linearVolume: Int) -> Unit)? = null + + private val mRTCVideoEventHandler = object : IRTCVideoEventHandler() { + override fun onLocalAudioPropertiesReport(audioPropertiesInfos: Array?) { + audioPropertiesInfos?.forEach { +// log("onLocalAudioPropertiesReport audioPropertiesInfos:${it.toString()}") + /** + * 线性音量,与原始音量呈线性关系,数值越大,音量越大。取值范围是:[0,255]。 + * + * [0, 25]: 近似无声 + * [26, 75]: 低音量 + * [76, 204]: 中音量 + * [205, 255]: 高音量 + */ + val linearVolume = it?.audioPropertiesInfo?.linearVolume ?: 0 + onAudioPropertiesReport?.invoke(true, linearVolume) + log(" 本地 在说话,音量是:$linearVolume") + } + } + + override fun onAudioPlaybackDeviceTestVolume(volume: Int) { + log("onAudioPlaybackDeviceTestVolume volume:$volume") + } + + override fun onRemoteAudioPropertiesReport( + audioPropertiesInfos: Array?, + totalRemoteVolume: Int + ) { + audioPropertiesInfos?.forEach { +// log("onRemoteAudioPropertiesReport audioPropertiesInfos:${it.toString()}") + /** + * 线性音量,与原始音量呈线性关系,数值越大,音量越大。取值范围是:[0,255]。 + * + * [0, 25]: 近似无声 + * [26, 75]: 低音量 + * [76, 204]: 中音量 + * [205, 255]: 高音量 + */ + val linearVolume = it?.audioPropertiesInfo?.linearVolume ?: 0 + onAudioPropertiesReport?.invoke(false, linearVolume) + log(" 远端 在说话,音量是:$linearVolume") + } + } + + override fun onUserStartAudioCapture(roomId: String?, uid: String?) { + log("onUserStartAudioCapture roomId:$roomId uid:$uid") + + } + + override fun onUserStopAudioCapture(roomId: String?, uid: String?) { + log("onUserStopAudioCapture roomId:$roomId uid:$uid") + } + + override fun onLocalAudioStateChanged(state: LocalAudioStreamState?, error: LocalAudioStreamError?) { + log("onLocalAudioStateChanged state:$state error:$error") + } + + override fun onRemoteAudioStateChanged( + key: RemoteStreamKey?, + state: RemoteAudioState?, + reason: RemoteAudioStateChangeReason? + ) { + log("onRemoteAudioStateChanged state:$state reason:$reason") + } + + + override fun onStreamSyncInfoReceived( + streamKey: RemoteStreamKey?, + streamType: StreamSycnInfoConfig.SyncInfoStreamType?, + data: ByteBuffer? + ) { +// log("onStreamSyncInfoReceived streamType:$streamType data:$data") + } + + override fun onWarning(warn: Int) { + log("onWarning:$warn") + } + + override fun onError(err: Int) { + log("onError:$err") + } + + override fun onLoggerMessage(level: LogUtil.LogLevel?, msg: String?, throwable: Throwable?) { + log("onLoggerMessage level:$level msg:$msg throwable:$throwable") + } + } + + var joinRoomCallback: (() -> Unit)? = null + + var binaryMessageReceived: ((SubtitleMsgData) -> Unit)? = null + + private val mRTCRoomEventHandler = object : IRTCRoomEventHandler() { + + override fun onRoomStateChanged(roomId: String?, uid: String?, state: Int, extraInfo: String?) { + log("onRoomStateChanged: roomId=$roomId, uid=$uid, state=$state, extra=$extraInfo") + if (state == 0) { + joinRoomCallback?.invoke() + } + } + + override fun onStreamPublishSuccess(uid: String?, isScreen: Boolean) { + log("onRoomStateChanged: uid=$uid, isScreen=$isScreen") + } + + override fun onStreamStateChanged(roomId: String?, uid: String?, state: Int, extraInfo: String?) { + log("onStreamStateChanged:roomId=$roomId, uid=$uid, state=$state, extraInfo=$extraInfo") + } + + override fun onRoomBinaryMessageReceived(uid: String, message: ByteBuffer?) { + log("onRoomBinaryMessageReceived :$message") + message?.let { + val bytes = ByteArray(message.remaining()) + message.get(bytes) + BinaryMessageHandler().unpack(bytes) { subtitleMsgData -> + binaryMessageReceived?.invoke(subtitleMsgData) + } + } + } + + override fun onNetworkQuality(localQuality: NetworkQualityStats, remoteQualities: Array) { +// log("onNetworkQuality: localQuality=$localQuality, remoteQualities=$remoteQualities") + } + + private fun notifyLocalRTTUpdated(rtt: Int) { + log("notifyLocalRTTUpdated: rtt=$rtt") + } + } + + private var userId = "" + + fun createEngine(roomId: String, userId: String, token: String) { + this.userId = userId + if (mRTCVideo != null) { + log("createRTCVideo: already created") + return + } else { + joinRoom(roomId, userId, token) + } + + val context = CommonApplicationProxy.application + val rtcVideo = RTCVideo.createRTCVideo(context, BuildConfig.RTC_APP_ID, mRTCVideoEventHandler, null, null) + + val config = AudioPropertiesConfig(300) + with(rtcVideo) { + // 开启发言者音量监听 + enableAudioPropertiesReport(config) + setAudioScenario(AudioScenarioType.AUDIO_SCENARIO_COMMUNICATION) + setAudioProfile(AudioProfileType.AUDIO_PROFILE_DEFAULT) + +// // 获取 AudioManager 实例 +// val audioManager = +// CommonApplicationProxy.application.getSystemService(Context.AUDIO_SERVICE) as AudioManager +// +// // 获取当前媒体音量 +// val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) +// +// // 可选:获取最大媒体音量 +// val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) +// +// Timber.d("audioManager currentVolume:$currentVolume maxVolume:$maxVolume") +// +// setPlaybackVolume(1) + } + + mRTCVideo = rtcVideo + if (mRTCVideo != null) { + joinRoom(roomId, userId, token) + } + } + + fun destroyEngine() { + log("destroyEngine") + onAudioPropertiesReport = null + joinRoomCallback = null + binaryMessageReceived = null + leaveRoom() + if (mRTCVideo != null) { + RTCVideo.destroyRTCVideo() + mRTCVideo = null + } + } + + fun joinRoom(roomId: String, userId: String, token: String) { + log("joinRoom: roomId=$roomId, userId=$userId, token=$token") + + leaveRoom() + if (mRTCVideo == null) { + return + } + mRTCRoom = mRTCVideo?.createRTCRoom(roomId) + mRTCRoom?.setRTCRoomEventHandler(mRTCRoomEventHandler) + val userInfo = UserInfo(userId, null) + val roomConfig = RTCRoomConfig( + ChannelProfile.CHANNEL_PROFILE_CHAT, + true, + true, + false + ) + mRTCRoom?.setUserVisibility(false) + val result = mRTCRoom?.joinRoom(token, userInfo, roomConfig) + startInteract() + log("joinRoom: result=$result") + } + + fun leaveRoom() { + log("leaveRoom") + if (mRTCRoom != null) { + mRTCRoom?.leaveRoom() + mRTCRoom?.destroy() + } + } + + fun startInteract() { + mRTCRoom?.setUserVisibility(true) + } + + fun stopInteract() { + val userVisibility = mRTCRoom?.setUserVisibility(false) + } + + /** + * 本地音频采集 + * 开关推流 + */ + fun startAudioCapture(start: Boolean) { + if (mRTCVideo == null) { + return + } + if (start) { + val publishRes = mRTCRoom?.publishStream(MediaStreamType.RTC_MEDIA_STREAM_TYPE_AUDIO) + log("publishRes:$publishRes") + val startRes = mRTCVideo?.startAudioCapture() + log("startAudioCapture start:$startRes") + } else { + val stopRes = mRTCVideo?.stopAudioCapture() + log("startAudioCapture stop:$stopRes") + val unpublishRes = mRTCRoom?.unpublishStream(MediaStreamType.RTC_MEDIA_STREAM_TYPE_AUDIO) + log("unpublishRes:$unpublishRes") + } + } + + fun sendEndMsg(accountId: String, durationTime: Long) { + val callType = + if (durationTime < TimeUtils.ONE_SECOND) CustomCallData.CALL_CANCEL else CustomCallData.CALL_END + val raw = CustomCallData( + CustomRawData.CALL, + CustomCallData.CALL_CANCEL, durationTime + ) +// WalletManager.refreshWallet() + val v2Message = V2NIMMessageCreator.createCustomMessage(raw.callTxt, Gson().toJson(raw)) + NimManager.v2SendMessage(v2Message, accountId) + } + + /** + * 心动等级变化 + */ + private val _levelFlow = MutableSharedFlow() + val levelFlow = _levelFlow.asSharedFlow() + + fun sendIMLevelMessage(message: IMLevelMessage?) { + MainScope().launch { _levelFlow.emit(message) } + } + + /** + * 语音通话中分数变化 + */ + private val _scoreFlow = MutableSharedFlow() + val scoreFlow = _scoreFlow.asSharedFlow() + + fun sendIMScoreMessage(score: Double) { + MainScope().launch { _scoreFlow.emit(score) } + } + + /** + * 更新心动关系 + */ + private val _relationLiveData = MutableSharedFlow() + val relationLiveData = _relationLiveData.asSharedFlow() + + fun setRelation(relation: HeartbeatRelation?) { + MainScope().launch { _relationLiveData.emit(relation) } + } + + /** + * 余额不足,关闭语音通话 + */ + private val _closeRCTFlow = MutableSharedFlow() + val closeRCTFlow = _closeRCTFlow.asSharedFlow() + + fun balanceInsufficient() { + MainScope().launch { _closeRCTFlow.emit(true) } + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/detail/flirting/FlirtingLevelActivity.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/detail/flirting/FlirtingLevelActivity.kt new file mode 100644 index 0000000..bbeb505 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/detail/flirting/FlirtingLevelActivity.kt @@ -0,0 +1,248 @@ +package com.remax.visualnovel.ui.chat.message.detail.flirting + +/** + * Created by HJW on 2025/8/16 + */ +import android.annotation.SuppressLint +import android.view.View +import androidx.activity.viewModels +import androidx.core.view.isGone +import androidx.core.view.isVisible +import com.airbnb.lottie.LottieAnimationView +import com.alibaba.android.arouter.facade.annotation.Autowired +import com.alibaba.android.arouter.facade.annotation.Route +import com.alibaba.android.arouter.launcher.ARouter +import com.remax.visualnovel.R +import com.remax.visualnovel.app.base.BaseBindingActivity +import com.remax.visualnovel.app.delegate.isFull +import com.remax.visualnovel.app.delegate.titleTextAlpha +import com.remax.visualnovel.app.widget.tips.TipsSwitchWindow +import com.remax.visualnovel.entity.request.AIIsShowDTO +import com.remax.visualnovel.entity.response.HeartbeatLevel +import com.remax.visualnovel.extension.addScrollerAlpha +import com.remax.visualnovel.extension.getTemperatureTxt +import com.remax.visualnovel.extension.glide.loadAndRoundCorner +import com.remax.visualnovel.extension.launchAndLoadingCollect +import com.remax.visualnovel.extension.setMargin +import com.remax.visualnovel.extension.setOnClick +import com.remax.visualnovel.extension.setSize +import com.remax.visualnovel.extension.setSpanTypeFace +import com.remax.visualnovel.extension.showMoreTxtDialog +import com.remax.visualnovel.extension.translationYObjectAnimator +import com.remax.visualnovel.utils.Routers +import com.remax.visualnovel.utils.spannablex.spannable +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.ui.UserAvatarView +import com.remax.visualnovel.widget.uitoken.changeTextFont +import com.remax.visualnovel.widget.uitoken.handleUIToken +import com.drake.brv.annotaion.DividerOrientation +import com.drake.brv.utils.divider +import com.drake.brv.utils.grid +import com.drake.brv.utils.models +import com.drake.brv.utils.setup +import com.remax.visualnovel.databinding.ActivityFlirtingLevelBinding +import com.remax.visualnovel.databinding.ItemFlirtingLevelBinding +import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber + +@AndroidEntryPoint +@Route(path = Routers.CHAT_FLIRTING_LEVEL) +class FlirtingLevelActivity : BaseBindingActivity() { + + private val flirtingLevelViewModel by viewModels() + + @JvmField + @Autowired + var aiId = "" + + @SuppressLint("SetTextI18n") + override fun initView() { + ARouter.getInstance().inject(this) + setToolbar(R.string.app_name) { + isFull = true + titleTextAlpha = 1f + setRightIconBtn1(R.string.icon_more) { + val switchWindow = TipsSwitchWindow() + switchWindow.build( + this@FlirtingLevelActivity, + R.string.hide_relations, + flirtingLevelViewModel.levelOutput?.aiUserHeartbeatRelation?.isShow != true + ) { switchView, isChecked -> + launchAndLoadingCollect({ + flirtingLevelViewModel.relationSwitch(AIIsShowDTO(aiId, if (isChecked) 0 else 1)) + }) { + onSuccess = { + with(binding) { + listOf(tvMeet, dividerMeet1, dividerMeet2).forEach { view -> + view.isGone = + flirtingLevelViewModel.levelOutput?.aiUserHeartbeatRelation?.isShow != true + } + } + } + + onFailed = { _, _ -> + switchView.isChecked = !isChecked + } + } + } + switchWindow.showAsDropDown(this) + } + + setRightIconBtn2(R.string.icon_faq) { + showMoreTxtDialog( + texts = listOf( + R.string.flirting_tips_txt_1, + R.string.flirting_tips_txt_2, + R.string.flirting_tips_txt_3 + ).map { getString(it) }) + } + } + with(binding) { + scrollView.addScrollerAlpha(aiAvatarView, includeBgAlpha = true, textAlpha = false) + + tvMeet.text = "· ${getString(R.string.meet)} ·" + + setOnClick(retrieveGroup) { + flirtingLevelViewModel.levelOutput?.aiUserHeartbeatRelation?.let { + val retrieveDialog = FlirtingRetrieveDialog(this@FlirtingLevelActivity) + retrieveDialog.build(it) { + launchAndLoadingCollect({ + flirtingLevelViewModel.buyHeartbeatVal(aiId) + }) { + onSuccess = { + initData() + retrieveDialog.dismiss() + } + } + } + retrieveDialog.binding.run { + showHeartAnim(levelBg, levelBgTop, lottieView, 84f) + } + retrieveDialog.show() + } + } + + rv.grid(2) + .divider { + setDivider(16, true) + orientation = DividerOrientation.VERTICAL + }.setup { + addType(R.layout.item_flirting_level) + onBind { + val item = getModel() + with(getBinding()) { + tvName.text = item.name + tvName.isEnabled = item.isUnlock + lockView.isVisible = !item.isUnlock + imageView.loadAndRoundCorner(item.imgUrl, 16) + tvTemperature.text = item.startVal.getTemperatureTxt() + tvTemperature.isEnabled = item.isUnlock + imageStroke.isVisible = item.isUnlock + } + } + } + } + } + + @SuppressLint("SetTextI18n", "DefaultLocale") + override fun initData() { + launchAndLoadingCollect({ + flirtingLevelViewModel.getHeartbeatLevel(aiId) + }) { + onSuccess = { + val relation = it?.aiUserHeartbeatRelation + with(binding) { + listOf(tvMeet, dividerMeet1, dividerMeet2).forEach { view -> + view.isGone = relation?.isShow != true + } + + showHeartAnim(levelBg, levelBgTop, lottieView, avatarView = aiAvatarView) + + aiAvatarView.loadAvatar(relation?.aiHeadImg) + myAvatarView.loadAvatar(relation?.userHeadImg) + val currHeartbeat = relation?.currHeartbeatEnum + if (currHeartbeat == null) { + tvMeet.changeTextFont { + textUITextToken = getString(R.string.txt_title_s) + } + tvMeet.setText(R.string.no_intention_yet) + } else { + tvMeet.changeTextFont { + textUITextToken = getString(R.string.txt_display_s) + } + tvMeet.setText(currHeartbeat.tagName) + } + val heartbeatScore = String.format("%.2f", (relation?.heartbeatScore ?: 0.0f) * 100) + + tvMeetDesc.text = + getString(R.string.flirting_desc, relation?.dayCount ?: 0, heartbeatScore) + tvLevel.text = spannable { + if (currHeartbeat != null) { + currHeartbeat.levelContent.text() + " 丨 ".color(handleUIToken(R.string.color_outline_normal)?.color ?: 0) + } + (relation?.heartbeatVal ?: 0.0).toString().text() + getString(R.string.temperature).span { + setSpanTypeFace(this@FlirtingLevelActivity, R.string.txt_numMonotype_s) + } + } + + /** + * 已经扣减的心动分 + */ + retrieveGroup.isVisible = (relation?.subtractHeartbeatVal ?: 0.0) != 0.0 + retrieveTitle.text = + getString(R.string.flirting_deducted_desc, (relation?.subtractHeartbeatVal ?: 0.0).toString()) + + rv.models = it?.heartbeatLeveLDictList + } + } + } + } + + private fun showHeartAnim( + levelBg: View, + levelBgTop: View, + heartLottie: LottieAnimationView, + bgBottomMargin: Float = 0f, + avatarView: UserAvatarView? = null, + ) { + levelBg.post { + val currProgress = + flirtingLevelViewModel.levelOutput?.aiUserHeartbeatRelation?.currHeartbeatEnum?.level ?: 0 + val progress = currProgress / 10f + + // 弹窗中的切图需要往上移动 + levelBg.setMargin(bottomMargin = -((bgBottomMargin / 400f) * levelBgTop.measuredHeight).toInt()) + + // 根据切图比例调整宽高 + val heartSize = ((98f / 214f) * levelBgTop.measuredHeight).toInt() + heartLottie.setSize(heartSize, heartSize) + + // 头像框距离顶部的边距 + val marginTop = ((120f / 214f) * levelBgTop.measuredHeight) + (heartSize - 64.dp) / 2f + avatarView?.setMargin(topMargin = marginTop.toInt()) + avatarView?.isVisible = true + binding.myAvatarView.isVisible = true + + // 动画持续时间 + val totalTime = 1000L + // 总高度进度算 + val totalHeight = progress * heartSize + Timber.i("showHeartAnim heartSize:$heartSize totalHeight:$totalHeight") + heartLottie.translationYObjectAnimator(-totalHeight, totalTime) + } + } + + companion object { + fun start(aiId: String?) { + if (aiId != null) { + ARouter.getInstance() + .build(Routers.CHAT_FLIRTING_LEVEL) + .withString("aiId", aiId) + .navigation() + } + } + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/detail/flirting/FlirtingLevelViewModel.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/detail/flirting/FlirtingLevelViewModel.kt new file mode 100644 index 0000000..24a0bde --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/detail/flirting/FlirtingLevelViewModel.kt @@ -0,0 +1,32 @@ +package com.remax.visualnovel.ui.chat.message.detail.flirting + +/** + * Created by HJW on 2025/8/16 + */ +import com.remax.visualnovel.app.viewmodel.base.BaseViewModel +import com.remax.visualnovel.entity.request.AIIsShowDTO +import com.remax.visualnovel.entity.request.HeartbeatBuy +import com.remax.visualnovel.entity.response.HeartbeatLevelOutput +import com.remax.visualnovel.repository.api.ChatRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class FlirtingLevelViewModel @Inject constructor(private val chatRepository: ChatRepository) : BaseViewModel() { + + var levelOutput: HeartbeatLevelOutput? = null + private set + + suspend fun getHeartbeatLevel(aiId: String) = chatRepository.getHeartbeatLevel(aiId).transformResult({ + levelOutput = it + }) + + suspend fun relationSwitch(request: AIIsShowDTO) = chatRepository.relationSwitch(request).transformResult({ + levelOutput?.aiUserHeartbeatRelation?.isShow = request.isShow == 1 + }) + + suspend fun buyHeartbeatVal(aiId: String) = chatRepository.buyHeartbeatVal(HeartbeatBuy( + aiId, + levelOutput?.aiUserHeartbeatRelation?.subtractHeartbeatVal + )) +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/detail/flirting/FlirtingRetrieveDialog.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/detail/flirting/FlirtingRetrieveDialog.kt new file mode 100644 index 0000000..b14aa75 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/detail/flirting/FlirtingRetrieveDialog.kt @@ -0,0 +1,42 @@ +package com.remax.visualnovel.ui.chat.message.detail.flirting + +import android.content.Context +import com.remax.visualnovel.R +import com.remax.visualnovel.databinding.DialogFlirtingRetrieveBinding +import com.remax.visualnovel.entity.request.HeartbeatRelation +import com.remax.visualnovel.extension.formatPrice +import com.remax.visualnovel.extension.getTemperatureTxt +import com.remax.visualnovel.extension.setOnClick +import com.remax.visualnovel.ui.wallet.manager.WalletManager +import com.remax.visualnovel.widget.dialoglib.LBindingDialog + +/** + * Created by HJW on 2025/8/17 + */ + +class FlirtingRetrieveDialog(context: Context) : + LBindingDialog(context, DialogFlirtingRetrieveBinding::inflate) { + + fun build(relation: HeartbeatRelation, buyCallback: () -> Unit): FlirtingRetrieveDialog { + with() + setBottom() + setBgColorToken(R.string.color_transparent) + setCancelBtn(R.id.cancel) + with(binding) { + quantity.text = relation.subtractHeartbeatVal?.getTemperatureTxt() + priceView.setPrice(formatPrice(relation.price)) + val total = maxOf(1.0, (relation.subtractHeartbeatVal ?: 0.0) * (relation.price ?: 0L)) + relationPrice.setPrice(formatPrice(total.toLong())) + + setOnClick(purchaseBtn) { + if (WalletManager.balance < total) { + WalletManager.showChargeDialog() + } else { + buyCallback.invoke() + } + } + } + return this + } + +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/ChatSettingEvents.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/ChatSettingEvents.kt new file mode 100644 index 0000000..a266ea8 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/ChatSettingEvents.kt @@ -0,0 +1,24 @@ +package com.remax.visualnovel.ui.chat.message.events + +import com.remax.visualnovel.entity.request.AIFeedback +import com.remax.visualnovel.entity.request.AIIDRequest +import com.pengxr.modular.eventbus.facade.annotation.EventGroup + + +/** + * Created by HJW on 2023/5/18 + * 用户修改了AI相关的设置 + */ +@EventGroup(moduleName = "chatSetting", autoClear = true) +interface ChatSettingEvents { + + /** + * 修改设置 + */ + fun settingChanged(): AIIDRequest + + /** + * 修改点赞/点踩 + */ + fun aiResponseFeedback(): AIFeedback +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/model/ChatSetAutoPlayEvent.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/model/ChatSetAutoPlayEvent.kt new file mode 100644 index 0000000..e434f5d --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/model/ChatSetAutoPlayEvent.kt @@ -0,0 +1,14 @@ +package com.remax.visualnovel.ui.chat.message.events.model + +import com.remax.visualnovel.entity.request.AIIDRequest + +/** + * Created by HJW on 2025/8/24 + */ +data class ChatSetAutoPlayEvent( + override val aiId: String, + /** + * 自动播放语音开关 1:开 0:关 + */ + val isAutoPlayVoice: Int? = null, +) : AIIDRequest(aiId) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/model/ChatSetBackgroundEvent.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/model/ChatSetBackgroundEvent.kt new file mode 100644 index 0000000..f4612f4 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/model/ChatSetBackgroundEvent.kt @@ -0,0 +1,14 @@ +package com.remax.visualnovel.ui.chat.message.events.model + +import com.remax.visualnovel.entity.request.AIIDRequest + +/** + * Created by HJW on 2025/8/24 + */ +data class ChatSetBackgroundEvent( + override val aiId: String, + /** + * 聊天背景图片 + */ + val backgroundImg: String? = null, +): AIIDRequest(aiId) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/model/ChatSetBubbleEvent.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/model/ChatSetBubbleEvent.kt new file mode 100644 index 0000000..add5cb3 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/message/events/model/ChatSetBubbleEvent.kt @@ -0,0 +1,16 @@ +package com.remax.visualnovel.ui.chat.message.events.model + +import com.remax.visualnovel.entity.request.AIIDRequest +import com.remax.visualnovel.entity.response.ChatBubble + +/** + * Created by HJW on 2025/8/24 + */ +data class ChatSetBubbleEvent( + override val aiId: String, + + /** + * 聊天气泡 + */ + val bubble: ChatBubble? = null, +): AIIDRequest(aiId) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/model/ChatModelDialog.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/model/ChatModelDialog.kt new file mode 100644 index 0000000..053b046 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/model/ChatModelDialog.kt @@ -0,0 +1,115 @@ +package com.remax.visualnovel.ui.chat.setting.model + +import android.app.Activity +import androidx.core.view.isVisible +import com.remax.visualnovel.R +import com.remax.visualnovel.databinding.DialogChatModelBinding +import com.remax.visualnovel.databinding.ItemChatModelBinding +import com.remax.visualnovel.entity.response.ChatModel +import com.remax.visualnovel.extension.formatPrice +import com.remax.visualnovel.extension.setOnClick +import com.remax.visualnovel.extension.showMoreTxtDialog +import com.remax.visualnovel.ui.wallet.WalletActivity +import com.remax.visualnovel.ui.wallet.manager.WalletManager +import com.remax.visualnovel.widget.dialoglib.LBindingDialog +import com.drake.brv.utils.divider +import com.drake.brv.utils.linear +import com.drake.brv.utils.setup + +/** + * Created by HJW on 2025/8/18 + */ +class ChatModelDialog(private val activity: Activity) : + LBindingDialog(activity, DialogChatModelBinding::inflate) { + + private var selectCode: String? = null + + fun build( + models: List, + selectCode: String?, + recharge: (() -> Unit)? = null, + click: (ChatModel) -> Unit + ) { + this.selectCode = selectCode + with() + setBottom() + setCancelBtn(R.id.cancel) + with(binding) { + dialogTitle.setText(if (recharge == null) R.string.dialog_model else R.string.recharge_dialog_title) + modelHint.isVisible = recharge == null + listOf(btnRecharge, dialogText, priceView).forEach { + it.isVisible = !modelHint.isVisible + } + + setOnClick(btnRecharge) { + WalletActivity.start() + dismiss() + } + priceView.setPrice(formatPrice(WalletManager.balance)) + + rvModel.linear() + .divider { + setDivider(16, true) + }.setup { + addType(R.layout.item_chat_model) + onClick(R.id.group) { + val item = getModel() + click.invoke(item) + } + onBind { + val item = getModel() + with(getBinding()) { + modelSelect.isVisible = item.code == selectCode + modelTitle.text = item.name + modelDesc.text = item.description + priceView1.setPrice("${formatPrice(item.textPrice)}/${context.getString(R.string.text_message)}") + priceView2.setPrice("${formatPrice(item.voicePrice)}/${context.getString(R.string.send_or_play_voice)}") + priceView3.setPrice("${formatPrice(item.voiceChatPrice)}/${context.getString(R.string.voice_call_message)}") + + setOnClick(priceTips) { + activity.showMoreTxtDialog( + texts = listOf( + item.questionMark ?: "" + ) + ) + } + } + } + }.models = models + } + } + + +// inner class ChatModelAdapter(private val activity: Activity, data: List) : +// BannerAdapter(data) { +// +// inner class ViewHolder(view: View, itemBinding: ItemChatModelBinding) : RecyclerView.ViewHolder(view) { +// val binding = itemBinding +// } +// +// override fun onCreateHolder(parent: ViewGroup, viewType: Int): ViewHolder { +// val binding = ItemChatModelBinding.inflate(LayoutInflater.from(parent.context), parent, false) +// return ViewHolder(binding.root, binding) +// } +// +// override fun onBindView(holder: ViewHolder?, data: ChatModel?, position: Int, size: Int) { +// data?.let { item -> +// holder?.binding?.run { +// modelSelect.isVisible = item.code == selectCode +// modelTitle.text = item.name +// modelDesc.text = item.description +// priceView1.setPrice("${item.textPrice}/${context.getString(R.string.text_message)}") +// priceView2.setPrice("${item.voiceChatPrice}/${context.getString(R.string.voice_call_message)}") +// +// setOnClick(priceTips) { +// activity.showMoreTxtDialog( +// texts = listOf( +// item.questionMark ?: "" +// ) +// ) +// } +// } +// } +// } +// } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/HoldToTalkDialog.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/HoldToTalkDialog.kt new file mode 100644 index 0000000..788d5c4 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/HoldToTalkDialog.kt @@ -0,0 +1,25 @@ +package com.remax.visualnovel.ui.chat.ui + +import android.content.Context +import com.remax.visualnovel.R +import com.remax.visualnovel.databinding.DialogHoldToTalkBinding +import com.remax.visualnovel.widget.dialoglib.LBindingDialog + +/** + * Created by HJW on 2025/8/16 + */ +class HoldToTalkDialog(context: Context) : + LBindingDialog(context, DialogHoldToTalkBinding::inflate) { + + fun build(): HoldToTalkDialog { + with() + setBottom() + setBgColorToken(R.string.color_transparent) + setMaskValue(0f) + + show() + + return this + } + +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorsAdapter.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorsAdapter.kt index 53583d8..a947006 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorsAdapter.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorsAdapter.kt @@ -5,7 +5,7 @@ import com.chad.library.adapter.base.module.LoadMoreModule import com.remax.visualnovel.R import com.remax.visualnovel.app.BaseBindingQuickAdapter import com.remax.visualnovel.databinding.FragmentMainActorItemBinding -import com.remax.visualnovel.ui.Chat.ChatActivity +import com.remax.visualnovel.ui.chat.ChatActivity class ActorsAdapter : BaseBindingQuickAdapter(FragmentMainActorItemBinding::inflate), LoadMoreModule { init { diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/WalletActivity.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/WalletActivity.kt new file mode 100644 index 0000000..f81df84 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/WalletActivity.kt @@ -0,0 +1,107 @@ +package com.remax.visualnovel.ui.wallet + +/** + * Created by HJW on 2025/8/26 + */ + +import android.annotation.SuppressLint +import androidx.activity.viewModels +import androidx.core.view.isVisible +import com.alibaba.android.arouter.facade.annotation.Route +import com.alibaba.android.arouter.launcher.ARouter +import com.remax.visualnovel.R +import com.remax.visualnovel.app.base.BaseBindingActivity +import com.remax.visualnovel.databinding.ActivityWalletBinding +import com.remax.visualnovel.extension.addScrollerAlpha +import com.remax.visualnovel.extension.launchAndLoadingCollect +import com.remax.visualnovel.extension.setMagicIndicator +import com.remax.visualnovel.extension.setOnClick +import com.remax.visualnovel.ui.wallet.income.IncomeFragment +import com.remax.visualnovel.ui.wallet.manager.WalletManager +import com.remax.visualnovel.ui.wallet.recharge.RechargeFragment +import com.remax.visualnovel.utils.Routers +import com.remax.visualnovel.utils.spannablex.activateClick +import com.remax.visualnovel.utils.spannablex.span.SimpleClickableConfig +import com.remax.visualnovel.utils.spannablex.spannable +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.indicator.SecondaryNavigatorAdapter +import com.remax.visualnovel.widget.uitoken.expend.dsl.expandDp +import com.remax.visualnovel.widget.uitoken.handleUIToken +import dagger.hilt.android.AndroidEntryPoint +import kotlin.getValue + +@AndroidEntryPoint +@Route(path = Routers.WALLET) +class WalletActivity : BaseBindingActivity() { + + private val walletViewModel by viewModels() + + override fun initView() { + setToolbar(R.string.wallet) + with(binding) { + appBarLayout.addScrollerAlpha(tvTitle, isFromToolbar = true) + + setMagicIndicator( + listOf(RechargeFragment.newInstance(), IncomeFragment.newInstance()), + SecondaryNavigatorAdapter( + listOf( + R.string.recharge, + R.string.income, + ).map { res -> getString(res) }, viewPager2 = viewPager + ), + indicator, leftPadding = 24.dp + ) { + bottomLayout.isVisible = it == 0 + } + + agreement.activateClick(false).text = spannable { + getString(R.string.recharge_agreement_hint).text() + + val color = handleUIToken(R.string.color_primary_variant_normal)?.color ?: 0 + " ${getString(R.string.recharge_agreement)}".span { + clickable(color, config = SimpleClickableConfig(false)) { _, _ -> + Routers.navigationToRA() + } + } + } + + radioRecharge.expandDp(30, 30) + setOnClick(radioRecharge, btnRecharge) { + when (this) { + radioRecharge -> { + radioRecharge.viewChecked(!radioRecharge.isChecked) + btnRecharge.isEnabled = radioRecharge.isChecked + } + + btnRecharge -> { + walletViewModel.products?.find { it.selected }?.let { + WalletManager.onCreateOrder(it) + } + } + } + } + } + } + + override fun initData() { + launchAndLoadingCollect({ + walletViewModel.getMyWallet() + }) + } + + @SuppressLint("SetTextI18n") + fun refreshRechargeTxt() { + walletViewModel.products?.find { it.selected }?.let { item -> + binding.btnRecharge.text = "${getString(R.string.recharge)} ${item.local}" + } + } + + companion object { + fun start() { + ARouter.getInstance() + .build(Routers.WALLET) + .navigation() + } + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/WalletViewModel.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/WalletViewModel.kt new file mode 100644 index 0000000..cf427f3 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/WalletViewModel.kt @@ -0,0 +1,44 @@ +package com.remax.visualnovel.ui.wallet + +/** + * Created by HJW on 2025/8/26 + */ + +import com.remax.visualnovel.app.viewmodel.base.BaseViewModel +import com.remax.visualnovel.entity.request.ChargeProduct +import com.remax.visualnovel.entity.request.PageQuery +import com.remax.visualnovel.entity.request.SearchPage +import com.remax.visualnovel.entity.response.Transaction +import com.remax.visualnovel.entity.response.base.Response +import com.remax.visualnovel.repository.api.PayRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class WalletViewModel @Inject constructor(private val payRepository: PayRepository) : BaseViewModel() { + + private var currPn = 1 + + suspend fun getBillList(isRefresh: Boolean, type: String?): Response { + if (isRefresh) currPn = 1 + return payRepository.getTransactionList( + SearchPage( + PageQuery.Page(currPn), type = type + ) + ).transformResult({ + currPn++ + }) + } + + + suspend fun getMyWallet() = payRepository.getMyWallet() + + var products: List? = null + private set + + suspend fun getProducts() = payRepository.getProducts().transformResult({ + products = it?.productList + products?.firstOrNull()?.selected = true + }) + +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/dialog/RechargeDialog.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/dialog/RechargeDialog.kt new file mode 100644 index 0000000..1c8b6f3 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/dialog/RechargeDialog.kt @@ -0,0 +1,83 @@ +package com.remax.visualnovel.ui.wallet.dialog + +import android.annotation.SuppressLint +import android.app.Activity +import androidx.recyclerview.widget.RecyclerView +import com.remax.visualnovel.R +import com.remax.visualnovel.databinding.DialogRechargeBinding +import com.remax.visualnovel.databinding.ItemRechargeBinding +import com.remax.visualnovel.entity.request.ChargeProduct +import com.remax.visualnovel.extension.formatPrice +import com.remax.visualnovel.extension.setOnClick +import com.remax.visualnovel.ui.wallet.manager.WalletManager +import com.remax.visualnovel.widget.dialoglib.LBindingDialog +import com.remax.visualnovel.widget.uitoken.changeBackground +import com.drake.brv.annotaion.DividerOrientation +import com.drake.brv.utils.divider +import com.drake.brv.utils.grid +import com.drake.brv.utils.setup + +/** + * Created by HJW on 2025/9/15 + */ +class RechargeDialog(val activity: Activity) : + LBindingDialog(activity, DialogRechargeBinding::inflate) { + + @SuppressLint("SetTextI18n") + fun build(productList: List) { + with() + setBottom() + setCancelBtn(R.id.cancelBtn) + with(binding) { + balance.setPrice(formatPrice(WalletManager.balance)) + rvRecharge.setRechargeProduct(productList) { item -> + btnRecharge.text = "${activity.getString(R.string.recharge)} ${item.local}" + } + setOnClick(btnRecharge) { + productList.find { it.selected }?.let { product -> + WalletManager.onCreateOrder(product) + dismiss() + } + } + } + show() + } +} + +@SuppressLint("NotifyDataSetChanged") +fun RecyclerView.setRechargeProduct(productList: List?, selectCallback: (ChargeProduct) -> Unit) { + grid(2) + .divider { + setDivider(16, true) + orientation = DividerOrientation.VERTICAL + }.setup { + addType(R.layout.item_recharge) + onClick(R.id.group) { + val item = getModel() + models?.forEach { product -> + (product as? ChargeProduct)?.apply { + selected = item == product + if (selected) { + selectCallback.invoke(item) + } + } + } + bindingAdapter?.notifyDataSetChanged() + } + onBind { + val item = getModel() + with(getBinding()) { + group.changeBackground { + strokeUIColorToken = context.getString( + if (item.selected) + R.string.color_primary_normal + else + R.string.color_surface_base_normal + ) + } + priceView.setPrice(item.chargeAmount.toString()) + amountView.text = item.local + } + } + }.models = productList +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/income/IncomeFragment.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/income/IncomeFragment.kt new file mode 100644 index 0000000..75cc208 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/income/IncomeFragment.kt @@ -0,0 +1,113 @@ +package com.remax.visualnovel.ui.wallet.income + +/** + * Created by HJW on 2025/9/12 + */ +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.alibaba.android.arouter.facade.annotation.Route +import com.alibaba.android.arouter.launcher.ARouter +import com.remax.visualnovel.R +import com.remax.visualnovel.app.base.BaseBindingFragment +import com.remax.visualnovel.databinding.ItemIncomeHeaderBinding +import com.remax.visualnovel.databinding.LayoutSimpleRecyclerviewRefreshBinding +import com.remax.visualnovel.entity.response.AccountBuffBill +import com.remax.visualnovel.extension.autoRefreshList +import com.remax.visualnovel.extension.formatPrice +import com.remax.visualnovel.extension.launchAndCollect +import com.remax.visualnovel.extension.setOnClick +import com.remax.visualnovel.extension.showSingleBtnDialog +import com.remax.visualnovel.ui.wallet.WalletViewModel +import com.remax.visualnovel.ui.wallet.transaction.TransactionAdapter +import com.remax.visualnovel.ui.wallet.transaction.TransactionDetailActivity +import com.remax.visualnovel.utils.Routers +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.itemdecoration.SpaceItemDecoration +import com.pengxr.modular.eventbus.generated.events.EventDefineOfWalletEvents +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +@Route(path = Routers.INCOME) +class IncomeFragment : BaseBindingFragment() { + + private val walletViewModel by activityViewModels() + + private val transactionAdapter by lazy { + TransactionAdapter(AccountBuffBill.INCOME) + } + + private val incomeHeaderBinding by lazy { + ItemIncomeHeaderBinding.inflate(layoutInflater) + } + + @SuppressLint("SetTextI18n") + override fun onCreated(bundle: Bundle?) { + with(binding) { + setOnClick(incomeHeaderBinding.transactionEnter, incomeHeaderBinding.pendingTips) { + when (this) { + incomeHeaderBinding.transactionEnter -> { + TransactionDetailActivity.start() + } + + incomeHeaderBinding.pendingTips -> { + activity?.showSingleBtnDialog(text = getString(R.string.transaction_tips)) + } + } + } + + with(refreshLayout) { + refreshLayout.autoRefreshList() + onRefresh { + getData(true) + } + } + with(recyclerView) { + adapter = transactionAdapter + layoutManager = LinearLayoutManager(recyclerView.context) + addItemDecoration(SpaceItemDecoration(16.dp, 24.dp,0,24.dp)) + with(transactionAdapter) { + addHeaderView(incomeHeaderBinding.root) + incomeHeaderBinding.transactionEnter.text = "${getString(R.string.transaction_detail)}>" + headerWithEmptyEnable = true + loadMoreModule.setOnLoadMoreListener { + getData(false) + } + } + } + } + } + + override fun subscribeUi() { + EventDefineOfWalletEvents.buffBalanceUpdateSucceeded().observe(this) { + incomeHeaderBinding.income.setPrice(formatPrice(it?.income)) + incomeHeaderBinding.pending.setPrice(formatPrice(it?.awaitingIncome)) + } + } + + private fun getData(isRefresh: Boolean) { + launchAndCollect({ + walletViewModel.getBillList(isRefresh, AccountBuffBill.INCOME) + }) { + onSuccess = { + val data = it?.pageList?.datas ?: emptyList() + transactionAdapter.setTransactionData(isRefresh, data, 32) + } + + onComplete = { + if (isRefresh) { + binding.refreshLayout.finishRefresh() + } + } + } + } + + companion object { + fun newInstance(): IncomeFragment { + return ARouter.getInstance().build(Routers.INCOME) + .navigation() as IncomeFragment + } + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/manager/WalletManager.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/manager/WalletManager.kt new file mode 100644 index 0000000..8b4fa8e --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/manager/WalletManager.kt @@ -0,0 +1,116 @@ +package com.remax.visualnovel.ui.wallet.manager + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer +import com.remax.visualnovel.R +import com.remax.visualnovel.api.factory.ServiceFactory +import com.remax.visualnovel.app.base.BaseBindingActivity +import com.remax.visualnovel.entity.request.ChargeProduct +import com.remax.visualnovel.entity.response.Wallet +import com.remax.visualnovel.entity.response.base.parseData +import com.remax.visualnovel.extension.launchFlow +import com.remax.visualnovel.extension.toast +import com.remax.visualnovel.manager.pay.GooglePayManager +import com.remax.visualnovel.repository.api.PayRepository +import com.remax.visualnovel.ui.wallet.dialog.RechargeDialog +import com.pengxr.modular.eventbus.generated.events.EventDefineOfWalletEvents +import com.remax.visualnovel.configs.NovelApplication +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** + * Created by HJW on 2025/9/15 + */ +object WalletManager : DefaultLifecycleObserver { + + var balance: Long = 0 + + private val walletObserver = Observer { wallet -> + balance = wallet?.balance ?: 0L + } + + override fun onCreate(owner: LifecycleOwner) { + EventDefineOfWalletEvents.buffBalanceUpdateSucceeded().observeForever(walletObserver) + } + + override fun onResume(owner: LifecycleOwner) { + refreshRechargeProducts() + } + + override fun onDestroy(owner: LifecycleOwner) { + EventDefineOfWalletEvents.buffBalanceUpdateSucceeded().removeObserver(walletObserver) + } + + private val payRepository by lazy { + PayRepository(ServiceFactory.createService()) + } + + fun refreshWallet() { + MainScope().launch { + launchFlow(payRepository::getMyWallet).collect() + } + } + + private val productList by lazy { + arrayListOf() + } + + /** + * 创建google pay充值订单 + * @param product ChargeProduct 后端返回的实体对象 + */ + fun onCreateOrder(product: ChargeProduct) { + MainScope().launch { + launchFlow({ + payRepository.createOrder(product) + }, { + (NovelApplication.getCurrentActivity() as? BaseBindingActivity<*>)?.showLoading() + }) { + (NovelApplication.getCurrentActivity() as? BaseBindingActivity<*>)?.hideLoading() + }.collect { + it.parseData({ + onSuccess = { res -> + res?.run { + (NovelApplication.getCurrentActivity() as? BaseBindingActivity<*>)?.let { act -> + GooglePayManager.pay(act, product.productId, tradeNo = res.tradeNo) { + act.toast(act.getString(R.string.google_pay_fail_toast)) + } + } + } + } + + onFailed = { errorCode, _ -> + + } + }) + } + } + } + + fun refreshRechargeProducts() { + GooglePayManager.checkProductDetails() + MainScope().launch { + if (productList.isEmpty()) { + payRepository.getProducts().transformResult({ + productList.clear() + productList.addAll(it?.productList ?: emptyList()) + }) + } + } + } + + fun showChargeDialog() { + if (productList.isNotEmpty()) { + NovelApplication.getCurrentActivity()?.let { act -> + productList.forEachIndexed { index, product -> + product.selected = index == 0 + } + RechargeDialog(act).build(productList) + } + } else { + refreshRechargeProducts() + } + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/recharge/RechargeFragment.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/recharge/RechargeFragment.kt new file mode 100644 index 0000000..8efaf57 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/recharge/RechargeFragment.kt @@ -0,0 +1,83 @@ +package com.remax.visualnovel.ui.wallet.recharge + +/** + * Created by HJW on 2025/9/12 + */ +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.fragment.app.activityViewModels +import com.alibaba.android.arouter.facade.annotation.Route +import com.alibaba.android.arouter.launcher.ARouter +import com.remax.visualnovel.R +import com.remax.visualnovel.app.base.BaseBindingFragment +import com.remax.visualnovel.databinding.FragmentRechargeBinding +import com.remax.visualnovel.extension.formatPrice +import com.remax.visualnovel.extension.launchAndCollect +import com.remax.visualnovel.extension.setOnClick +import com.remax.visualnovel.manager.pay.GooglePayManager +import com.remax.visualnovel.ui.wallet.WalletActivity +import com.remax.visualnovel.ui.wallet.WalletViewModel +import com.remax.visualnovel.ui.wallet.dialog.setRechargeProduct +import com.remax.visualnovel.ui.wallet.transaction.TransactionDetailActivity +import com.remax.visualnovel.utils.Routers +import com.pengxr.modular.eventbus.generated.events.EventDefineOfWalletEvents +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +@Route(path = Routers.RECHARGE) +class RechargeFragment : BaseBindingFragment() { + + private val walletViewModel by activityViewModels() + + @SuppressLint("SetTextI18n") + override fun onCreated(bundle: Bundle?) { + with(binding) { + transactionEnter.text = "${getString(R.string.transaction_detail)}>" + setOnClick(transactionEnter) { + when (this) { + transactionEnter -> { + TransactionDetailActivity.start() + } + } + } + } + getProducts() + } + + @SuppressLint("SetTextI18n") + private fun getProducts() { + launchAndCollect({ + walletViewModel.getProducts() + }) { + onSuccess = { + it?.productList?.forEachIndexed { index, chargeProduct -> + GooglePayManager.productDetails.forEach { productDetails -> + if (chargeProduct.productId == productDetails?.productId) { + chargeProduct.local = productDetails.oneTimePurchaseOfferDetails?.formattedPrice + chargeProduct.localCurrencyCode = + productDetails.oneTimePurchaseOfferDetails?.priceCurrencyCode ?: "USD" + } + } + } + (activity as? WalletActivity)?.refreshRechargeTxt() + binding.rvRecharge.setRechargeProduct(it?.productList) { item -> + (activity as? WalletActivity)?.refreshRechargeTxt() + } + } + } + } + + override fun subscribeUi() { + EventDefineOfWalletEvents.buffBalanceUpdateSucceeded().observe(this) { + binding.balance.setPrice(formatPrice(it?.balance)) + } + } + + companion object { + fun newInstance(): RechargeFragment { + return ARouter.getInstance().build(Routers.RECHARGE) + .navigation() as RechargeFragment + } + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/transaction/TransactionAdapter.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/transaction/TransactionAdapter.kt new file mode 100644 index 0000000..d765064 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/transaction/TransactionAdapter.kt @@ -0,0 +1,73 @@ +package com.remax.visualnovel.ui.wallet.transaction + +/** + * Created by HJW on 2025/9/12 + */ + +import com.chad.library.adapter.base.module.LoadMoreModule +import com.remax.visualnovel.R +import com.remax.visualnovel.app.BaseBindingQuickAdapter +import com.remax.visualnovel.app.widget.setMyEmptyView +import com.remax.visualnovel.databinding.ItemIncomeBinding +import com.remax.visualnovel.entity.request.PageQuery +import com.remax.visualnovel.entity.response.AccountBuffBill +import com.remax.visualnovel.entity.response.TransactionGift +import com.remax.visualnovel.extension.convertFromJson +import com.remax.visualnovel.extension.formatPrice +import com.remax.visualnovel.extension.glide.load +import com.remax.visualnovel.manager.gift.GiftManager +import com.remax.visualnovel.utils.TimeUtils +import com.remax.visualnovel.widget.uitoken.changeTextColor + +/** + * 这是一个适配器,LoadMoreModule按需取舍 + */ +class TransactionAdapter(private val type: String? = null) : + BaseBindingQuickAdapter(ItemIncomeBinding::inflate), + LoadMoreModule { + + override fun convert(holder: BaseBindingHolder, item: AccountBuffBill) { + with(holder.getViewBinding()) { + timeIncome.text = TimeUtils.format_y_m_d_h_m_s(item.time) + titleIncome.text = item.item + ivIncome.setImageResource(R.mipmap.ic_transaction) + GiftManager.gifts?.find { it.id == item.extend.convertFromJson()?.giftId }?.let { gift -> + ivIncome.load(gift.icon) + } + + when { + type == AccountBuffBill.INCOME -> { + priceIncome.setPrice("+${formatPrice(item.amount)}") + } + + item.isIn -> { + priceIncome.getContentView()?.changeTextColor { + textUIColorToken = context.getString(R.string.color_positive_normal) + } + priceIncome.setPrice("+${formatPrice(item.amount)}") + } + + else -> { + priceIncome.getContentView()?.changeTextColor { + textUIColorToken = context.getString(R.string.color_important_variant_normal) + } + priceIncome.setPrice("-${formatPrice(item.amount)}") + } + } + } + } + + fun setTransactionData(isRefresh: Boolean, data: List, topMargin: Int) { + if (isRefresh) { + setList(data) + setMyEmptyView(R.string.no_transaction_yet, topMargin = topMargin) + } else { + addData(data) + loadMoreModule.loadMoreComplete() + } + if (data.size < PageQuery.DEFAULT_PAGE_SIZE) { + loadMoreModule.loadMoreEnd() + } + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/transaction/TransactionDetailActivity.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/transaction/TransactionDetailActivity.kt new file mode 100644 index 0000000..569ee3c --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/wallet/transaction/TransactionDetailActivity.kt @@ -0,0 +1,82 @@ +package com.remax.visualnovel.ui.wallet.transaction + +/** + * Created by HJW on 2025/9/12 + */ +import androidx.activity.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.alibaba.android.arouter.facade.annotation.Route +import com.alibaba.android.arouter.launcher.ARouter +import com.remax.visualnovel.R +import com.remax.visualnovel.app.base.BaseBindingActivity +import com.remax.visualnovel.databinding.ActivityTransactionBinding +import com.remax.visualnovel.extension.addScrollerAlpha +import com.remax.visualnovel.extension.autoRefreshList +import com.remax.visualnovel.extension.launchAndCollect +import com.remax.visualnovel.ui.wallet.WalletViewModel +import com.remax.visualnovel.utils.Routers +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.itemdecoration.SpaceItemDecoration +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +@Route(path = Routers.TRANSACTION) +class TransactionDetailActivity : BaseBindingActivity() { + + private val walletViewModel by viewModels() + + private val transactionAdapter by lazy { + TransactionAdapter() + } + + override fun initView() { + setToolbar(R.string.transaction_detail) + with(binding) { + appBarLayout.addScrollerAlpha(tvTitle, isFromToolbar = true) + refreshLayout.setOnRefreshListener { + getData(true) + } + with(recyclerView) { + adapter = transactionAdapter + layoutManager = LinearLayoutManager(recyclerView.context) + addItemDecoration(SpaceItemDecoration(16.dp, 24.dp,0,24.dp)) + with(transactionAdapter) { + setOnItemClickListener { _, _, position -> + + } + loadMoreModule.setOnLoadMoreListener { + getData(false) + } + } + } + } + } + + override fun initData() { + binding.refreshLayout.autoRefreshList() + } + + private fun getData(isRefresh: Boolean) { + launchAndCollect({ walletViewModel.getBillList(isRefresh, null) }) { + onSuccess = { + val data = it?.pageList?.datas ?: emptyList() + transactionAdapter.setTransactionData(isRefresh, data, 120) + } + + onComplete = { + if (isRefresh) { + binding.refreshLayout.finishRefresh() + } + } + } + } + + companion object { + fun start() { + ARouter.getInstance() + .build(Routers.TRANSACTION) + .navigation() + } + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/RecordHelper.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/RecordHelper.kt new file mode 100644 index 0000000..d6ad93c --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/RecordHelper.kt @@ -0,0 +1,91 @@ +package com.remax.visualnovel.utils + +import android.app.Activity +import android.os.Environment +import com.buihha.audiorecorder.Mp3Recorder +import com.buihha.audiorecorder.Mp3Recorder.OnRecordListener +import timber.log.Timber +import java.io.File + +/** + * Created by HJW on 2025/8/16 + */ +class RecordHelper { + + private var recorder: Mp3Recorder? = null + private var filename: String = "null" + + fun isRecording(): Boolean { + return recorder != null && recorder!!.isRecording() + } + + @Synchronized + fun startRecording(activity: Activity, onStart: () -> Unit, onStop: () -> Unit) { + try { + recorder = Mp3Recorder() + recorder!!.setOnRecordListener(object : OnRecordListener { + override fun onStart() { + //开始录音 + onStart.invoke() + } + + override fun onStop() { + //停止录音 + onStop.invoke() + } + + override fun onError() { + Timber.i("startRecording onError") + //录音错误,主要针对OPPO手机在调用startRecord方法时弹窗安全权限提示,此时如果拒绝,则会执行该回调 + onStop.invoke() + } + + override fun onRecording(i: Int, v: Double) { + //Timber.d("采样:" + i + "Hz 音量:" + v + "分贝"); + } + }) + if (!recorder!!.isRecording()) { + val filePath = activity.getExternalFilesDir(Environment.DIRECTORY_MUSIC) + val file = File( + filePath, + "record_${System.currentTimeMillis()}.mp3" + ) + filename = file.path + recorder!!.startRecording(filePath?.path, file.getName()) + } + } catch (e: Exception) { + Timber.i("startRecording catch :${e.localizedMessage}") + e.printStackTrace() + onStop.invoke() + } + } + + @Synchronized + fun stopRecording() { + try { + if (recorder != null && recorder!!.isRecording()) { + recorder!!.stopRecording() + recorder = null + } + } catch (e: Exception) { + e.printStackTrace() + Timber.i("startRecording stopRecording :${e.localizedMessage}") + + recorder = null + + val file = File(filename) + if (file.exists()) { + file.delete() + } + } + } + + fun getFilename(): String { + return filename + } + + fun setFilename(filename: String) { + this.filename = filename + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/Routers.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/Routers.kt index 77e0a08..ba6b05b 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/Routers.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/Routers.kt @@ -13,7 +13,17 @@ class Routers { private const val ROUTER = "/router/" + //----------------- Pre ---------------- + const val CHAT_FLIRTING_LEVEL = "${ROUTER}chatFlirtingLevel" + const val WALLET = "${ROUTER}wallet" + const val INCOME = "${ROUTER}income" + const val TRANSACTION = "${ROUTER}transaction" + const val RECHARGE = "${ROUTER}recharge" + + + + //----------------- New ---------------- const val SPLASH = "${ROUTER}splash" const val MAIN = "${ROUTER}main" const val ROUTE_FRAG_BOOKLIST = "${ROUTER}bookList" @@ -39,6 +49,10 @@ class Routers { navigationToBrowser(url = BuildConfig.EPAL_TERMS_SERVICES) } + fun navigationToRA() { + navigationToBrowser(url = BuildConfig.RECHAEGE_SERVICES) + } + fun navigationToBrowser(title: String = "", url: String, needTitle: Boolean = true) { ARouter.getInstance() .build(BROWSER) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/analytics/AnalyticsEvent.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/analytics/AnalyticsEvent.kt new file mode 100644 index 0000000..b98a77e --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/analytics/AnalyticsEvent.kt @@ -0,0 +1,10 @@ +package com.remax.visualnovel.utils.analytics + +/** + * Created by HJW on 2020/9/23 + * 埋点事件 + */ +object AnalyticsEvent { + + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/analytics/AnalyticsUtils.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/analytics/AnalyticsUtils.kt new file mode 100644 index 0000000..5e3ee79 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/analytics/AnalyticsUtils.kt @@ -0,0 +1,38 @@ +package com.remax.visualnovel.utils.analytics + +import android.content.Context +import android.os.Bundle +import com.remax.visualnovel.manager.login.LoginManager +import com.google.firebase.analytics.FirebaseAnalytics +import timber.log.Timber + +object AnalyticsUtils { + + private lateinit var firebaseAnalytics: FirebaseAnalytics + + fun init(context: Context) { + if (!this::firebaseAnalytics.isInitialized) { + firebaseAnalytics = FirebaseAnalytics.getInstance(context) + } + refreshUserId() + } + + fun refreshUserId() { + Timber.d("firebase埋点更新userId:${LoginManager.user?.userId}") + if (this::firebaseAnalytics.isInitialized) { + firebaseAnalytics.setUserId(LoginManager.user?.userId?:"") + } + } + + /** + * 添加埋点 + */ + fun logAnalytics(event: String, bundle: Bundle? = null) { + Timber.d("埋点事件:$event") + if (this::firebaseAnalytics.isInitialized) { + Timber.d("firebase埋点 event:$event bundle:${bundle.toString()}") + firebaseAnalytics.logEvent(event, bundle) + } + } + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/RoundFrameLayout.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/RoundFrameLayout.java new file mode 100644 index 0000000..105d2d1 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/RoundFrameLayout.java @@ -0,0 +1,183 @@ +package com.remax.visualnovel.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import com.remax.visualnovel.R; + +/** + * 裁掉圆角边的父布局 + */ +public class RoundFrameLayout extends FrameLayout { + + private float topLeftRadius; + private float topRightRadius; + private float bottomLeftRadius; + private float bottomRightRadius; + + private Paint roundPaint; + private Paint imagePaint; + + public RoundFrameLayout(Context context) { + this(context, null); + } + + public RoundFrameLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RoundFrameLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (attrs != null) { + TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundFrameLayout); + float radius = ta.getDimension(R.styleable.RoundFrameLayout_radius, 0); + topLeftRadius = ta.getDimension(R.styleable.RoundFrameLayout_topLeftRadius, radius); + topRightRadius = ta.getDimension(R.styleable.RoundFrameLayout_topRightRadius, radius); + bottomLeftRadius = ta.getDimension(R.styleable.RoundFrameLayout_bottomLeftRadius, radius); + bottomRightRadius = ta.getDimension(R.styleable.RoundFrameLayout_bottomRightRadius, radius); + ta.recycle(); + } + roundPaint = new Paint(); + roundPaint.setColor(Color.WHITE); + roundPaint.setAntiAlias(true); + roundPaint.setStyle(Paint.Style.FILL); + roundPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + + imagePaint = new Paint(); + imagePaint.setXfermode(null); + } + + public void setRadius(float radius) { + topLeftRadius = radius; + topRightRadius = radius; + bottomLeftRadius = radius; + bottomRightRadius = radius; + } + + public void setRadius(float topLeftRadius, float topRightRadius, float bottomLeftRadius, float bottomRightRadius) { + this.topLeftRadius = topLeftRadius; + this.topRightRadius = topRightRadius; + this.bottomLeftRadius = bottomLeftRadius; + this.bottomRightRadius = bottomRightRadius; + } + + //实现1 + @Override + protected void dispatchDraw(Canvas canvas) { + int width = getWidth(); + int height = getHeight(); + Path path = new Path(); + path.moveTo(0, topLeftRadius); + path.arcTo(new RectF(0, 0, topLeftRadius * 2, topLeftRadius * 2), -180, 90); + path.lineTo(width - topRightRadius, 0); + path.arcTo(new RectF(width - 2 * topRightRadius, 0, width, topRightRadius * 2), -90, 90); + path.lineTo(width, height - bottomRightRadius); + path.arcTo(new RectF(width - 2 * bottomRightRadius, height - 2 * bottomRightRadius, width, height), 0, 90); + path.lineTo(bottomLeftRadius, height); + path.arcTo(new RectF(0, height - 2 * bottomLeftRadius, bottomLeftRadius * 2, height), 90, 90); + path.close(); + canvas.clipPath(path); + super.dispatchDraw(canvas); + } + +// //实现2 +// @Override +// protected void dispatchDraw(Canvas canvas) { +// super.dispatchDraw(canvas); +// drawTopLeft(canvas);//用PorterDuffXfermode +// drawTopRight(canvas);//用PorterDuffXfermode +// drawBottomLeft(canvas);//用PorterDuffXfermode +// drawBottomRight(canvas);//用PorterDuffXfermode +// } + +// //实现3 +// @Override +// protected void dispatchDraw(Canvas canvas) { +// Bitmap bitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888); +// Canvas newCanvas = new Canvas(bitmap); +// super.dispatchDraw(newCanvas); +// drawTopLeft(newCanvas); +// drawTopRight(newCanvas); +// drawBottomLeft(newCanvas); +// drawBottomRight(newCanvas); +// canvas.drawBitmap(bitmap, 0, 0, imagePaint); +//// invalidate(); +// } + +// //实现4 +// @Override +// protected void dispatchDraw(Canvas canvas) { +// canvas.saveLayer(new RectF(0, 0, canvas.getWidth(), canvas.getHeight()), imagePaint, Canvas.ALL_SAVE_FLAG); +// super.dispatchDraw(canvas); +// drawTopLeft(canvas); +// drawTopRight(canvas); +// drawBottomLeft(canvas); +// drawBottomRight(canvas); +// canvas.restore(); +// } + + private void drawTopLeft(Canvas canvas) { + if (topLeftRadius > 0) { + Path path = new Path(); + path.moveTo(0, topLeftRadius); + path.lineTo(0, 0); + path.lineTo(topLeftRadius, 0); + path.arcTo(new RectF(0, 0, topLeftRadius * 2, topLeftRadius * 2), + -90, -90); + path.close(); + canvas.drawPath(path, roundPaint); + } + } + + private void drawTopRight(Canvas canvas) { + if (topRightRadius > 0) { + int width = getWidth(); + Path path = new Path(); + path.moveTo(width - topRightRadius, 0); + path.lineTo(width, 0); + path.lineTo(width, topRightRadius); + path.arcTo(new RectF(width - 2 * topRightRadius, 0, width, + topRightRadius * 2), 0, -90); + path.close(); + canvas.drawPath(path, roundPaint); + } + } + + private void drawBottomLeft(Canvas canvas) { + if (bottomLeftRadius > 0) { + int height = getHeight(); + Path path = new Path(); + path.moveTo(0, height - bottomLeftRadius); + path.lineTo(0, height); + path.lineTo(bottomLeftRadius, height); + path.arcTo(new RectF(0, height - 2 * bottomLeftRadius, + bottomLeftRadius * 2, height), 90, 90); + path.close(); + canvas.drawPath(path, roundPaint); + } + } + + private void drawBottomRight(Canvas canvas) { + if (bottomRightRadius > 0) { + int height = getHeight(); + int width = getWidth(); + Path path = new Path(); + path.moveTo(width - bottomRightRadius, height); + path.lineTo(width, height); + path.lineTo(width, height - bottomRightRadius); + path.arcTo(new RectF(width - 2 * bottomRightRadius, height - 2 + * bottomRightRadius, width, height), 0, 90); + path.close(); + canvas.drawPath(path, roundPaint); + } + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryNavigatorAdapter.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryNavigatorAdapter.kt new file mode 100644 index 0000000..696c130 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryNavigatorAdapter.kt @@ -0,0 +1,64 @@ +package com.remax.visualnovel.widget.indicator + +import android.content.Context +import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2 +import com.remax.visualnovel.app.base.BaseCommonNavigatorAdapter +import com.remax.visualnovel.extension.setOnClick +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IPagerIndicator +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IPagerTitleView + +class SecondaryNavigatorAdapter( + private val mTitleList: List, + override val viewPager2: ViewPager2? = null, + override val viewPager: ViewPager? = null, + private val lockList: List? = null, +) : BaseCommonNavigatorAdapter(viewPager2, viewPager) { + + var indexClickBlock: ((Int) -> Unit)? = null + + override fun getCount(): Int { + return mTitleList.size + } + + override fun getTitleView(context: Context, index: Int): IPagerTitleView { + val secondaryPagerTitleView = + if (lockList == null) { + SecondaryPagerTitleView(context) + } else { + SecondaryPagerLockedTitleView(context) + } + + + (secondaryPagerTitleView as? SecondaryPagerLockedTitleView)?.let { + val isLock = lockList?.getOrNull(index) == true + it.setIcon(isLock) + it.getTitleTextView().text = mTitleList[index] + } + + (secondaryPagerTitleView as? SecondaryPagerMoreTitleView)?.let { + it.getTitleTextView().text = mTitleList[index] + } + + (secondaryPagerTitleView as? SecondaryPagerTitleView)?.let { + it.getTitleTextView().text = mTitleList[index] + } + + setOnClick(secondaryPagerTitleView) { + indexClickBlock?.invoke(index) + viewPager2?.let { + it.currentItem = index + } + viewPager?.let { + it.currentItem = index + } + } + + + return secondaryPagerTitleView + } + + override fun getIndicator(context: Context): IPagerIndicator? { + return null + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryNavigatorDictAdapter.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryNavigatorDictAdapter.kt new file mode 100644 index 0000000..68053ef --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryNavigatorDictAdapter.kt @@ -0,0 +1,48 @@ +package com.remax.visualnovel.widget.indicator + +import android.annotation.SuppressLint +import android.content.Context +import com.remax.visualnovel.app.base.BaseCommonNavigatorAdapter +import com.remax.visualnovel.entity.response.AIDictItem +import com.remax.visualnovel.extension.setOnClick +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IPagerIndicator +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IPagerTitleView + +class SecondaryNavigatorDictAdapter( + private val mTitleList: List, +) : BaseCommonNavigatorAdapter(null, null) { + + var indexClickBlock: ((Int) -> Unit)? = null + + override fun getCount(): Int { + return mTitleList.size + } + + @SuppressLint("SetTextI18n") + override fun getTitleView(context: Context, index: Int): IPagerTitleView { + val secondaryPagerTitleView = SecondaryPagerTitleView(context) + val currItem = mTitleList[index] + secondaryPagerTitleView.getTitleTextView().text = "#${currItem.name}" + if (currItem.select) { + secondaryPagerTitleView.onSelected(index, mTitleList.size) + } else { + secondaryPagerTitleView.onDeselected(index, mTitleList.size) + } + setOnClick(secondaryPagerTitleView) { + mTitleList.forEach { tag -> + if (tag == currItem) tag.select = !tag.select + } + if (currItem.select) { + secondaryPagerTitleView.onSelected(index, mTitleList.size) + } else { + secondaryPagerTitleView.onDeselected(index, mTitleList.size) + } + indexClickBlock?.invoke(index) + } + return secondaryPagerTitleView + } + + override fun getIndicator(context: Context): IPagerIndicator? { + return null + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryPagerLockedTitleView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryPagerLockedTitleView.kt new file mode 100644 index 0000000..17d5a9d --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryPagerLockedTitleView.kt @@ -0,0 +1,156 @@ +package com.remax.visualnovel.widget.indicator + +import android.content.Context +import android.graphics.Rect +import android.text.TextUtils +import android.view.Gravity +import android.widget.FrameLayout +import androidx.appcompat.widget.AppCompatTextView +import com.remax.visualnovel.R +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.ui.IconFontTextView +import com.remax.visualnovel.widget.uitoken.changeBackground +import com.remax.visualnovel.widget.uitoken.changeTextColor +import com.remax.visualnovel.widget.uitoken.changeTextFont +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IMeasurablePagerTitleView + +class SecondaryPagerLockedTitleView(context: Context) : FrameLayout(context, null), IMeasurablePagerTitleView { + + private lateinit var titleTextView: IconFontTextView + private var isActive = false + + init { + init(context) + } + + private fun init(context: Context) { + titleTextView = IconFontTextView(context) + addView(titleTextView) + with(titleTextView) { + setPaddingRelative(16.dp, 0, 16.dp, 0) + setSingleLine() + ellipsize = TextUtils.TruncateAt.END + includeFontPadding = false + height = 32.dp + gravity = Gravity.CENTER + changeTextFont { + textUITextToken = context.getString(R.string.txt_label_m) + } + } + + setPaddingRelative(0, 0, 12.dp, 0) + } + + fun setIcon(isLock: Boolean) { + titleTextView.setIconFontDrawable( + endIconFont = if (isLock) context.getString(R.string.icon_private) else null, + iconColorToken = context.getString(R.string.color_txt_primary_normal), + iconSize = 16, iconPadding = 4 + ) + } + + private fun setActiveStatus(isActive: Boolean) { + this.isActive = isActive + with(titleTextView) { + if (isActive) { + changeTextColor { + textUIColorToken = context.getString(R.string.color_txt_primary_specialmap_normal) + textUIPressedColorToken = context.getString(R.string.color_txt_primary_specialmap_normal) + } + + changeBackground { + radiusToken = context.getString(R.string.radius_pill) + + strokeUIWidthToken = context.getString(R.string.border_s) + + backgroundUIColorToken = context.getString(R.string.color_primary_onpic_normal) + strokeUIColorToken = context.getString(R.string.color_primary_variant_normal) + + backgroundUIDisabledColorToken = context.getString(R.string.color_surface_element_disabled) + strokeUIDisabledColorToken = context.getString(R.string.color_outline_disabled) + + backgroundUIPressedColorToken = context.getString(R.string.color_primary_onpic_press) + strokeUIPressedColorToken = context.getString(R.string.color_primary_variant_normal) + } + + } else { + changeTextColor { + textUIColorToken = context.getString(R.string.color_txt_primary_normal) + textUIPressedColorToken = context.getString(R.string.color_txt_primary_normal) + textUIDisabledColorToken = context.getString(R.string.color_txt_disabled) + } + + changeBackground { + radiusToken = context.getString(R.string.radius_pill) + //无筛选状态将描边恢复默认 + strokeUIWidthToken = "" + + backgroundUIColorToken = context.getString(R.string.color_surface_element_normal) + + backgroundUIDisabledColorToken = context.getString(R.string.color_surface_element_disabled) + + backgroundUIPressedColorToken = context.getString(R.string.color_surface_element_press) + } + } + } + + } + + override fun onSelected(index: Int, totalCount: Int) { + setActiveStatus(true) + } + + override fun onDeselected(index: Int, totalCount: Int) { + setActiveStatus(false) + } + + override fun onLeave(index: Int, totalCount: Int, leavePercent: Float, leftToRight: Boolean) {} + override fun onEnter(index: Int, totalCount: Int, enterPercent: Float, leftToRight: Boolean) {} + override fun getContentLeft(): Int { + val bound = Rect() + var longestString = "" + if (titleTextView.text.toString().contains("\n")) { + val brokenStrings = titleTextView.text.toString().split("\\n".toRegex()).toTypedArray() + for (each in brokenStrings) { + if (each.length > longestString.length) longestString = each + } + } else { + longestString = titleTextView.text.toString() + } + titleTextView.paint.getTextBounds(longestString, 0, longestString.length, bound) + val contentWidth = bound.width() + return left + width / 2 - contentWidth / 2 + } + + override fun getContentTop(): Int { + val metrics = titleTextView.paint.fontMetrics + val contentHeight = metrics.bottom - metrics.top + return (height / 2 - contentHeight / 2).toInt() + } + + override fun getContentRight(): Int { + val bound = Rect() + var longestString = "" + if (titleTextView.text.toString().contains("\n")) { + val brokenStrings = titleTextView.text.toString().split("\\n".toRegex()).toTypedArray() + for (each in brokenStrings) { + if (each.length > longestString.length) longestString = each + } + } else { + longestString = titleTextView.text.toString() + } + titleTextView.paint.getTextBounds(longestString, 0, longestString.length, bound) + val contentWidth = bound.width() + return left + width / 2 + contentWidth / 2 + } + + override fun getContentBottom(): Int { + val metrics = titleTextView.paint.fontMetrics + val contentHeight = metrics.bottom - metrics.top + return (height / 2 + contentHeight / 2).toInt() + } + + fun getTitleTextView(): AppCompatTextView { + return titleTextView + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryPagerMoreTitleView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryPagerMoreTitleView.kt new file mode 100644 index 0000000..a155d4d --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryPagerMoreTitleView.kt @@ -0,0 +1,102 @@ +package com.remax.visualnovel.widget.indicator + +import android.content.Context +import android.graphics.Rect +import android.text.TextUtils +import android.view.Gravity +import android.widget.FrameLayout +import androidx.appcompat.widget.AppCompatTextView +import com.remax.visualnovel.R +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.ui.IconFontTextView +import com.remax.visualnovel.widget.uitoken.changeTextStyle +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IMeasurablePagerTitleView + +class SecondaryPagerMoreTitleView(context: Context) : FrameLayout(context, null), IMeasurablePagerTitleView { + + private lateinit var titleTextView: IconFontTextView + + init { + init(context) + } + + private fun init(context: Context) { + titleTextView = IconFontTextView(context) + addView(titleTextView) + with(titleTextView) { + setPaddingRelative(16.dp, 0, 16.dp, 0) + setSingleLine() + ellipsize = TextUtils.TruncateAt.END + includeFontPadding = false + height = 32.dp + gravity = Gravity.CENTER + setIconFontDrawable( + endIconFont = context.getString(R.string.icon_arrow_down_fill), + iconColorToken = context.getString(R.string.color_txt_primary_normal), + iconSize = 16, iconPadding = 4 + ) + changeTextStyle { + textUITextToken = context.getString(R.string.txt_label_m) + textUIColorToken = context.getString(R.string.color_txt_primary_specialmap_normal) + } + } + } + + override fun onSelected(index: Int, totalCount: Int) { + + } + + override fun onDeselected(index: Int, totalCount: Int) { + + } + + override fun onLeave(index: Int, totalCount: Int, leavePercent: Float, leftToRight: Boolean) {} + override fun onEnter(index: Int, totalCount: Int, enterPercent: Float, leftToRight: Boolean) {} + override fun getContentLeft(): Int { + val bound = Rect() + var longestString = "" + if (titleTextView.text.toString().contains("\n")) { + val brokenStrings = titleTextView.text.toString().split("\\n".toRegex()).toTypedArray() + for (each in brokenStrings) { + if (each.length > longestString.length) longestString = each + } + } else { + longestString = titleTextView.text.toString() + } + titleTextView.paint.getTextBounds(longestString, 0, longestString.length, bound) + val contentWidth = bound.width() + return left + width / 2 - contentWidth / 2 + } + + override fun getContentTop(): Int { + val metrics = titleTextView.paint.fontMetrics + val contentHeight = metrics.bottom - metrics.top + return (height / 2 - contentHeight / 2).toInt() + } + + override fun getContentRight(): Int { + val bound = Rect() + var longestString = "" + if (titleTextView.text.toString().contains("\n")) { + val brokenStrings = titleTextView.text.toString().split("\\n".toRegex()).toTypedArray() + for (each in brokenStrings) { + if (each.length > longestString.length) longestString = each + } + } else { + longestString = titleTextView.text.toString() + } + titleTextView.paint.getTextBounds(longestString, 0, longestString.length, bound) + val contentWidth = bound.width() + return left + width / 2 + contentWidth / 2 + } + + override fun getContentBottom(): Int { + val metrics = titleTextView.paint.fontMetrics + val contentHeight = metrics.bottom - metrics.top + return (height / 2 + contentHeight / 2).toInt() + } + + fun getTitleTextView(): AppCompatTextView { + return titleTextView + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryPagerTitleView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryPagerTitleView.kt new file mode 100644 index 0000000..2a4f39d --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/indicator/SecondaryPagerTitleView.kt @@ -0,0 +1,82 @@ +package com.remax.visualnovel.widget.indicator + +import android.content.Context +import android.graphics.Rect +import android.widget.FrameLayout +import androidx.appcompat.widget.AppCompatTextView +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.ui.chips.FilterChipView +import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.IMeasurablePagerTitleView + +class SecondaryPagerTitleView(context: Context) : FrameLayout(context, null), IMeasurablePagerTitleView { + + private lateinit var titleTextView: FilterChipView + + init { + init(context) + } + + private fun init(context: Context) { + titleTextView = FilterChipView(context) + addView(titleTextView) + setPaddingRelative(0, 0, 12.dp, 0) + } + + override fun onSelected(index: Int, totalCount: Int) { + titleTextView.setActive() + } + + override fun onDeselected(index: Int, totalCount: Int) { + titleTextView.setInActive() + } + + override fun onLeave(index: Int, totalCount: Int, leavePercent: Float, leftToRight: Boolean) {} + override fun onEnter(index: Int, totalCount: Int, enterPercent: Float, leftToRight: Boolean) {} + override fun getContentLeft(): Int { + val bound = Rect() + var longestString = "" + if (titleTextView.text.toString().contains("\n")) { + val brokenStrings = titleTextView.text.toString().split("\\n".toRegex()).toTypedArray() + for (each in brokenStrings) { + if (each.length > longestString.length) longestString = each + } + } else { + longestString = titleTextView.text.toString() + } + titleTextView.paint.getTextBounds(longestString, 0, longestString.length, bound) + val contentWidth = bound.width() + return left + width / 2 - contentWidth / 2 + } + + override fun getContentTop(): Int { + val metrics = titleTextView.paint.fontMetrics + val contentHeight = metrics.bottom - metrics.top + return (height / 2 - contentHeight / 2).toInt() + } + + override fun getContentRight(): Int { + val bound = Rect() + var longestString = "" + if (titleTextView.text.toString().contains("\n")) { + val brokenStrings = titleTextView.text.toString().split("\\n".toRegex()).toTypedArray() + for (each in brokenStrings) { + if (each.length > longestString.length) longestString = each + } + } else { + longestString = titleTextView.text.toString() + } + titleTextView.paint.getTextBounds(longestString, 0, longestString.length, bound) + val contentWidth = bound.width() + return left + width / 2 + contentWidth / 2 + } + + override fun getContentBottom(): Int { + val metrics = titleTextView.paint.fontMetrics + val contentHeight = metrics.bottom - metrics.top + return (height / 2 + contentHeight / 2).toInt() + } + + fun getTitleTextView(): AppCompatTextView { + return titleTextView + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/itemdecoration/VerticalItemDecoration.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/itemdecoration/VerticalItemDecoration.kt index 8cfce0f..3c2cc05 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/itemdecoration/VerticalItemDecoration.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/itemdecoration/VerticalItemDecoration.kt @@ -1,4 +1,4 @@ -package com.crushlevel.android.widget.itemdecoration +package com.remax.visualnovel.widget.itemdecoration import android.graphics.Rect import android.view.View diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/AssistChipView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/AssistChipView.kt new file mode 100644 index 0000000..b79bd5d --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/AssistChipView.kt @@ -0,0 +1,38 @@ +package com.remax.visualnovel.widget.ui.chips + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import com.remax.visualnovel.R +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.ui.IconFontTextView +import com.remax.visualnovel.widget.uitoken.changeBackground +import com.remax.visualnovel.widget.uitoken.changeTextStyle + +/** + * create by wl on 2022/9/9 + * 帮助chip + */ +class AssistChipView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : IconFontTextView(context, attrs, defStyleAttr) { + + init { + //初始化固定token + changeTextStyle { + textUITextToken = context.getString(R.string.txt_label_m) + textUIColorToken = context.getString(R.string.color_txt_primary_normal) + textUIDisabledColorToken = context.getString(R.string.color_txt_disabled) + textUIPressedColorToken = context.getString(R.string.color_txt_primary_normal) + } + changeBackground { + backgroundUIColorToken = context.getString(R.string.color_surface_element_normal) + backgroundUIDisabledColorToken = context.getString(R.string.color_surface_element_disabled) + backgroundUIPressedColorToken = context.getString(R.string.color_surface_element_press) + radiusToken = context.getString(R.string.radius_round) + } + gravity = Gravity.CENTER + height = 32.dp + setPaddingRelative(12.dp, 0, 16.dp, 0) + } + + +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/DropdownChipView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/DropdownChipView.kt new file mode 100644 index 0000000..176d12d --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/DropdownChipView.kt @@ -0,0 +1,57 @@ +package com.remax.visualnovel.widget.ui.chips + +import android.content.Context +import android.util.AttributeSet +import com.remax.visualnovel.R +import com.remax.visualnovel.widget.ui.IconFontTextView +import com.remax.visualnovel.widget.uitoken.changeBackground +import com.remax.visualnovel.widget.uitoken.changeTextColor +import com.remax.visualnovel.widget.uitoken.changeTextFont +import com.scwang.smart.refresh.layout.util.SmartUtil + +class DropdownChipView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : IconFontTextView(context, attrs, defStyleAttr) { + + + init { + //初始化固定token + customViewToken.run { + textUITextToken = context.getString(R.string.txt_label_m) + textUIDisabledColorToken = context.getString(R.string.color_txt_disabled) + backgroundUIPressedColorToken = context.getString(R.string.color_surface_element_press) + radiusToken = context.getString(R.string.radius_round) + } + changeTextFont(customViewToken) + setInActive() + setPaddingRelative(SmartUtil.dp2px(16f), SmartUtil.dp2px(6f), SmartUtil.dp2px(8f), SmartUtil.dp2px(6f)) + } + + /** + * 筛选状态 + */ + fun setActive() { + customViewToken.run { + //normal + textUIColorToken = context.getString(R.string.color_primary_variant_normal) + //press + textUIPressedColorToken = context.getString(R.string.color_primary_variant_normal) + setIconFontDrawable(endIconFont = context.getString(R.string.icon_arrow_down_fill), iconPadding = 4, iconSize = 12) + changeTextColor(customViewToken) + changeBackground(customViewToken) + } + } + + /** + * 无筛选状态 + */ + fun setInActive() { + customViewToken.run { + //normal + textUIColorToken = context.getString(R.string.color_txt_primary_normal) + //press + textUIPressedColorToken = context.getString(R.string.color_txt_primary_normal) + setIconFontDrawable(endIconFont = context.getString(R.string.icon_arrow_down_fill), iconPadding = 4, iconSize = 12) + changeTextColor(customViewToken) + changeBackground(customViewToken) + } + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/FilterChipView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/FilterChipView.kt new file mode 100644 index 0000000..ee18e52 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/FilterChipView.kt @@ -0,0 +1,103 @@ +package com.remax.visualnovel.widget.ui.chips + +import android.content.Context +import android.text.TextUtils +import android.util.AttributeSet +import android.view.Gravity +import com.remax.visualnovel.R +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.uitoken.changeBackground +import com.remax.visualnovel.widget.uitoken.changeTextColor +import com.remax.visualnovel.widget.uitoken.changeTextFont +import com.remax.visualnovel.widget.uitoken.view.UITokenTextView + +/** + * create by wl on 2022/9/8 + * 筛选chip + */ +class FilterChipView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : UITokenTextView(context, attrs, defStyleAttr) { + + var isActive = false + private set + + init { + //初始化固定token + customViewToken.run { + radiusToken = context.getString(R.string.radius_round) + textUITextToken = context.getString(R.string.txt_label_m) + textUIDisabledColorToken = context.getString(R.string.color_txt_disabled) + } + changeTextFont(customViewToken) + setInActive() + setPaddingRelative(16.dp, 0, 16.dp, 0) + setSingleLine() + ellipsize = TextUtils.TruncateAt.END + includeFontPadding = false + height = 32.dp + gravity = Gravity.CENTER + } + + /** + * 筛选状态 + */ + fun setActive() { + isActive = true + setActiveStatus() + } + + /** + * 无筛选状态 + */ + fun setInActive() { + isActive = false + setActiveStatus() + } + + private fun setActiveStatus() { + if (isActive) { + changeTextColor { + textUIColorToken = context.getString(R.string.color_txt_primary_specialmap_normal) + textUIPressedColorToken = context.getString(R.string.color_txt_primary_specialmap_normal) + } + + changeBackground { + strokeUIWidthToken = context.getString(R.string.border_s) + + backgroundUIColorToken = context.getString(R.string.color_primary_normal) + strokeUIColorToken = context.getString(R.string.color_outline_normal) + + backgroundUIDisabledColorToken = context.getString(R.string.color_emphasis_disabled) + strokeUIDisabledColorToken = context.getString(R.string.color_outline_disabled) + + backgroundUIPressedColorToken = context.getString(R.string.color_primary_press) + strokeUIPressedColorToken = context.getString(R.string.color_primary_variant_normal) + } + + } else { + changeTextColor { + textUIColorToken = context.getString(R.string.color_txt_primary_normal) + textUIPressedColorToken = context.getString(R.string.color_txt_primary_normal) + textUIDisabledColorToken = context.getString(R.string.color_txt_disabled) + } + + changeBackground { + strokeUIWidthToken = "" + + backgroundUIColorToken = context.getString(R.string.color_surface_element_normal) + + backgroundUIDisabledColorToken = context.getString(R.string.color_surface_element_disabled) + + backgroundUIPressedColorToken = context.getString(R.string.color_surface_element_press) + } + } + } + + fun setActive(isActive: Boolean?) { + this.isActive = isActive == true + setActiveStatus() + } + + fun getActive(): Boolean { + return isActive + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/RemovableChipView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/RemovableChipView.kt new file mode 100644 index 0000000..807d062 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/ui/chips/RemovableChipView.kt @@ -0,0 +1,127 @@ +package com.remax.visualnovel.widget.ui.chips + +import android.content.Context +import android.util.AttributeSet +import androidx.core.content.withStyledAttributes +import com.remax.visualnovel.R +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.ui.IconFontTextView +import com.remax.visualnovel.widget.uitoken.changeBackground +import com.remax.visualnovel.widget.uitoken.changeTextColor +import com.remax.visualnovel.widget.uitoken.changeTextFont + +/** + * create by wl on 2022/9/9 + * 筛选chip + */ +class RemovableChipView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : IconFontTextView(context, attrs, defStyleAttr) { + + companion object { + const val START = 1 + const val END = 2 + } + + private var iconDirection = END + private var iconFont: String = "" + + init { + context.withStyledAttributes(attrs, R.styleable.RemovableChipView) { + iconFont = getString(R.styleable.RemovableChipView_iconFont) ?: context.getString(R.string.icon_delete_2) + iconDirection = getInt(R.styleable.RemovableChipView_chipIconDirection, END) + } + + //初始化固定token + customViewToken.run { + textUITextToken = context.getString(R.string.txt_label_m) + textUIDisabledColorToken = context.getString(R.string.color_txt_disabled) + textUIPressedColorToken = context.getString(R.string.color_txt_primary_normal) + backgroundUIDisabledColorToken = context.getString(R.string.color_surface_element_disabled) + radiusToken = context.getString(R.string.radius_round) + } + changeTextFont(customViewToken) + setInActive() + setPaddingRelative(16.dp, 6.dp, 12.dp, 6.dp) + } + + /** + * 筛选状态 + */ + fun setActive() { + customViewToken.run { + strokeUIWidthToken = context.getString(R.string.border_s) + //normal + backgroundUIColorToken = context.getString(R.string.color_primary_normal) + strokeUIColorToken = context.getString(R.string.color_primary_variant_normal) + //disable + backgroundUIDisabledColorToken = context.getString(R.string.color_surface_element_disabled) + strokeUIDisabledColorToken = context.getString(R.string.color_outline_disabled) + //press + backgroundUIPressedColorToken = context.getString(R.string.color_primary_press) + strokeUIPressedColorToken = context.getString(R.string.color_primary_variant_press) + } + changeTextColor(customViewToken) + changeBackground(customViewToken) + when (iconDirection) { + START -> { + setIconFontDrawable( + startIconFont = iconFont, + iconPadding = 8, + iconSize = 16, + iconColorToken = if (isEnabled) context.getString(R.string.color_txt_primary_specialmap_normal) else context.getString(R.string.color_txt_secondary_disabled) + ) + } + + END -> { + setIconFontDrawable( + endIconFont = iconFont, + iconPadding = 8, + iconSize = 16, + iconColorToken = if (isEnabled) context.getString(R.string.color_txt_primary_specialmap_normal) else context.getString(R.string.color_txt_secondary_disabled) + ) + } + } + + } + + /** + * 无筛选状态 + */ + fun setInActive() { + customViewToken.run { + //无筛选状态将描边恢复默认 + strokeUIWidthToken = "" + strokeUIColorToken = "" + strokeUIDisabledColorToken = "" + strokeUIPressedColorToken = "" + //normal + textUIColorToken = context.getString(R.string.color_txt_primary_normal) + backgroundUIColorToken = context.getString(R.string.color_surface_element_normal) + //disable + backgroundUIDisabledColorToken = context.getString(R.string.color_surface_element_disabled) + //press + backgroundUIPressedColorToken = context.getString(R.string.color_surface_element_press) + } + changeTextColor(customViewToken) + changeBackground(customViewToken) + when (iconDirection) { + START -> { + setIconFontDrawable( + startIconFont = iconFont, + iconPadding = 8, + iconSize = 16, + iconColorToken = if (isEnabled) context.getString(R.string.color_txt_secondary_normal) else context.getString(R.string.color_txt_secondary_disabled) + ) + } + + END -> { + setIconFontDrawable( + endIconFont = iconFont, + iconPadding = 8, + iconSize = 16, + iconColorToken = if (isEnabled) context.getString(R.string.color_txt_secondary_normal) else context.getString(R.string.color_txt_secondary_disabled) + ) + } + } + + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/drawable/shape_dialog_hold_to_talk.xml b/VisualNovel/app/src/main/res/drawable/shape_dialog_hold_to_talk.xml new file mode 100644 index 0000000..17fa0d6 --- /dev/null +++ b/VisualNovel/app/src/main/res/drawable/shape_dialog_hold_to_talk.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/drawable/shape_oval_talk.xml b/VisualNovel/app/src/main/res/drawable/shape_oval_talk.xml new file mode 100644 index 0000000..cf4aea2 --- /dev/null +++ b/VisualNovel/app/src/main/res/drawable/shape_oval_talk.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/layout/activity_actor_chat.xml b/VisualNovel/app/src/main/res/layout/activity_actor_chat.xml index 3766bfc..6cc7196 100644 --- a/VisualNovel/app/src/main/res/layout/activity_actor_chat.xml +++ b/VisualNovel/app/src/main/res/layout/activity_actor_chat.xml @@ -35,7 +35,7 @@ /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VisualNovel/app/src/main/res/layout/activity_transaction.xml b/VisualNovel/app/src/main/res/layout/activity_transaction.xml new file mode 100644 index 0000000..9148574 --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/activity_transaction.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + diff --git a/VisualNovel/app/src/main/res/layout/activity_wallet.xml b/VisualNovel/app/src/main/res/layout/activity_wallet.xml new file mode 100644 index 0000000..5946c5a --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/activity_wallet.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VisualNovel/app/src/main/res/layout/chat_inputpanel.xml b/VisualNovel/app/src/main/res/layout/chat_inputpanel.xml index 5e6babd..5fbd689 100644 --- a/VisualNovel/app/src/main/res/layout/chat_inputpanel.xml +++ b/VisualNovel/app/src/main/res/layout/chat_inputpanel.xml @@ -15,7 +15,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" > - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/layout/dialog_flirting_retrieve.xml b/VisualNovel/app/src/main/res/layout/dialog_flirting_retrieve.xml new file mode 100644 index 0000000..6907356 --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/dialog_flirting_retrieve.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VisualNovel/app/src/main/res/layout/dialog_hold_to_talk.xml b/VisualNovel/app/src/main/res/layout/dialog_hold_to_talk.xml new file mode 100644 index 0000000..104cc80 --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/dialog_hold_to_talk.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + diff --git a/VisualNovel/app/src/main/res/layout/dialog_recharge.xml b/VisualNovel/app/src/main/res/layout/dialog_recharge.xml new file mode 100644 index 0000000..06737ac --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/dialog_recharge.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/layout/fragment_recharge.xml b/VisualNovel/app/src/main/res/layout/fragment_recharge.xml new file mode 100644 index 0000000..9e0369a --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/fragment_recharge.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VisualNovel/app/src/main/res/layout/item_chat_model.xml b/VisualNovel/app/src/main/res/layout/item_chat_model.xml new file mode 100644 index 0000000..688c100 --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/item_chat_model.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/layout/item_flirting_level.xml b/VisualNovel/app/src/main/res/layout/item_flirting_level.xml new file mode 100644 index 0000000..e28ed85 --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/item_flirting_level.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/layout/item_income.xml b/VisualNovel/app/src/main/res/layout/item_income.xml new file mode 100644 index 0000000..f1f130c --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/item_income.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/layout/item_income_header.xml b/VisualNovel/app/src/main/res/layout/item_income_header.xml new file mode 100644 index 0000000..929c66a --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/item_income_header.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VisualNovel/app/src/main/res/layout/item_recharge.xml b/VisualNovel/app/src/main/res/layout/item_recharge.xml new file mode 100644 index 0000000..f9cd3e6 --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/item_recharge.xml @@ -0,0 +1,45 @@ + + + + + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/layout/layout_simple_recyclerview_refresh.xml b/VisualNovel/app/src/main/res/layout/layout_simple_recyclerview_refresh.xml new file mode 100644 index 0000000..33e38ba --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/layout_simple_recyclerview_refresh.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/mipmap-xxhdpi/book_archive.webp b/VisualNovel/app/src/main/res/mipmap-xxhdpi/book_archive.webp new file mode 100644 index 0000000..c967895 Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxhdpi/book_archive.webp differ diff --git a/VisualNovel/app/src/main/res/mipmap-xxxhdpi/crush_level_bg.png b/VisualNovel/app/src/main/res/mipmap-xxxhdpi/crush_level_bg.png new file mode 100644 index 0000000..160bbe2 Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxxhdpi/crush_level_bg.png differ diff --git a/VisualNovel/app/src/main/res/mipmap-xxxhdpi/crush_level_bg_bottom.png b/VisualNovel/app/src/main/res/mipmap-xxxhdpi/crush_level_bg_bottom.png new file mode 100644 index 0000000..054aecd Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxxhdpi/crush_level_bg_bottom.png differ diff --git a/VisualNovel/app/src/main/res/mipmap-xxxhdpi/crush_level_bg_top.png b/VisualNovel/app/src/main/res/mipmap-xxxhdpi/crush_level_bg_top.png new file mode 100644 index 0000000..e5b2ac5 Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxxhdpi/crush_level_bg_top.png differ diff --git a/VisualNovel/app/src/main/res/mipmap-xxxhdpi/ic_transaction.png b/VisualNovel/app/src/main/res/mipmap-xxxhdpi/ic_transaction.png new file mode 100644 index 0000000..b4a17d6 Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxxhdpi/ic_transaction.png differ diff --git a/VisualNovel/app/src/main/res/raw/voice.json b/VisualNovel/app/src/main/res/raw/voice.json new file mode 100644 index 0000000..17fc4b6 --- /dev/null +++ b/VisualNovel/app/src/main/res/raw/voice.json @@ -0,0 +1 @@ +{"v":"5.6.10","fr":24,"ip":0,"op":16,"w":28,"h":28,"nm":"voice","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状图层 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2,14,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,40,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"t":16,"s":[100,40,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-11.875],[0,12.125]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[0]},{"t":16,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[100]},{"t":16,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":16,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"形状图层 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[26,14,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,40,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"t":16,"s":[100,60,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-11.875],[0,12.125]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[0]},{"t":16,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[100]},{"t":16,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":16,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"形状图层 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[14,14,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-11.875],[0,12.125]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[40]},{"t":16,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[60]},{"t":16,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":16,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/raw/voice_recording.json b/VisualNovel/app/src/main/res/raw/voice_recording.json new file mode 100644 index 0000000..38b0ac3 --- /dev/null +++ b/VisualNovel/app/src/main/res/raw/voice_recording.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":24,"ip":0,"op":20,"w":470,"h":80,"nm":"voice","ddd":0,"assets":[{"id":"comp_0","nm":"wave","fr":24,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"line1-1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[223.5,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[45]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[30]},{"t":20,"s":[45]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[55]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[70]},{"t":20,"s":[55]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"line5-1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[199,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[30]},{"t":20,"s":[15]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[85]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[70]},{"t":20,"s":[85]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"line4-1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[175.5,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[10]},{"t":20,"s":[30]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[90]},{"t":20,"s":[70]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"line2-1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[151,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":20,"s":[20]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[60]},{"t":20,"s":[80]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"line5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[127.5,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[10]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[20]},{"t":20,"s":[10]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[80]},{"t":20,"s":[90]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"line2-1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[103.5,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":20,"s":[20]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[100]},{"t":20,"s":[80]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"line4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[80,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[20]},{"t":20,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[80]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"line3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[55,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[45]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":20,"s":[45]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[55]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[100]},{"t":20,"s":[55]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"line2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[32,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[40]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[25]},{"t":20,"s":[40]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[75]},{"t":20,"s":[60]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"line1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[7.5,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":20,"s":[20]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[60]},{"t":20,"s":[80]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"wave","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[355,40,0],"ix":2,"l":2},"a":{"a":0,"k":[115,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":230,"h":80,"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"wave","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,40,0],"ix":2,"l":2},"a":{"a":0,"k":[115,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":230,"h":80,"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/raw/wave.json b/VisualNovel/app/src/main/res/raw/wave.json new file mode 100644 index 0000000..b1e13ed --- /dev/null +++ b/VisualNovel/app/src/main/res/raw/wave.json @@ -0,0 +1 @@ +{"v":"5.6.9","fr":24,"ip":0,"op":48,"w":124,"h":124,"nm":"合成 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Up","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[94,65,0],"to":[-15.667,0,0],"ti":[15.667,0,0]},{"t":48,"s":[0,65,0]}],"ix":2},"a":{"a":0,"k":[138,58,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0]],"o":[[0,0],[0,0],[23,0],[23,0],[23,0],[23,0],[23,0],[23,0],[0,0]],"v":[[138,58],[-138,58],[-138,-58],[-92,-48],[-46,-58],[0,-48],[46,-58],[92,-48],[138,-58]],"c":true},"ix":2},"nm":"path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"mergh path","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.314,0.671,0.499,1,0.239,0.502,1,1,0.165,0.333,0,0.8,0.5,0.5,1,0.2],"ix":9}},"s":{"a":0,"k":[0,-50],"ix":5},"e":{"a":0,"k":[0,20],"ix":6},"t":1,"nm":"Gradient Fill","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[138,58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"cp":true,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"New Shape 3","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[-37,67.25,0],"to":[13.805,0,0],"ti":[-19.24,0,0]},{"t":48,"s":[54.8,67.25,0]}],"ix":2},"a":{"a":0,"k":[115,58,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0]],"o":[[0,0],[0,0],[23,0],[23,0],[23,0],[23,0],[23,0],[23,0],[0,0]],"v":[[138,58],[-138,58],[-138,-58],[-92,-48],[-46,-58],[0,-48],[46,-58],[92,-48],[138,-58]],"c":true},"ix":2},"nm":"path2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"merge","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":2,"k":{"a":0,"k":[0,1,1,1,1,0,0,0],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[100,0],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":true},{"ty":"fl","c":{"a":0,"k":[0.5,0.5,0.5,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[138,58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"3","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"cp":true,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"G","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[72,64,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[51.03,89.746,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[218.321,205.245],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"path1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.455,0,0.204,0.499,0.727,0.057,0.359,1,1,0.114,0.514],"ix":9}},"s":{"a":0,"k":[-76.201,86.642],"ix":5},"e":{"a":0,"k":[89.749,-75.465],"ix":6},"t":1,"nm":"Gradient Fill 2","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-18.063,-2.243],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[143.92,71.442],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"cp":true,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"New Shape 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[-37,67.25,0],"to":[13.805,0,0],"ti":[-19.24,0,0]},{"t":48,"s":[54.8,67.25,0]}],"ix":2},"a":{"a":0,"k":[115,58,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0]],"o":[[0,0],[0,0],[23,0],[23,0],[23,0],[23,0],[23,0],[23,0],[0,0]],"v":[[138,58],[-138,58],[-138,-58],[-92,-48],[-46,-58],[0,-48],[46,-58],[92,-48],[138,-58]],"c":true},"ix":2},"nm":"path2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"merge","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.455,0,0.204,0.499,0.727,0.057,0.359,1,1,0.114,0.514],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[100,0],"ix":6},"t":1,"nm":"Gradient Fill 3","mn":"ADBE Vector Graphic - G-Fill","hd":true},{"ty":"fl","c":{"a":0,"k":[0.5,0.5,0.5,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[138,58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"3","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"cp":true,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/VisualNovel/gradle.properties b/VisualNovel/gradle.properties index 8d4bd9e..7f04421 100644 --- a/VisualNovel/gradle.properties +++ b/VisualNovel/gradle.properties @@ -36,8 +36,15 @@ kapt.include.compile.classpath=false android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false +# Test flags for improve compile speed +org.gradle.configureondemand=true + + + + + # business related KEYSTORE_PWD=visualNoval2025_remax_pw KEY_ALIAS=visualNoval_alias_remax KEY_PWD=visualNoval2025_remax_pw -storeFile=../visual_noval_keystore \ No newline at end of file +storeFile=../visual_noval_keystore