From b535a7667de3c7e8174325b9bc3f3529d9a27d06 Mon Sep 17 00:00:00 2001
From: renhaoting <370797079@qq.com>
Date: Tue, 21 Oct 2025 11:43:55 +0800
Subject: [PATCH] init project uploading
---
VisualNovel/.idea/AndroidProjectSystem.xml | 6 +
VisualNovel/app/src/main/AndroidManifest.xml | 27 ++
.../visualnovel/api/interceptor/util/AES.java | 76 ++++++
.../java/com/remax/visualnovel/app/AbsView.kt | 17 ++
.../activityresultapi/ActivityResultApiEx.kt | 230 ++++++++++++++++++
.../impl/ActivityLifecycleInitializer.kt | 101 ++++++++
.../visualnovel/entity/request/AIFeedback.kt | 34 +++
.../visualnovel/entity/response/AIDict.kt | 68 ++++++
.../visualnovel/entity/response/AIVoice.kt | 38 +++
.../visualnovel/entity/response/Album.kt | 50 ++++
.../repository/api/ActorsRepository.kt | 14 ++
.../ui/main/actor/ActorListFragment.kt | 78 ++++++
.../ui/main/actor/ActorListViewModel.kt | 26 ++
.../res/anim/act_slide_in_from_bottom.xml | 9 +
.../res/anim/act_slide_out_from_bottom.xml | 9 +
.../app/src/main/res/layout/activity_main.xml | 78 ++++++
.../src/main/AndroidManifest.xml | 5 +
.../src/main/AndroidManifest.xml | 1 +
.../src/main/AndroidManifest.xml | 5 +
.../viewbinding/base/ActivityBinding.kt | 38 +++
.../src/main/AndroidManifest.xml | 5 +
.../viewbinding/nonreflection/Activity.kt | 30 +++
22 files changed, 945 insertions(+)
create mode 100644 VisualNovel/.idea/AndroidProjectSystem.xml
create mode 100644 VisualNovel/app/src/main/AndroidManifest.xml
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/api/interceptor/util/AES.java
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/app/AbsView.kt
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/app/activityresultapi/ActivityResultApiEx.kt
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/app/initializer/impl/ActivityLifecycleInitializer.kt
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIFeedback.kt
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AIDict.kt
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AIVoice.kt
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Album.kt
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ActorsRepository.kt
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorListFragment.kt
create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorListViewModel.kt
create mode 100644 VisualNovel/app/src/main/res/anim/act_slide_in_from_bottom.xml
create mode 100644 VisualNovel/app/src/main/res/anim/act_slide_out_from_bottom.xml
create mode 100644 VisualNovel/app/src/main/res/layout/activity_main.xml
create mode 100644 VisualNovel/loadingstateview-ktx/src/main/AndroidManifest.xml
create mode 100644 VisualNovel/loadingstateview/src/main/AndroidManifest.xml
create mode 100644 VisualNovel/viewbinding-base/src/main/AndroidManifest.xml
create mode 100644 VisualNovel/viewbinding-base/src/main/java/com/dylanc/viewbinding/base/ActivityBinding.kt
create mode 100644 VisualNovel/viewbinding-nonreflection-ktx/src/main/AndroidManifest.xml
create mode 100644 VisualNovel/viewbinding-nonreflection-ktx/src/main/java/com/dylanc/viewbinding/nonreflection/Activity.kt
diff --git a/VisualNovel/.idea/AndroidProjectSystem.xml b/VisualNovel/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/VisualNovel/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/AndroidManifest.xml b/VisualNovel/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3cc5eb7
--- /dev/null
+++ b/VisualNovel/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/interceptor/util/AES.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/interceptor/util/AES.java
new file mode 100644
index 0000000..0ff7863
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/interceptor/util/AES.java
@@ -0,0 +1,76 @@
+package com.remax.visualnovel.api.interceptor.util;
+
+import java.nio.charset.StandardCharsets;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import timber.log.Timber;
+
+public class AES {
+
+ private static final String KEY_SUFFIX = "90e8kDQUIWpXg8Jp";
+
+ /**
+ * For a 128-bit AES key you need 16 bytes.
+ * For a 256-bit AES key you need 32 bytes.
+ */
+ public static String KEY;
+
+ /**
+ * IV length: must be 16 bytes long
+ */
+ public static final String IV = "sdf4ddfsFD86Vdf2";
+
+ private AES() {
+ }
+
+ public static void genKey(String token) {
+ //token加密最多截取前32位
+ String encodeStr = (token.length() > 32 ? token.substring(0, 32) : token) + KEY_SUFFIX;
+ KEY = Md5.encode(encodeStr).toUpperCase();
+ Timber.d("genKey: %s", KEY);
+ }
+
+ public static String encrypt(String data) {
+ return encrypt(data, KEY, IV);
+ }
+
+ public static String desEncrypt(String data) {
+ return desEncrypt(data, KEY, IV);
+ }
+
+ public static String encrypt(String data, String key, String initVector) {
+ try {
+ IvParameterSpec iv = new IvParameterSpec(initVector.getBytes(StandardCharsets.UTF_8));
+ SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
+
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
+ cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
+
+ byte[] encrypted = cipher.doFinal(data.getBytes());
+ return Base64.encode(encrypted);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ return data;
+ }
+ }
+
+ public static String desEncrypt(String data, String key, String initVector) {
+ try {
+ byte[] encrypted = Base64.decode(data);
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
+ SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(initVector.getBytes());
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);
+
+ byte[] original = cipher.doFinal(encrypted);
+
+ return new String(original);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return data;
+ }
+ }
+}
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/app/AbsView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/app/AbsView.kt
new file mode 100644
index 0000000..3a9ac54
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/app/AbsView.kt
@@ -0,0 +1,17 @@
+package com.remax.visualnovel.app
+
+
+import androidx.lifecycle.LifecycleOwner
+
+
+interface AbsView : LifecycleOwner{
+
+ fun showLoading()
+
+ fun hideLoading()
+
+ fun showToast(text: String?)
+
+ fun showToast(resId: Int)
+}
+
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/app/activityresultapi/ActivityResultApiEx.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/app/activityresultapi/ActivityResultApiEx.kt
new file mode 100644
index 0000000..d3b216a
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/app/activityresultapi/ActivityResultApiEx.kt
@@ -0,0 +1,230 @@
+package com.remax.visualnovel.app.activityresultapi
+
+import android.content.Intent
+import android.text.TextUtils
+import android.widget.Toast
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultCallback
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import com.alibaba.android.arouter.core.LogisticsCenter
+import com.alibaba.android.arouter.exception.NoRouteFoundException
+import com.alibaba.android.arouter.facade.Postcard
+import com.alibaba.android.arouter.facade.callback.InterceptorCallback
+import com.alibaba.android.arouter.facade.callback.NavigationCallback
+import com.alibaba.android.arouter.facade.enums.RouteType
+import com.alibaba.android.arouter.facade.service.DegradeService
+import com.alibaba.android.arouter.facade.service.InterceptorService
+import com.alibaba.android.arouter.facade.service.PretreatmentService
+import com.alibaba.android.arouter.launcher.ARouter
+import com.remax.visualnovel.app.initializer.impl.ActivityLifecycleInitializer
+
+/**
+ * 获取activityResultLauncher
+ */
+fun FragmentActivity.activityResultLauncher(): XActivityResultContract? {
+ val activityKey = intent.getStringExtra(ActivityLifecycleInitializer.KEY_ACTIVITY_RESULT_API)
+ return if (TextUtils.isEmpty(activityKey)) null else ActivityLifecycleInitializer.resultLauncherMap[activityKey]
+}
+
+/**
+ * 在Activity使用registerForActivityResult
+ */
+@JvmOverloads
+fun FragmentActivity.registerForActivityResult(
+ intent: Intent,
+ activityResultCallback: ActivityResultCallback? = null
+) {
+ activityResultLauncher()?.launch(intent, activityResultCallback)
+}
+
+/**
+ * 在Activity使用registerForActivityResult
+ */
+@JvmOverloads
+inline fun FragmentActivity.registerForActivityResult(
+ intentExtra: (intent: Intent) -> Unit = {},
+ activityResultCallback: ActivityResultCallback? = null
+) {
+ val intent = Intent(this, T::class.java)
+ intentExtra(intent)
+ registerForActivityResult(intent, activityResultCallback)
+}
+
+/**
+ * 在Fragment使用registerForActivityResult
+ */
+@JvmOverloads
+fun Fragment.registerForActivityResult(
+ intent: Intent,
+ activityResultCallback: ActivityResultCallback? = null
+) {
+ requireActivity().activityResultLauncher()?.launch(intent, activityResultCallback)
+}
+
+/**
+ * 在Fragment使用registerForActivityResult
+ */
+@JvmOverloads
+inline fun Fragment.registerForActivityResult(
+ intentExtra: (intent: Intent) -> Unit = {},
+ activityResultCallback: ActivityResultCallback? = null
+) {
+ val intent = Intent(this.requireActivity(), T::class.java)
+ intentExtra(intent)
+ registerForActivityResult(intent, activityResultCallback)
+}
+
+/**
+ * Activity中ARouter导航
+ * @param [activity] activity
+ * @param [activityResultCallback] 返回数据回调
+ */
+fun Postcard.navigation(
+ activity: FragmentActivity?,
+ activityResultCallback: ActivityResultCallback
+): Any? {
+ return navigation(activity, null, activityResultCallback)
+}
+
+/**
+ * Fragment中ARouter导航
+ * @param [fragment] Fragment
+ * @param [activityResultCallback] 返回数据回调
+ */
+fun Postcard.navigation(
+ fragment: Fragment?,
+ activityResultCallback: ActivityResultCallback
+): Any? {
+ return navigation(fragment?.requireActivity(), null, activityResultCallback)
+}
+
+
+/**
+ * Fragment中ARouter导航
+ * @param [fragment] Fragment
+ * @param [callback] 回调
+ * @param [activityResultCallback] 返回数据回调
+ */
+fun Postcard.navigation(
+ fragment: Fragment?,
+ callback: NavigationCallback?,
+ activityResultCallback: ActivityResultCallback
+): Any? {
+ return navigation(fragment?.requireActivity(), callback, activityResultCallback)
+}
+
+/**
+ * Activity中ARouter导航
+ * @param [activity] Fragment
+ * @param [callback] 回调
+ * @param [activityResultCallback] 返回数据回调
+ */
+fun Postcard.navigation(
+ activity: FragmentActivity?,
+ callback: NavigationCallback?,
+ activityResultCallback: ActivityResultCallback
+): Any? {
+ if (activity == null) {
+ return null
+ }
+ val _postcard = this
+ val pretreatmentService = ARouter.getInstance().navigation(PretreatmentService::class.java)
+ if (null != pretreatmentService && !pretreatmentService.onPretreatment(activity, this)) {
+ return null
+ }
+
+ try {
+ LogisticsCenter.completion(_postcard)
+ } catch (ex: NoRouteFoundException) {
+ debugLog(activity, path, group)
+ if (null != callback) {
+ callback.onLost(_postcard)
+ } else {
+ val degradeService = ARouter.getInstance().navigation(DegradeService::class.java)
+ degradeService?.onLost(activity, _postcard)
+ }
+
+ return null
+ }
+ callback?.onFound(_postcard)
+ val interceptorService = ARouter.getInstance().navigation(InterceptorService::class.java)
+ if (!isGreenChannel && interceptorService != null) {
+ interceptorService.doInterceptions(_postcard, object : InterceptorCallback {
+
+ override fun onContinue(postcard: Postcard?) {
+ _navigation(activity, _postcard, activityResultCallback)
+ }
+
+ override fun onInterrupt(exception: Throwable?) {
+ callback?.onInterrupt(_postcard)
+ }
+
+ })
+ } else {
+ return _navigation(activity, this, activityResultCallback)
+ }
+ return null
+}
+
+/**
+ * Debug模式下日志打印
+ */
+private fun debugLog(activity: FragmentActivity, path: String?, group: String?) {
+ if (ARouter.debuggable()) {
+ // Show friendly tips for user.
+ activity.runOnUiThread {
+ Toast.makeText(
+ activity,
+ "There's no route matched!Path = [${path}]Group = [${group}]",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+}
+
+private fun _navigation(
+ activity: FragmentActivity,
+ postcard: Postcard,
+ activityResultCallback: ActivityResultCallback,
+): Any? {
+
+ return when (postcard.type) {
+ RouteType.ACTIVITY -> {
+
+ val intent = Intent(activity, postcard.destination)
+ postcard.extras?.let { intent.putExtras(it) }
+ if (postcard.flags != -1) {
+ intent.flags = postcard.flags
+ }
+
+ postcard.action?.let { intent.action = postcard.action }
+ activity.runOnUiThread {
+ //适配动画
+ if ((postcard.enterAnim != -1 && postcard.exitAnim != -1)) {
+ activity.overridePendingTransition(postcard.enterAnim, postcard.exitAnim)
+ }
+ activity.registerForActivityResult(intent, activityResultCallback)
+ }
+ null
+ }
+ RouteType.PROVIDER -> {
+ postcard.provider
+ }
+ RouteType.FRAGMENT -> {
+ val fragmentMeta = postcard.destination
+ try {
+ val instance = fragmentMeta.getConstructor().newInstance()
+ if (instance is Fragment) {
+ instance.arguments = postcard.extras
+ }
+ instance
+ } catch (ex: Exception) {
+ ex.printStackTrace()
+ }
+ }
+ else -> {
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/app/initializer/impl/ActivityLifecycleInitializer.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/app/initializer/impl/ActivityLifecycleInitializer.kt
new file mode 100644
index 0000000..965e8d3
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/app/initializer/impl/ActivityLifecycleInitializer.kt
@@ -0,0 +1,101 @@
+package com.remax.visualnovel.app.initializer.impl
+
+import android.app.Activity
+import android.app.Application
+import android.content.Intent
+import android.os.Bundle
+import android.text.TextUtils
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultCaller
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.fragment.app.FragmentActivity
+import com.remax.visualnovel.app.activityresultapi.XActivityResultContract
+import com.remax.visualnovel.app.initializer.AppInitializers
+import com.remax.visualnovel.app.viewmodel.AppIMViewModel
+import com.remax.visualnovel.configs.NovelApplication
+import com.remax.visualnovel.utils.StatusBarUtils
+import timber.log.Timber
+
+/**
+ * Created by HJW on 2023/5/11
+ */
+class ActivityLifecycleInitializer(val application: Application, private val appIMViewModel: AppIMViewModel) : AppInitializers {
+
+ override fun init() {
+ application.registerActivityLifecycleCallbacks(AppLifecycleCallbacks(appIMViewModel))
+ }
+
+ inner class AppLifecycleCallbacks constructor(private val appIMViewModel: AppIMViewModel) : Application.ActivityLifecycleCallbacks {
+
+ private var activityCount = 0
+
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ if (activity is ActivityResultCaller) {
+ //生成一个Key
+ val activityKey = activity.javaClass.simpleName + System.currentTimeMillis()
+ //添加一个默认ActivityResultLauncher
+ val resultLauncher =
+ XActivityResultContract(activity, ActivityResultContracts.StartActivityForResult())
+ //把生成的Key放到intent中,作为每一个Activity的唯一标识
+ activity.intent.putExtra(KEY_ACTIVITY_RESULT_API, activityKey)
+ //存放到Map中
+ resultLauncherMap[activityKey] = resultLauncher
+ }
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+ activityCount++
+ }
+
+ override fun onActivityPaused(activity: Activity) {
+ NovelApplication.setCurrentActivity(null)
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ Timber.d("currentActivity:${activity::class.java.name}")
+ NovelApplication.setCurrentActivity(activity)
+ if (StatusBarUtils.statusBarHeight == 0) {
+ StatusBarUtils.getStatusBarHeight(activity)
+ }
+
+ //TODO - check pay info later
+ /*if (activity !is WelcomeActivity) {
+ GooglePayManager.checkProductDetails()
+ }*/
+ }
+
+ override fun onActivityStopped(activity: Activity) {
+ activityCount--
+ }
+
+ override fun onActivityDestroyed(activity: Activity) {
+ if (activity is FragmentActivity) {
+ val activityKey = activity.intent.getStringExtra(KEY_ACTIVITY_RESULT_API)
+ if (!TextUtils.isEmpty(activityKey)) {
+ resultLauncherMap[activityKey]?.unregister()
+ //移除activity的resultLauncher
+ resultLauncherMap.remove(activityKey)
+ }
+ }
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
+
+ }
+
+ }
+
+ companion object {
+ /**
+ * 保存ActivityResultApi的Key
+ */
+ const val KEY_ACTIVITY_RESULT_API = "activityResultApi"
+
+ /**
+ * 保存activity和Fragment的resultLauncher
+ */
+ val resultLauncherMap: MutableMap> =
+ mutableMapOf()
+
+ }
+}
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIFeedback.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIFeedback.kt
new file mode 100644
index 0000000..fe7a839
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/request/AIFeedback.kt
@@ -0,0 +1,34 @@
+package com.remax.visualnovel.entity.request
+
+/**
+ * Created by HJW on 2025/8/26
+ */
+data class AIFeedback(
+ /**
+ * AI的ID
+ */
+ val aiId: String,
+
+ /**
+ * 文本内容
+ */
+ val content: String?,
+
+ /**
+ * 消息ID
+ */
+ val messageId: String?,
+
+ /**
+ * 操作类型:空或0 无操作、1 赞、2 踩
+ */
+ val optType: Int
+) {
+
+ companion object {
+ const val NONE = 0
+ const val LIKE = 1
+ const val DISLIKE = 2
+ }
+
+}
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AIDict.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AIDict.kt
new file mode 100644
index 0000000..d27aa0e
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AIDict.kt
@@ -0,0 +1,68 @@
+package com.remax.visualnovel.entity.response
+
+import android.os.Parcelable
+import androidx.annotation.DrawableRes
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
+
+/**
+ * Created by HJW on 2025/7/18
+ */
+@Parcelize
+data class AIDict(
+ val roleDictList: List,
+ val characterDictList: List,
+ val imageStyleDictList: List,
+ val timbreDictList: List,
+) : Parcelable {
+
+ fun setInitSelect(character: Character?) {
+ val imageStyleCode = character?.aiUserExt?.imageStyleCode
+ val findImageStyle = imageStyleDictList.find { character -> character.code == imageStyleCode }
+ (findImageStyle ?: imageStyleDictList.firstOrNull())?.select = true
+
+ val roleCode = character?.roleCode
+ if (roleCode == null) {
+ roleDictList.firstOrNull()?.childDictList?.firstOrNull()?.select = true
+ } else {
+ roleDictList.forEach { roleChild ->
+ roleChild.childDictList?.forEach { role ->
+ role.select = role.code == roleCode
+ }
+ }
+ }
+
+ val characterCode = character?.characterCode
+ val findCharacter = characterDictList.find { character -> character.code == characterCode }
+ (findCharacter ?: characterDictList.firstOrNull())?.select = true
+
+ val tagCode = character?.tagCode
+ if (tagCode == null) {
+ characterDictList.firstOrNull()?.childDictList?.firstOrNull()?.select = true
+ } else {
+ characterDictList.forEach { characterChild ->
+ characterChild.childDictList?.forEach { tag ->
+ tag.select = tag.code == tagCode
+ }
+ }
+ }
+ }
+}
+
+@Parcelize
+data class AIDictItem(
+ val code: String,
+ val name: String,
+ val url: String?,
+ val prompt: String?,
+ val childDictList: List?,
+ var select: Boolean = false
+) : Parcelable {
+ @IgnoredOnParcel
+ @DrawableRes
+ var filterIcon: Int? = null
+
+ @IgnoredOnParcel
+ @DrawableRes
+ var selectBackgroundRes: Int? = null
+}
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AIVoice.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AIVoice.kt
new file mode 100644
index 0000000..901685f
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/AIVoice.kt
@@ -0,0 +1,38 @@
+package com.remax.visualnovel.entity.response
+
+import android.os.Parcelable
+import com.remax.visualnovel.entity.response.base.BaseVoice
+import kotlinx.parcelize.Parcelize
+
+/**
+ * Created by HJW on 2025/7/21
+ */
+
+@Parcelize
+data class AIVoice(
+ val type: Int, // 1 男 2 女
+ var select: Boolean = false,
+ var name: String?,
+ val description: String?,
+ val voiceType: String?,
+ val supportEmotions: String?,
+ val voiceText: String?,
+ var url: String?,
+ var filePath: String?,
+ val code: String?,
+ val pitchRate: Int?,
+ val speechRate: Int?
+) : Parcelable, BaseVoice() {
+
+ override fun filePathName(): String {
+ return filePath ?: ""
+ }
+
+ override fun id(): String {
+ return name ?: ""
+ }
+
+ override fun url(): String {
+ return url ?: ""
+ }
+}
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Album.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Album.kt
new file mode 100644
index 0000000..1786e3b
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/Album.kt
@@ -0,0 +1,50 @@
+package com.remax.visualnovel.entity.response
+
+import android.os.Parcelable
+import com.remax.visualnovel.constant.LockTypeConstant
+import com.remax.visualnovel.entity.model.base.BasePhoto
+import com.remax.visualnovel.manager.login.LoginManager
+import kotlinx.parcelize.Parcelize
+
+/**
+ * Created by HJW on 2025/7/14
+ */
+@Parcelize
+data class Album(
+ val albumId: Long,
+ var userId: String?,
+ var aiId: String?,
+ var imgUrl: String?,
+ var img1: String?,
+ var img2: String?,
+ var img3: String?,
+ var likedStatus: String?,
+ var likedCount: Long,
+ var width: Long,
+ var height: Long,
+ var unlockPrice: Long?,
+ val imgOrder: Int,
+ var lockStatus: String?,
+ var isDefault: Boolean?
+) : BasePhoto(), Parcelable {
+
+ fun isLike() = LIKED == likedStatus
+
+ /**
+ * 该图片是否解锁
+ */
+ fun isUnLock() = LoginManager.isMyself(userId) || LockTypeConstant.isUnLock(lockStatus)
+
+ /**
+ * 该图片是否上锁
+ */
+ fun isOpen() = (unlockPrice ?: 0L) <= 0L
+
+ override fun paramId(): Long = albumId
+
+ companion object {
+ const val CANCELED: String = "CANCELED"
+ const val LIKED: String = "LIKED"
+
+ }
+}
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ActorsRepository.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ActorsRepository.kt
new file mode 100644
index 0000000..f2496b8
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ActorsRepository.kt
@@ -0,0 +1,14 @@
+package com.remax.visualnovel.repository.api
+
+import com.remax.visualnovel.entity.response.Book
+import com.remax.visualnovel.repository.api.base.BaseRepository
+import com.remax.visualnovel.api.service.BookService
+import com.remax.visualnovel.entity.response.base.Response
+import javax.inject.Inject
+
+
+class ActorsRepository @Inject constructor(private val bookService: BookService) : BaseRepository() {
+ suspend fun getBooks(): Response {
+ return bookService.getBooks()
+ }
+}
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorListFragment.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorListFragment.kt
new file mode 100644
index 0000000..1373154
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorListFragment.kt
@@ -0,0 +1,78 @@
+package com.remax.visualnovel.ui.main.actor
+
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.fragment.app.viewModels
+import com.alibaba.android.arouter.facade.annotation.Route
+import com.alibaba.android.arouter.launcher.ARouter
+import com.dylanc.loadingstateview.BgColorType
+import com.remax.visualnovel.app.base.BaseBindingFragment
+import com.remax.visualnovel.databinding.FragmentMainActorBinding
+import com.remax.visualnovel.databinding.FragmentMainBookBinding
+import com.remax.visualnovel.ui.main.book.BookListViewModel
+import com.remax.visualnovel.utils.Routers
+import com.remax.visualnovel.widget.custom.TagItem
+import dagger.hilt.android.AndroidEntryPoint
+import kotlin.getValue
+
+
+
+@AndroidEntryPoint
+@Route(path = Routers.ROUTE_FRAG_ACTORLIST)
+class ActorListFragment : BaseBindingFragment() {
+
+ private val contactViewModel by viewModels()
+
+ override fun onCreated(bundle: Bundle?) {
+ setUI()
+ }
+
+ override fun backgroundColorType(): BgColorType {
+ return BgColorType.TRANSPARENT
+ }
+
+ private fun setUI() {
+ initTagLayout()
+
+ with(binding) {
+ tagFlowLayout
+ }
+ }
+
+ private fun initTagLayout() {
+ with(binding) {
+ // 模拟数据
+ val tags = listOf(
+ TagItem("1", "Youth"),
+ TagItem("2", "Lolita"),
+ TagItem("2", "Lolita2"),
+ TagItem("3", "Overbearing CEO ABCDEFG Overbearing CEO ABCDEFG Overbearing CEO ABCDEFG"),
+ TagItem("3", "ggggggggggg CEO ABCDEFG Overbearing CEO ABCDEFG Overbearing CEO ABCDEFG"),
+ TagItem("4", "Uncle"),
+ TagItem("5", "Character Status"),
+ TagItem("6", "Imouto"),
+ TagItem("7", "Fanwork"),
+ TagItem("8", "LastLine"),
+ )
+
+ tagFlowLayout.setTags(tags)
+
+ tagFlowLayout.setOnTagClickListener { tag ->
+ Toast.makeText(context, "Clicked: ${tag.text}", Toast.LENGTH_SHORT).show()
+ }
+
+ tagFlowLayout.setOnExpandStateChangeListener { isExpanded ->
+ Log.d("TagFlowLayout", "Expand state: $isExpanded")
+ }
+ }
+ }
+
+ companion object {
+ fun newInstance(): ActorListFragment {
+ return ARouter.getInstance().build(Routers.ROUTE_FRAG_ACTORLIST)
+ .navigation() as ActorListFragment
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorListViewModel.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorListViewModel.kt
new file mode 100644
index 0000000..323ebb8
--- /dev/null
+++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/main/actor/ActorListViewModel.kt
@@ -0,0 +1,26 @@
+package com.remax.visualnovel.ui.main.actor
+
+
+import com.remax.visualnovel.entity.response.Book
+import com.remax.visualnovel.app.viewmodel.base.BaseViewModel
+import com.remax.visualnovel.entity.response.base.Response
+import com.remax.visualnovel.repository.api.ActorsRepository
+import com.remax.visualnovel.repository.api.BooksRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import javax.inject.Inject
+
+
+
+@HiltViewModel
+class ActorListViewModel @Inject constructor(private val chatRepository: ActorsRepository) : BaseViewModel() {
+
+ private val _msgStatFlow = MutableSharedFlow>()
+ val msgStatFlow = _msgStatFlow.asSharedFlow()
+
+ suspend fun getMessageStat() {
+ _msgStatFlow.emit(chatRepository.getBooks())
+ }
+
+}
diff --git a/VisualNovel/app/src/main/res/anim/act_slide_in_from_bottom.xml b/VisualNovel/app/src/main/res/anim/act_slide_in_from_bottom.xml
new file mode 100644
index 0000000..5a3d3db
--- /dev/null
+++ b/VisualNovel/app/src/main/res/anim/act_slide_in_from_bottom.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/res/anim/act_slide_out_from_bottom.xml b/VisualNovel/app/src/main/res/anim/act_slide_out_from_bottom.xml
new file mode 100644
index 0000000..ca47db8
--- /dev/null
+++ b/VisualNovel/app/src/main/res/anim/act_slide_out_from_bottom.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VisualNovel/app/src/main/res/layout/activity_main.xml b/VisualNovel/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..3eefb4d
--- /dev/null
+++ b/VisualNovel/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VisualNovel/loadingstateview-ktx/src/main/AndroidManifest.xml b/VisualNovel/loadingstateview-ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..901e222
--- /dev/null
+++ b/VisualNovel/loadingstateview-ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/VisualNovel/loadingstateview/src/main/AndroidManifest.xml b/VisualNovel/loadingstateview/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cc947c5
--- /dev/null
+++ b/VisualNovel/loadingstateview/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/VisualNovel/viewbinding-base/src/main/AndroidManifest.xml b/VisualNovel/viewbinding-base/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..901e222
--- /dev/null
+++ b/VisualNovel/viewbinding-base/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/VisualNovel/viewbinding-base/src/main/java/com/dylanc/viewbinding/base/ActivityBinding.kt b/VisualNovel/viewbinding-base/src/main/java/com/dylanc/viewbinding/base/ActivityBinding.kt
new file mode 100644
index 0000000..15bcf1b
--- /dev/null
+++ b/VisualNovel/viewbinding-base/src/main/java/com/dylanc/viewbinding/base/ActivityBinding.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2020. Dylan Cai
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.dylanc.viewbinding.base
+
+import android.app.Activity
+import android.view.View
+import androidx.viewbinding.ViewBinding
+
+interface ActivityBinding {
+ val binding: VB
+ fun Activity.setContentViewWithBinding(): View
+}
+
+class ActivityBindingDelegate : ActivityBinding {
+ private lateinit var _binding: VB
+
+ override val binding: VB get() = _binding
+
+ override fun Activity.setContentViewWithBinding(): View {
+ _binding = ViewBindingUtil.inflateWithGeneric(this, layoutInflater)
+ setContentView(_binding.root)
+ return _binding.root
+ }
+}
\ No newline at end of file
diff --git a/VisualNovel/viewbinding-nonreflection-ktx/src/main/AndroidManifest.xml b/VisualNovel/viewbinding-nonreflection-ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..901e222
--- /dev/null
+++ b/VisualNovel/viewbinding-nonreflection-ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/VisualNovel/viewbinding-nonreflection-ktx/src/main/java/com/dylanc/viewbinding/nonreflection/Activity.kt b/VisualNovel/viewbinding-nonreflection-ktx/src/main/java/com/dylanc/viewbinding/nonreflection/Activity.kt
new file mode 100644
index 0000000..7b23cb5
--- /dev/null
+++ b/VisualNovel/viewbinding-nonreflection-ktx/src/main/java/com/dylanc/viewbinding/nonreflection/Activity.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2020. Dylan Cai
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.dylanc.viewbinding.nonreflection
+
+import android.view.LayoutInflater
+import androidx.activity.ComponentActivity
+import androidx.databinding.ViewDataBinding
+import androidx.viewbinding.ViewBinding
+import kotlin.LazyThreadSafetyMode.NONE
+
+fun ComponentActivity.binding(inflate: (LayoutInflater) -> VB, setContentView: Boolean = true) = lazy(NONE) {
+ inflate(layoutInflater).also { binding ->
+ if (setContentView) setContentView(binding.root)
+ if (binding is ViewDataBinding) binding.lifecycleOwner = this
+ }
+}