项目初步框架代码

This commit is contained in:
renhaoting 2025-10-21 13:32:05 +08:00
parent b535a7667d
commit bb87a3d138
522 changed files with 44342 additions and 0 deletions

17
VisualNovel/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
*/build/
**/build/
/captures
.externalNativeBuild
.cxx
local.properties

3
VisualNovel/.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<compositeConfiguration>
<compositeBuild compositeDefinitionSource="SCRIPT">
<builds>
<build path="$PROJECT_DIR$/buildSrc" name="buildSrc">
<projects>
<project path="$PROJECT_DIR$/buildSrc" />
</projects>
</build>
</builds>
</compositeBuild>
</compositeConfiguration>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/buildSrc" />
<option value="$PROJECT_DIR$/loadingstateview" />
<option value="$PROJECT_DIR$/loadingstateview-ktx" />
<option value="$PROJECT_DIR$/viewbinding-base" />
<option value="$PROJECT_DIR$/viewbinding-nonreflection-ktx" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.0.21" />
</component>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View File

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

1
VisualNovel/app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,283 @@
import com.android.build.api.dsl.ApplicationProductFlavor
plugins {
id("com.android.application")
kotlin("android")
id("kotlin-parcelize")
kotlin("kapt")
id("dagger.hilt.android.plugin")
//TODO - enable later: id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
}
android {
namespace = Version.applicationId
compileSdk = 36
defaultConfig {
applicationId = Version.applicationId
minSdk = Version.minSdk
targetSdk = Version.targetSdk
compileSdk = Version.targetSdk
versionCode = Version.versionCode
versionName = Version.versionName
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
setProperty("archivesBaseName", "${Version.applicationId}-build${Version.versionCode}")
multiDexEnabled = true
ndk {
abiFilters += listOf("armeabi", "armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
renderscriptTargetApi = 23
renderscriptSupportModeEnabled = true
}
kapt {
correctErrorTypes = true
arguments {
arg("AROUTER_MODULE_NAME", project.name)
}
}
hilt {
enableExperimentalClasspathAggregation = true
enableAggregatingTask = false
}
buildFeatures {
dataBinding = true
viewBinding = true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
signingConfigs {
val storeFileName = "storeFile"
val storePasswordName = "KEYSTORE_PWD"
val keyAliasName = "KEY_ALIAS"
val keyPasswordName = "KEY_PWD"
fun names(suffix: String = "") =
listOf(storeFileName + suffix, storePasswordName + suffix, keyAliasName + suffix, keyPasswordName + suffix)
create("release") {
storeFile = file(project.properties[names()[0]]?.toString() ?: "")
storePassword = project.properties[names()[1]] as? String?
keyAlias = project.properties[names()[2]] as? String?
keyPassword = project.properties[names()[3]] as? String?
}
getByName("debug") {
storeFile = file(project.properties[names()[0]]?.toString() ?: "")
storePassword = project.properties[names()[1]] as? String?
keyAlias = project.properties[names()[2]] as? String?
keyPassword = project.properties[names()[3]] as? String?
}
}
buildTypes {
release {
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
targetCompatibility(JavaVersion.VERSION_17)
sourceCompatibility(JavaVersion.VERSION_17)
}
val flavorDimensionName = "VisualNovel"
flavorDimensions.add(flavorDimensionName)
productFlavors {
fun ApplicationProductFlavor.buildConfigString(name: String, value: String) =
buildConfigField("String", name, "\"$value\"")
fun ApplicationProductFlavor.buildConfigBoolean(name: String, value: String) =
buildConfigField("Boolean", name, value)
create("novelTest") {
dimension = flavorDimensionName
signingConfig = signingConfigs.getByName("debug")
buildConfigString("HOST", "https://www.xxxxx.ai/")
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")
}
create("product") {
dimension = flavorDimensionName
signingConfig = signingConfigs.getByName("release")
buildConfigString("HOST", "https://test.xxxxx.ai/")
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")
}
}
}
dependencies {
implementation(fileTree("dir" to "libs", "include" to listOf("*.jar", "*.aar")))
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
coreLibraryDesugaring(Deps.desugar)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
//dagger hilt
implementation(Deps.hilt)
kapt(Deps.hiltAndroidCompiler)
kapt(Deps.hiltCompiler)
//Retrofit2
implementation(Deps.retrofit2)
implementation(Deps.retrofitAdapterRxjava2)
implementation(Deps.retrofit2ConverterGson)
implementation(Deps.okhttp3LoggingInterceptor)
// timber used for: recording logs
implementation(Deps.timber)
// tecent mmkv
implementation(Deps.mmkv)
implementation(Deps.material)
// gson
implementation(Deps.gson)
// viewpager2
implementation(Deps.viewpager2)
// for multiple dex support
implementation(Deps.multidex)
//google firebase
implementation(platform(Deps.firebaseBom))
implementation(Deps.firebaseMessageKtx)
implementation(Deps.firebaseAnalyticsKtx)
implementation(Deps.firebaseCrashlyticsKtx)
implementation(Deps.firebaseAuthKtx)
implementation(Deps.credentials)
implementation(Deps.credentialsAuth)
implementation(Deps.googleId)
// coroutine support
implementation(Deps.kotlinCoroutinesCore)
implementation(Deps.kotlinCoroutinesAndroid)
implementation(Deps.appcompat)
implementation(Deps.constraintlayout)
implementation(Deps.flexbox)
implementation(Deps.coreKtx)
implementation(Deps.activityKtx)
implementation(Deps.activityCompose)
implementation(Deps.fragmentKtx)
// vm and lifecycle
implementation(Deps.viewModel)
implementation(Deps.livedata)
implementation(Deps.lifecycleJava8)
implementation(Deps.lifecycleRuntime)
implementation(Deps.datastore)
//glide
implementation(Deps.glide)
kapt(Deps.glideCompiler)
implementation(Deps.glideTransformations)
implementation(Deps.glideWebpdecoder)
// eventbus
implementation(Deps.modularEventbus)
kapt(Deps.modularEventbusCompiler)
// indicator
implementation(Deps.magicIndicator)
// BlurView and Luban
implementation(Deps.Luban)
implementation(Deps.BlurView)
//ali aRouter
implementation(Deps.arouter)
kapt(Deps.arouterCompiler)
//Permission
implementation(Deps.permission)
// lottie
implementation(Deps.lottie)
// float window
implementation(Deps.easyFloat)
// guide
implementation(Deps.newbieGuide)
// refreshLayout
implementation(Deps.refreshLayout)
// apng
implementation(Deps.apng)
// softKey board
implementation(Deps.skbGlobal)
// baseRecyclerAdapter
implementation(Deps.brvah)
implementation(Deps.swipeMenuLayout)
implementation(Deps.BRV)
//banner
implementation(Deps.banner)
// spannable
implementation(Deps.spannablex)
// Media related libs
implementation(Deps.mp3Recorder)
implementation(Deps.photoView)
implementation(Deps.transition)
implementation(Deps.paging)
implementation(Deps.exoplayer)
implementation(Deps.subsamplingScaleImageView)
implementation(project(mapOf("path" to ":loadingstateview")))
implementation(project(mapOf("path" to ":loadingstateview-ktx")))
implementation(project(mapOf("path" to ":viewbinding-base")))
implementation(project(mapOf("path" to ":viewbinding-nonreflection-ktx")))
}

Binary file not shown.

21
VisualNovel/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package com.remax.visualnovel
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.remax.visualnovel", appContext.packageName)
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,202 @@
{
"color.context.vip.normal": "@GRA:$glo.deg.ltr,$glo.color.red.20,$glo.color.violet.20,$glo.color.mint.20",
"glo.color.orange.10": "$glo.color.orange.10",
"color.primary.normal": "$glo.color.magenta.50",
"color.primary.hover": "$glo.color.magenta.40",
"color.primary.press": "$glo.color.magenta.60",
"color.primary.disabled": "$color.surface.nest.disabled",
"color.primary.variant.normal": "$glo.color.magenta.40",
"color.primary.variant.hover": "$glo.color.magenta.30",
"color.primary.variant.press": "$glo.color.magenta.50",
"color.primary.variant.disabled": "$color.surface.nest.disabled",
"color.secondary.gradient.normal": "@GRA:$glo.deg.ltr,$glo.color.red.20,$glo.color.violet.20,$glo.color.mint.20",
"color.primary.gradient.normal": "@GRA:$glo.deg.ltr,$glo.color.magenta.30,$glo.color.purple.40",
"color.primary.gradient.hover": "@GRA:$glo.deg.ltr,$glo.color.magenta.20,$glo.color.purple.30",
"color.primary.gradient.press": "@GRA:$glo.deg.ltr,$glo.color.magenta.40,$glo.color.purple.50",
"color.primary.gradient.disabled": "$color.surface.nest.disabled",
"color.primary.onpic.normal": "$glo.color.violet.40,$glo.transparent.t85",
"color.primary.onpic.hover": "$glo.color.violet.30,$glo.transparent.t85",
"color.primary.onpic.press": "$glo.color.violet.50,$glo.transparent.t85",
"color.important.normal": "$glo.color.red.50",
"color.important.hover": "$glo.color.red.40",
"color.important.press": "$glo.color.red.60",
"color.important.disabled": "$color.surface.nest.disabled",
"color.important.variant.normal": "$glo.color.red.40",
"color.important.variant.hover": "$glo.color.red.30",
"color.important.variant.press": "$glo.color.red.50",
"color.important.variant.disabled": "$glo.color.blue.10,$glo.transparent.t25",
"color.important.gradient.normal": "@GRA:$glo.deg.ltr,$glo.color.orange.50,$glo.color.red.50",
"color.important.gradient.hover": "@GRA:$glo.deg.ltr,$glo.color.orange.40,$glo.color.red.40",
"color.important.gradient.press": "@GRA:$glo.deg.ltr,$glo.color.orange.60,$glo.color.red.60",
"color.important.gradient.disabled": "$color.surface.nest.disabled",
"color.important.onpic.normal": "$glo.color.red.50,$glo.transparent.t85",
"color.positive.normal": "$glo.color.mint.60",
"color.positive.hover": "$glo.color.mint.50",
"color.positive.press": "$glo.color.mint.70",
"color.positive.disabled": "$color.surface.nest.disabled",
"color.positive.variant.normal": "$glo.color.mint.40",
"color.positive.variant.hover": "$glo.color.mint.30",
"color.positive.variant.press": "$glo.color.mint.50",
"color.positive.variant.disabled": "$glo.color.blue.10,$glo.transparent.t25",
"color.positive.gradient.normal": "@GRA:$glo.deg.ltr,$glo.color.green.40,$glo.color.mint.60",
"color.positive.gradient.hover": "$glo.border.1",
"color.positive.gradient.press": "@GRA:$glo.deg.ltr,$glo.color.green.60,$glo.color.mint.70",
"color.positive.gradient.disabled": "$color.surface.nest.disabled",
"color.positive.onpic.normal": "$glo.color.mint.60,$glo.transparent.t85",
"color.warning.normal": "$glo.color.orange.50",
"color.warning.hover": "$glo.color.orange.40",
"color.warning.press": "$glo.color.orange.60",
"color.warning.disabled": "$color.surface.nest.disabled",
"color.warning.variant.normal": "$glo.color.orange.40",
"color.warning.variant.hover": "$glo.color.orange.30",
"color.warning.variant.press": "$glo.color.orange.50",
"color.warning.variant.disabled": "$glo.color.blue.10,$glo.transparent.t25",
"color.warning.gradient.normal": "@GRA:$glo.deg.ltr,$glo.color.yellow.40,$glo.color.orange.50",
"color.warning.gradient.hover": "@GRA:$glo.deg.ltr,$glo.color.yellow.30,$glo.color.orange.40",
"color.warning.gradient.press": "@GRA:$glo.deg.ltr,$glo.color.yellow.50,$glo.color.orange.60",
"color.warning.gradient.disabled": "$color.surface.nest.disabled",
"color.warning.onpic.normal": "$glo.color.orange.50,$glo.transparent.t85",
"color.emphasis.normal": "$glo.color.blue.40",
"color.emphasis.hover": "$glo.color.blue.30",
"color.emphasis.press": "$glo.color.blue.50",
"color.emphasis.disabled": "$color.surface.nest.disabled",
"color.emphasis.variant.normal": "$glo.color.blue.30",
"color.emphasis.variant.hover": "$glo.color.blue.20",
"color.emphasis.variant.press": "$glo.color.blue.40",
"color.emphasis.variant.disabled": "$glo.color.blue.10,$glo.transparent.t25",
"color.emphasis.grandient.normal": "@GRA:$glo.deg.ltr,$glo.color.sky.30,$glo.color.blue.40",
"color.emphasis.grandient.hover": "@GRA:$glo.deg.ltr,$glo.color.sky.20,$glo.color.blue.30",
"color.emphasis.grandient.press": "@GRA:$glo.deg.ltr,$glo.color.sky.40,$glo.color.blue.50",
"color.emphasis.grandient.disabled": "$color.surface.nest.disabled",
"color.emphasis.onpic.normal": "$glo.color.blue.40,$glo.transparent.t85",
"color.background.default": "$glo.color.grey.100",
"color.background.specialmap": "$glo.color.grey.100",
"color.background.district": "$glo.color.black,$glo.transparent.t30",
"color.surface.base.normal": "$glo.color.grey.80",
"color.surface.base.hover": "$glo.color.grey.70",
"color.surface.base.press": "$glo.color.grey.90",
"color.surface.base.disabled": "$glo.color.grey.90",
"color.surface.base.specialmap.normal": "$glo.color.grey.80",
"color.surface.base.specialmap.hover": "$glo.color.grey.70",
"color.surface.base.specialmap.press": "$glo.color.grey.90,$glo.transparent.t30",
"color.surface.base.specialmap.disabled": "$glo.color.white,$glo.transparent.t8",
"color.surface.float.normal": "$glo.color.grey.70",
"color.surface.float.hover": "$glo.color.grey.60",
"color.surface.float.press": "$glo.color.grey.80",
"color.surface.float.disabled": "$glo.color.grey.80",
"color.surface.top.normal": "$glo.color.black,$glo.transparent.t65",
"color.surface.top.hover": "$glo.color.black,$glo.transparent.t45",
"color.surface.top.press": "$glo.color.black,$glo.transparent.t85",
"color.surface.top.disabled": "$glo.color.black,$glo.transparent.t30",
"color.surface.district.normal": "$glo.color.purple.0,$glo.transparent.t4",
"color.surface.district.hover": "$glo.color.purple.0,$glo.transparent.t12",
"color.surface.district.press": "$glo.color.black,$glo.transparent.t25",
"color.surface.district.disabled": "$glo.color.black,$glo.transparent.t25",
"color.surface.nest.normal": "$glo.color.purple.0,$glo.transparent.t8",
"color.surface.nest.hover": "$glo.color.purple.0,$glo.transparent.t12",
"color.surface.nest.press": "$glo.color.purple.0,$glo.transparent.t4",
"color.surface.nest.disabled": "$glo.color.purple.0,$glo.transparent.t4",
"color.surface.element.normal": "$color.surface.nest.normal",
"color.surface.element.hover": "$color.surface.nest.hover",
"color.surface.element.press": "$color.surface.nest.press",
"color.surface.element.disabled": "$color.surface.nest.disabled",
"color.surface.element.dark.normal": "$glo.color.black,$glo.transparent.t65",
"color.surface.element.dark.hover": "$glo.color.black,$glo.transparent.t45",
"color.surface.element.dark.press": "$glo.color.black,$glo.transparent.t85",
"color.surface.element.dark.disabled": "$glo.color.black,$glo.transparent.t45",
"color.surface.element.light.normal": "$glo.color.white,$glo.transparent.t15",
"color.surface.element.light.hover": "$glo.color.white,$glo.transparent.t25",
"color.surface.element.light.press": "$glo.color.white,$glo.transparent.t8",
"color.surface.element.light.disabled": "$glo.color.white,$glo.transparent.t8",
"color.surface.white.normal": "$glo.color.white",
"color.surface.white.hover": "$glo.color.white,$glo.transparent.t85",
"color.surface.white.press": "$glo.color.white,$glo.transparent.t65",
"color.surface.white.disabled": "$glo.color.white,$glo.transparent.t45",
"color.outline.normal": "$glo.color.purple.0,$glo.transparent.t20",
"color.outline.hover": "$glo.color.purple.0,$glo.transparent.t30",
"color.outline.press": "$glo.color.purple.0,$glo.transparent.t8",
"color.outline.disabled": "$color.surface.element.disabled",
"color.overlay.primary": "$glo.color.violet.30,$glo.transparent.t30",
"color.overlay.gradient": "@GRA:$glo.deg.ttb,$glo.color.black&$glo.transparent.t0,$glo.color.black&$glo.transparent.t100",
"color.overlay.dark": "$glo.color.black,$glo.transparent.t65",
"color.overlay.base": "@GRA:$glo.deg.ttb,$glo.color.grey.80&$glo.transparent.t0,$glo.color.grey.80&$glo.transparent.t100",
"color.context.subscribe.normal": "@GRA:$glo.deg.ltr,$glo.color.purple.50,$glo.color.violet.50",
"color.context.subscribe.hover": "@GRA:$glo.deg.ltr,$glo.color.purple.30,$glo.color.violet.30",
"color.context.subscribe.press": "@GRA:$glo.deg.ltr,$glo.color.purple.70,$glo.color.violet.70",
"color.context.subscribe.disabled": "$color.surface.nest.disabled",
"color.context.legends.normal": "@GRA:$glo.deg.ltr,$glo.color.yellow.20,$glo.color.yellow.60",
"color.context.legends.hover": "@GRA:$glo.deg.ltr,$glo.color.yellow.10,$glo.color.yellow.40",
"color.context.legends.press": "@GRA:$glo.deg.ltr,$glo.color.yellow.60,$glo.color.yellow.90",
"color.context.legends.disabled": "$color.surface.nest.disabled",
"color.context.legends.variant.normal": "$glo.color.yellow.20",
"color.context.legends.variant.hover": "$glo.color.yellow.10",
"color.context.legends.variant.press": "$glo.color.yellow.40",
"color.context.legends.variant.disabled": "$color.surface.nest.disabled",
"color.txt.primary.normal": "$glo.color.white",
"color.txt.primary.hover": "$glo.color.magenta.30",
"color.txt.primary.press": "$glo.color.magenta.40",
"color.txt.primary.disabled": "$color.txt.disabled",
"color.txt.primary.specialmap.normal": "$glo.color.white",
"color.txt.primary.specialmap.hover": "$glo.color.white,$glo.transparent.t85",
"color.txt.primary.specialmap.press": "$glo.color.white,$glo.transparent.t65",
"color.txt.primary.specialmap.disabled": "$glo.color.white,$glo.transparent.t45",
"color.txt.secondary.normal": "$glo.color.grey.30",
"color.txt.secondary.hover": "$glo.color.magenta.30",
"color.txt.secondary.press": "$glo.color.magenta.40",
"color.txt.secondary.disabled": "$glo.color.grey.50",
"color.txt.tertiary.normal": "$glo.color.grey.40",
"color.txt.tertiary.hover": "$glo.color.grey.30",
"color.txt.tertiary": "$glo.color.grey.50",
"color.txt.tertiary.disabled": "$color.txt.disabled",
"color.txt.grass": "$glo.color.grass.40",
"color.txt.disabled": "$glo.color.grey.50",
"txt.display.xl": "$glo.font.family.display,$glo.font.size.64,$glo.font.weight.regular,$glo.font.lineheight.size64",
"txt.display.l": "$glo.font.family.display,$glo.font.size.36,$glo.font.weight.regular,$glo.font.lineheight.size36",
"txt.display.m": "$glo.font.family.display,$glo.font.size.24,$glo.font.weight.regular,$glo.font.lineheight.size24",
"txt.display.s": "$glo.font.family.display,$glo.font.size.16,$glo.font.weight.regular,$glo.font.lineheight.size16",
"txt.headline.l": "$glo.font.family.sys,$glo.font.size.36,$glo.font.weight.bold,$glo.font.lineheight.size36",
"txt.headline.m": "$glo.font.family.sys,$glo.font.size.24,$glo.font.weight.bold,$glo.font.lineheight.size24",
"txt.headline.s": "$glo.font.family.sys,$glo.font.size.20,$glo.font.weight.bold,$glo.font.lineheight.size20",
"txt.title.l": "$glo.font.family.sys,$glo.font.size.20,$glo.font.weight.semibold,$glo.font.lineheight.size20",
"txt.title.m": "$glo.font.family.sys,$glo.font.size.18,$glo.font.weight.semibold,$glo.font.lineheight.size18",
"txt.title.s": "$glo.font.family.sys,$glo.font.size.16,$glo.font.weight.semibold,$glo.font.lineheight.size16",
"txt.bodySemibold.l": "$glo.font.family.sys,$glo.font.size.16,$glo.font.weight.semibold,$glo.font.lineheight.size16",
"txt.bodySemibold.m": "$glo.font.family.sys,$glo.font.size.14,$glo.font.weight.semibold,$glo.font.lineheight.size14",
"txt.bodySemibold.s": "$glo.font.family.sys,$glo.font.size.12,$glo.font.weight.semibold,$glo.font.lineheight.size12",
"txt.body.l": "$glo.font.family.sys,$glo.font.size.16,$glo.font.weight.regular,$glo.font.lineheight.size16",
"txt.body.m": "$glo.font.family.sys,$glo.font.size.14,$glo.font.weight.regular,$glo.font.lineheight.size14",
"txt.body.s": "$glo.font.family.sys,$glo.font.size.12,$glo.font.weight.regular,$glo.font.lineheight.size12",
"txt.label.l": "$glo.font.family.sys,$glo.font.size.16,$glo.font.weight.medium,$glo.font.lineheight.size16",
"txt.label.m": "$glo.font.family.sys,$glo.font.size.14,$glo.font.weight.medium,$glo.font.lineheight.size14",
"txt.label.s": "$glo.font.family.sys,$glo.font.size.12,$glo.font.weight.medium,$glo.font.lineheight.size12",
"txt.numDisplay.xl": "$glo.font.family.numDisplay,$glo.font.size.48,$glo.font.weight.bold,$glo.font.lineheight.size48",
"txt.numDisplay.l": "$glo.font.family.numDisplay,$glo.font.size.36,$glo.font.weight.bold,$glo.font.lineheight.size36",
"txt.numDisplay.m": "$glo.font.family.numDisplay,$glo.font.size.24,$glo.font.weight.bold,$glo.font.lineheight.size24",
"txt.numDisplay.s": "$glo.font.family.numDisplay,$glo.font.size.20,$glo.font.weight.bold,$glo.font.lineheight.size20",
"txt.numMonotype.xl": "$glo.font.family.sys,$glo.font.size.20,$glo.font.weight.bold,$glo.font.lineheight.size20",
"txt.numMonotype.l": "$glo.font.family.sys,$glo.font.size.18,$glo.font.weight.bold,$glo.font.lineheight.size18",
"txt.numMonotype.m": "$glo.font.family.sys,$glo.font.size.16,$glo.font.weight.bold,$glo.font.lineheight.size16",
"txt.numMonotype.s": "$glo.font.family.sys,$glo.font.size.14,$glo.font.weight.medium,$glo.font.lineheight.size14",
"txt.numMonotype.xs": "$glo.font.family.sys,$glo.font.size.12,$glo.font.weight.medium,$glo.font.lineheight.size12",
"shadow.s": "@SHA:$glo.color.black,$glo.transparent.t15,0&0,4",
"shadow.m": "@SHA:$glo.color.black,$glo.transparent.t15,0&0,8",
"shadow.l": "@SHA:$glo.color.black,$glo.transparent.t15,0&0,16",
"radius.xs": "$glo.radius.4",
"radius.s": "$glo.radius.8",
"radius.m": "$glo.radius.12",
"radius.l": "$glo.radius.16",
"radius.xl": "$glo.radius.20",
"radius.xxl": "$glo.radius.24",
"radius.40": "$glo.radius.40",
"radius.42": "$glo.radius.42",
"radius.80": "$glo.radius.80",
"radius.round": "$glo.radius.round",
"radius.pill": "$glo.radius.round",
"border.divider": "$glo.border.half",
"border.s": "$glo.border.1",
"border.m": "$glo.border.2",
"border.l": "$glo.border.4"
}

View File

@ -0,0 +1,72 @@
package com.remax.visualnovel.api.factory
import androidx.core.net.toUri
import com.remax.visualnovel.BuildConfig
import com.remax.visualnovel.api.interceptor.GlobalInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object ServiceFactory {
/**
* 受信任的host列表
*/
private val hostNameList: List<String?> by lazy {
listOf(
BuildConfig.API_FROG,
BuildConfig.HOST
).map {
it.parseHost()
}
}
private fun String.parseHost() = this.toUri().host
private val retrofit: Retrofit by lazy {
createRetrofit()
}
private val okHttpClient: OkHttpClient by lazy {
createOkHttpClient()
}
/**
* 构建OkHttpClient对象
*/
private fun createOkHttpClient() = OkHttpClient.Builder()
.addInterceptor(GlobalInterceptor())
.callTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.hostnameVerifier { hostname, _ ->
hostname in hostNameList
}
.apply {
if (BuildConfig.DEBUG) {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
addNetworkInterceptor(interceptor)
}
}.build()
/**
* 构建Retrofit对象
*/
private fun createRetrofit() = Retrofit.Builder()
.baseUrl(BuildConfig.API_FROG)
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
inline fun <reified T> createService(): T = create(T::class.java)
fun <T> create(clazz: Class<T>): T {
return retrofit.create(clazz)
}
}

View File

@ -0,0 +1,89 @@
package com.remax.visualnovel.api.interceptor
import com.remax.visualnovel.BuildConfig
import com.remax.visualnovel.constant.AppConstant
import com.remax.visualnovel.manager.login.LoginManager
import com.remax.visualnovel.utils.AppUtils
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.asResponseBody
import okio.Buffer
import timber.log.Timber
import java.io.IOException
/**
* Created by HJW on 2022/10/17
*/
class GlobalInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val bodyStr = readBody(chain.request().body)
val emptyBody = "{}".toRequestBody("application/json;charset=utf-8".toMediaType())
val requestBody = if (bodyStr.isNotBlank()) request.body else emptyBody
// val platform = "ANDROID_${BuildConfig.VERSION_NAME}"
// val userAgent = String.format(
// "E-Pal/%s (%s; android %s)",
// BuildConfig.VERSION_NAME,
// Build.MODEL,
// Build.VERSION.RELEASE
// )
val headersBuilder = Headers.Builder()
.add("AUTH_TK", LoginManager.token ?: "")
.add("AUTH_DID", AppUtils.getAndroidID())
.add("platform", AppConstant.APP_CLIENT)
.add("versionNum", "100")
val headers = headersBuilder.build()
request = chain.request().newBuilder()
.headers(headers)
.post(requestBody ?: emptyBody)
.build()
val response = chain.proceed(request)
if (BuildConfig.DEBUG) {
try {
Timber.tag("发起请求")
Timber.d(
"""
我是开始分割线
${request.url}
入参
${readBody(requestBody)}
响应
${clone(response.body)?.string()}
我是结束分割线
""".trimIndent()
)
} catch (e: Exception) {
Timber.e("GlobalInterceptor request.exception : ${e.localizedMessage}}")
}
}
return response
}
private fun readBody(body: RequestBody?): String {
val buffer = Buffer()
body?.writeTo(buffer)
return buffer.readUtf8()
}
@Throws(IOException::class)
private fun clone(body: ResponseBody?): ResponseBody? {
val source = body?.source()
if (source?.request(Long.MAX_VALUE) == true) throw IOException("body too long!")
val bufferedCopy = source?.buffer?.clone()
return bufferedCopy?.asResponseBody(body.contentType(), body.contentLength())
}
}

View File

@ -0,0 +1,102 @@
package com.remax.visualnovel.api.interceptor.util;
import java.io.UnsupportedEncodingException;
public class Base64 {
private static final char[] ENCODE_CHARS = new char[]{'A', 'B', 'C', 'D',
'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '+', '/'};
private static final byte[] DECODE_CHARS = new byte[]{-1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1,
-1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1,
-1, -1};
public static String encode(byte[] data) {
StringBuffer buffer = new StringBuffer();
int length = data.length;
int indexx = 0;
int b1, b2, b3;
while (indexx < length) {
b1 = data[indexx++] & 0xff;
if (indexx == length) {
buffer.append(ENCODE_CHARS[b1 >>> 2]);
buffer.append(ENCODE_CHARS[(b1 & 0x3) << 4]);
buffer.append("==");
break;
}
b2 = data[indexx++] & 0xff;
if (indexx == length) {
buffer.append(ENCODE_CHARS[b1 >>> 2]);
buffer.append(ENCODE_CHARS[((b1 & 0x03) << 4) | ((b2 & 0xf0) >>> 4)]);
buffer.append(ENCODE_CHARS[(b2 & 0x0f) << 2]);
buffer.append("=");
break;
}
b3 = data[indexx++] & 0xff;
buffer.append(ENCODE_CHARS[b1 >>> 2]);
buffer.append(ENCODE_CHARS[((b1 & 0x03) << 4) | ((b2 & 0xf0) >>> 4)]);
buffer.append(ENCODE_CHARS[((b2 & 0x0f) << 2) | ((b3 & 0xc0) >>> 6)]);
buffer.append(ENCODE_CHARS[b3 & 0x3f]);
}
return buffer.toString();
}
public static byte[] decode(String str) throws UnsupportedEncodingException {
StringBuffer sb = new StringBuffer();
byte[] data = str.getBytes("US-ASCII");
int len = data.length;
int i = 0;
int b1, b2, b3, b4;
while (i < len) {
do {
b1 = DECODE_CHARS[data[i++]];
} while (i < len && b1 == -1);
if (b1 == -1) {
break;
}
do {
b2 = DECODE_CHARS[data[i++]];
} while (i < len && b2 == -1);
if (b2 == -1) {
break;
}
sb.append((char) ((b1 << 2) | ((b2 & 0x30) >>> 4)));
do {
b3 = data[i++];
if (b3 == 61) {
return sb.toString().getBytes("iso8859-1");
}
b3 = DECODE_CHARS[b3];
} while (i < len && b3 == -1);
if (b3 == -1) {
break;
}
sb.append((char) (((b2 & 0x0f) << 4) | ((b3 & 0x3c) >>> 2)));
do {
b4 = data[i++];
if (b4 == 61) {
return sb.toString().getBytes("iso8859-1");
}
b4 = DECODE_CHARS[b4];
} while (i < len && b4 == -1);
if (b4 == -1) {
break;
}
sb.append((char) (((b3 & 0x03) << 6) | b4));
}
return sb.toString().getBytes("iso8859-1");
}
}

View File

@ -0,0 +1,37 @@
package com.remax.visualnovel.api.interceptor.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Md5 {
private Md5() {
}
public static String encode(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
byte[] byteDigest = md.digest();
StringBuilder buf = new StringBuilder();
for (int b : byteDigest) {
int i = b;
if (i < 0) {
i += 256;
}
if (i < 16) {
buf.append("0");
}
buf.append(Integer.toHexString(i));
}
return buf.toString();
} catch (NoSuchAlgorithmException var6) {
var6.printStackTrace();
return "";
}
}
}

View File

@ -0,0 +1,13 @@
package com.remax.visualnovel.api.service
import com.remax.visualnovel.entity.response.Book
import com.remax.visualnovel.entity.response.base.Response
import retrofit2.http.POST
interface BookService {
@POST("/web/si/asi")
suspend fun getBooks(): Response<Book>
}

View File

@ -0,0 +1,33 @@
package com.remax.visualnovel.api.service
import com.remax.visualnovel.entity.response.base.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface DictService {
/**
* 获取聊天气泡字典
*/
/*@POST("/web/chat-set/get-chat-bubble-list")
suspend fun getChatBubbleList(@Body request: AIIDRequest): Response<List<ChatBubble>>*/
/**
* AI标签
*/
/*@POST("/web/get-ai-dict")
suspend fun getAIDict(): Response<AIDict>*/
/**
* 礼物字典
*/
/*@POST("/web/gift/dict-list")
suspend fun getGiftDict(@Body pageQuery: PageQuery = PageQuery(1).apply { page.ps = 100 }): Response<Pageable<Gift>>*/
/**
* chat模型
*/
/*@POST("/web/chat-model/dict-list")
suspend fun getAIChatModel(): Response<List<ChatModel>>*/
}

View File

@ -0,0 +1,30 @@
package com.remax.visualnovel.api.service
import com.remax.visualnovel.entity.request.CompleteUserInfoInput
import com.remax.visualnovel.entity.request.PlatformAccountVerifyDTO
import com.remax.visualnovel.entity.response.PlatformAccountVerify
import com.remax.visualnovel.entity.response.base.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface LoginService {
/**
* 重复昵称验证
*/
@POST("/web/user/nickname-check")
suspend fun checkUserNickname(@Body request: CompleteUserInfoInput): Response<Boolean>
/**
* 三方账号验证
*/
@POST("/web/third/login")
suspend fun platformThirdVerify(@Body request: PlatformAccountVerifyDTO): Response<PlatformAccountVerify>
@POST("/web/user/logout")
suspend fun logout(): Response<Any>
@POST("/web/user/complete-user-info")
suspend fun register(@Body request: CompleteUserInfoInput): Response<Any>
}

View File

@ -0,0 +1,34 @@
package com.remax.visualnovel.api.service
import com.remax.visualnovel.BuildConfig
import com.remax.visualnovel.entity.response.base.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface MessageService {
// /**
// * 删除会话
// */
// @POST(BuildConfig.API_COW + "/web/ai-message/del")
// suspend fun deleteConversation(@Body request: AIListRequest): Response<Any>
//
// /**
// * 送礼物
// */
// @POST("/web/ai-user-gift/send")
// suspend fun sendGift(@Body dto: SendGift): Response<Any>
//
// /**
// * 未读消息统计
// */
// @POST(BuildConfig.API_PIGEON + "/web/message/stat")
// suspend fun getMessageStat(): Response<MessageStatOutput>
//
// /**
// * 系统通知列表
// */
// @POST(BuildConfig.API_PIGEON + "/web/message/list")
// suspend fun getMessageList(@Body dto: PageQuery): Response<Pageable<MessageListOutput>>
}

View File

@ -0,0 +1,52 @@
package com.remax.visualnovel.api.service
import com.remax.visualnovel.BuildConfig
import com.remax.visualnovel.entity.request.CompleteUserInfoInput
import com.remax.visualnovel.entity.response.Character
import com.remax.visualnovel.entity.response.User
import com.remax.visualnovel.entity.response.base.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface UserService{
/**
* 签到
*/
@POST("/web/si/asi")
suspend fun signToday(): Response<Boolean>
/**
* 获取签到周期数据
*/
/*@POST("/web/si/list")
suspend fun getSignList(): Response<SignInRoundOutput>*/
/**
* 获取登录用户基础信息
*/
@POST("/web/user/base-info")
suspend fun getMyBaseInfo(): Response<User>
/**
* 获取me页面的ai列表
*/
@POST("/web/ai-user-search/base-list")
suspend fun getMyCharactersList(): Response<List<Character>>
/**
* 删除账号
*/
@POST("/web/user/del")
suspend fun deleteAccount(): Response<Any>
@POST("/web/user/edit-user-info")
suspend fun updateUserInfo(@Body request: CompleteUserInfoInput): Response<Any>
/**
* 获取云信appKey account token
*/
/*@POST(BuildConfig.API_PIGEON + "/web/im-user/get-account")
suspend fun getNimInfo(): Response<NimBean>*/
}

View File

@ -0,0 +1,45 @@
package com.remax.visualnovel.app
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.remax.visualnovel.app.initializer.utils.FirebaseHelper
import com.remax.visualnovel.app.viewmodel.AppGlobalViewModel
/**
* Created by HJW on 2022/9/21
* 监听整个app生命周期
*/
object ProcessLifecycleObserver : DefaultLifecycleObserver {
var isOnResume = false
private var refreshFirebaseTokenTime = 0L
private var appGlobalViewModel: AppGlobalViewModel? = null
fun setAppGlobalViewModel(appGlobalViewModel: AppGlobalViewModel) {
ProcessLifecycleObserver.appGlobalViewModel = appGlobalViewModel
}
/**
* APP在前台回调
*/
override fun onResume(owner: LifecycleOwner) {
isOnResume = true
val currTime = System.currentTimeMillis()
if (refreshFirebaseTokenTime != 0L && currTime - refreshFirebaseTokenTime > 60 * 1000) {
refreshFirebaseTokenTime = currTime
FirebaseHelper.getToken {
appGlobalViewModel?.updateTerminal(it)
}
}
}
/**
* APP进入后台回调
*/
override fun onPause(owner: LifecycleOwner) {
isOnResume = false
}
}

View File

@ -0,0 +1,37 @@
package com.remax.visualnovel.app.activityresultapi
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
class XActivityResultContract<I, O>(
activityResultCaller: ActivityResultCaller,
activityResultContract: ActivityResultContract<I, O>
) {
private var activityResultCallback: ActivityResultCallback<O>? = null
private val launcher: ActivityResultLauncher<I> =
activityResultCaller.registerForActivityResult(activityResultContract) {
activityResultCallback?.onActivityResult(it)
}
/**
* 启动
*/
fun launch(input: I, activityResultCallback: ActivityResultCallback<O>?) {
this.activityResultCallback = activityResultCallback
launcher.launch(input)
}
/**
* 注销
*/
fun unregister() {
launcher.unregister()
}
}

View File

@ -0,0 +1,144 @@
package com.remax.visualnovel.app.base
import android.content.res.Resources
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.remax.visualnovel.app.AbsView
import com.remax.visualnovel.app.widget.LoadingDialog
import com.remax.visualnovel.extension.fixedFontSize
import com.remax.visualnovel.extension.getBgColor
import com.remax.visualnovel.extension.isShouldHideKeyboard
import com.remax.visualnovel.extension.setStatusBarColor
import com.remax.visualnovel.extension.toast
import com.remax.visualnovel.extension.transitionFromAlpha
import com.remax.visualnovel.extension.transitionFromBottom
import com.remax.visualnovel.extension.withTransitionFromAlpha
import com.remax.visualnovel.extension.withTransitionFromBottom
import com.remax.visualnovel.utils.KeyboardUtils
import com.remax.visualnovel.utils.StatusBarUtils
import com.dylanc.loadingstateview.ActivityTransitionType
import com.dylanc.loadingstateview.BgColorType
import com.dylanc.loadingstateview.Decorative
import com.dylanc.loadingstateview.LoadingState
import com.dylanc.loadingstateview.LoadingStateDelegate
import com.dylanc.loadingstateview.OnReloadListener
import com.dylanc.viewbinding.base.ActivityBinding
import com.dylanc.viewbinding.base.ActivityBindingDelegate
/**
* Activity基类
*/
abstract class BaseBindingActivity<out VB : ViewBinding> : AppCompatActivity(), AbsView,
LoadingState by LoadingStateDelegate(), OnReloadListener, Decorative,
ActivityBinding<VB> by ActivityBindingDelegate() {
private val loadingDialog by lazy {
createLoadingDialog()
}
private fun createLoadingDialog(): LoadingDialog {
val dialog = LoadingDialog()
dialog.build(this)
return dialog
}
override fun getResources(): Resources {
return super.getResources().fixedFontSize(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentViewWithBinding()
binding.root.decorate(this, this)
StatusBarUtils.setStatusBarAndNavBarIsLight(this, false)
setStatusBarColor(backgroundColorType())
binding.root.setBackgroundColor(getBgColor(backgroundColorType()))
when (transitionType()) {
ActivityTransitionType.BOTTOM -> withTransitionFromBottom()
ActivityTransitionType.ALPHA -> withTransitionFromAlpha()
else -> {}
}
initView()
initData()
subscribeUi()
}
protected open fun backgroundColorType() = BgColorType.SPECIAL_MAP
protected abstract fun initView()
protected open fun subscribeUi() {}
protected open fun initData() {}
override fun onResume() {
super.onResume()
}
override fun onPause() {
super.onPause()
if (KeyboardUtils.isSoftInputVisible(this)) {
KeyboardUtils.hideSoftInput(this)
}
}
override fun finish() {
super.finish()
when (transitionType()) {
ActivityTransitionType.BOTTOM -> transitionFromBottom()
ActivityTransitionType.ALPHA -> transitionFromAlpha()
else -> {}
}
}
protected open var touchEventFun: ((MotionEvent) -> Unit)? = null
protected open fun touchHideKeyboardViewList(): List<View>? = null
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
touchEventFun?.let {
runCatching { ev?.run(it) }
}
if (touchHideKeyboardViewList() != null) {
if (ev?.action == MotionEvent.ACTION_DOWN && KeyboardUtils.isSoftInputVisible(this) && touchHideKeyboardViewList()?.any {
it.isShouldHideKeyboard(
ev
)
} == true) {
KeyboardUtils.hideSoftInput(this)
}
}
return super.dispatchTouchEvent(ev)
}
/**
* 页面开关方向
*/
protected open fun transitionType() = ActivityTransitionType.DEFAULT
override fun showLoading() {
if (!this.isDestroyed) {
runOnUiThread {
loadingDialog.show()
}
}
}
override fun hideLoading() {
if (!this.isDestroyed) {
runOnUiThread {
loadingDialog.dismiss()
}
}
}
override fun showToast(text: String?) {
toast(text)
}
override fun showToast(resId: Int) {
toast(resId)
}
}

View File

@ -0,0 +1,112 @@
package com.remax.visualnovel.app.base
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import com.dylanc.loadingstateview.BgColorType
import com.dylanc.loadingstateview.Decorative
import com.dylanc.loadingstateview.LoadingState
import com.dylanc.loadingstateview.LoadingStateDelegate
import com.dylanc.loadingstateview.OnReloadListener
import com.dylanc.viewbinding.base.FragmentBinding
import com.dylanc.viewbinding.base.FragmentBindingDelegate
import com.remax.visualnovel.app.AbsView
import com.remax.visualnovel.app.widget.LoadingDialog
import com.remax.visualnovel.extension.getBgColor
import com.remax.visualnovel.extension.toast
/**
* 基类Fragment
*/
abstract class BaseBindingFragment<VB : ViewBinding> : Fragment(), AbsView,
LoadingState by LoadingStateDelegate(), OnReloadListener, Decorative,
FragmentBinding<VB> by FragmentBindingDelegate() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
val view = createViewWithBinding(inflater, container).decorate(this, this)
view.setBackgroundColor(requireContext().getBgColor(backgroundColorType()))
return view
}
open fun backgroundColorType() = BgColorType.SPECIAL_MAP
private val loadingDialog by lazy {
createLoadingDialog()
}
private var isLoaded = false
private fun createLoadingDialog(): LoadingDialog {
val dialog = LoadingDialog()
dialog.build(requireActivity())
return dialog
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onCreated(arguments)
subscribeUi()
}
override fun onResume() {
super.onResume()
if (!isLoaded && !isHidden) {
lazyInit()
isLoaded = true
}
}
abstract fun onCreated(bundle: Bundle?)
open fun lazyInit() {}
open fun subscribeUi() {}
override fun showLoading() {
if (activity?.isDestroyed == false) {
activity?.runOnUiThread {
loadingDialog.show()
}
}
}
override fun hideLoading() {
if (activity?.isDestroyed == false) {
activity?.runOnUiThread {
loadingDialog.dismiss()
}
}
}
override fun showToast(text: String?) {
if (!isDetached) {
activity?.let {
if (!it.isDestroyed) {
it.toast(text)
}
}
}
}
override fun showToast(resId: Int) {
if (!isDetached) {
activity?.let {
if (!it.isDestroyed) {
it.toast(resId)
}
}
}
}
override fun onDetach() {
if (loadingDialog.getDialog().isShowing) {
loadingDialog.dismiss()
}
super.onDetach()
}
}

View File

@ -0,0 +1,13 @@
package com.remax.visualnovel.app.base
import androidx.viewpager.widget.ViewPager
import androidx.viewpager2.widget.ViewPager2
import net.lucode.hackware.magicindicator.buildins.commonnavigator.abs.CommonNavigatorAdapter
/**
* Created by HJW on 2023/7/18
*/
abstract class BaseCommonNavigatorAdapter(
open val viewPager2: ViewPager2?,
open val viewPager: ViewPager? = null
) : CommonNavigatorAdapter()

View File

@ -0,0 +1,30 @@
package com.remax.visualnovel.app.base.app
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.remax.visualnovel.app.viewmodel.AppGlobalViewModel
import com.remax.visualnovel.app.viewmodel.AppIMViewModel
import com.remax.visualnovel.repository.api.MessageRepository
import com.remax.visualnovel.repository.api.UserRepository
import javax.inject.Inject
/**
* 用于创建[AppIMViewModel, AppGlobalViewModel]等实例
*/
class AppViewModelFactory @Inject constructor(
private val application: Application,
private val userRepository: UserRepository,
private val messageRepository: MessageRepository
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
return when (modelClass) {
AppIMViewModel::class.java -> AppIMViewModel(application)
AppGlobalViewModel::class.java -> AppGlobalViewModel(application, userRepository, messageRepository)
else -> throw IllegalArgumentException("Unknown class $modelClass")
} as T
}
}

View File

@ -0,0 +1,11 @@
package com.remax.visualnovel.app.base.app
import android.app.Application
interface ApplicationProxy {
fun onCreate(application: Application)
fun onTerminate()
}

View File

@ -0,0 +1,22 @@
package com.remax.visualnovel.app.base.app
import android.app.Application
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
object CommonApplicationProxy : ApplicationProxy, ViewModelStoreOwner {
lateinit var application: Application
private set
override val viewModelStore = ViewModelStore()
override fun onCreate(application: Application) {
CommonApplicationProxy.application = application
}
override fun onTerminate() {
viewModelStore.clear()
}
}

View File

@ -0,0 +1,264 @@
package com.remax.visualnovel.app.delegate
import android.app.Activity
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.remax.visualnovel.R
import com.remax.visualnovel.databinding.LayoutToolbarBinding
import com.remax.visualnovel.extension.findActivityContext
import com.remax.visualnovel.extension.getBgColor
import com.remax.visualnovel.extension.getNavHeight
import com.remax.visualnovel.extension.setOnClick
import com.remax.visualnovel.extension.setSize
import com.remax.visualnovel.utils.StatusBarUtils
import com.remax.visualnovel.utils.spannablex.utils.dp
import com.remax.visualnovel.widget.ui.buttons.IconButtonView
import com.remax.visualnovel.widget.uitoken.changeTextColor
import com.remax.visualnovel.widget.uitoken.expend.dsl.expand
import com.remax.visualnovel.widget.uitoken.handleUIToken
import com.dylanc.loadingstateview.BaseToolbarViewDelegate
import com.dylanc.loadingstateview.NavBtnType
import com.dylanc.loadingstateview.ToolbarConfig
import com.dylanc.loadingstateview.toolbarExtras
var ToolbarConfig.titleTextColorToken: Int? by toolbarExtras()
var ToolbarConfig.titleTextColor: Int? by toolbarExtras()
var ToolbarConfig.titleTextAlpha: Float? by toolbarExtras()
/**
* 整个导航栏相关
*/
var ToolbarConfig.navBgColorToken: Int? by toolbarExtras()
var ToolbarConfig.navBgColor: Int? by toolbarExtras()
var ToolbarConfig.navBgAlpha: Float? by toolbarExtras() //一般和isFull = true 配合使用因为只有当navbar和content布局重叠时才需要滑动渐隐导航栏
var ToolbarConfig.navIsShow: Boolean? by toolbarExtras()
var ToolbarConfig.confirmEnabled: Boolean? by toolbarExtras()
var ToolbarConfig.isFull: Boolean? by toolbarExtras()
var ToolbarConfig.confirmContent: String? by toolbarExtras()
var ToolbarConfig.contractUSEnabled: Boolean? by toolbarExtras()
/**
* 返回键相关
*/
var ToolbarConfig.navBackColorToken: Int? by toolbarExtras()
var ToolbarConfig.navBackColor: Int? by toolbarExtras()
var ToolbarConfig.onNavBackClick: (() -> Unit)? by toolbarExtras()
var ToolbarConfig.onNavBackIconToken: Int? by toolbarExtras()
var ToolbarConfig.iconButtonType: IconButtonType? by toolbarExtras()
enum class IconButtonType {
ON_PIC, NORMAL
}
/**
* 公共导航栏
*/
class ToolbarViewDelegate : BaseToolbarViewDelegate() {
private lateinit var binding: LayoutToolbarBinding
private var context: Context? = null
override fun onCreateToolbar(inflater: LayoutInflater, parent: ViewGroup): View {
context = parent.context
binding = LayoutToolbarBinding.inflate(inflater, parent, false)
return binding.root
}
override fun onBindToolbar(config: ToolbarConfig) {
binding.apply {
val expendSize = 4.dp
tvTitle.text = config.title?.ifEmpty { " " } ?: " "
context?.run {
if (config.isFull == true) {
root.tag = "isFull"
(context?.findActivityContext() as? Activity)?.let {
StatusBarUtils.setTransparent(it)
navBg.setSize(height = it.getNavHeight())
}
navBg.alpha = 0f
tvTitle.alpha = 0f
}
//设置导航栏是否显示
config.navIsShow?.let { root.isVisible = it }
navBg.setBackgroundColor(getBgColor(config.colorType))
//设置导航栏颜色
config.navBgColorToken?.let { navBg.setBackgroundColor(handleUIToken(it)?.color ?: 0) }
config.navBgColor?.let { navBg.setBackgroundColor(it) }
//设置导航栏透明度
config.navBgAlpha?.let { navBg.alpha = it }
//设置标题颜色
config.titleTextColorToken?.let { tvTitle.setTextColor(handleUIToken(it)?.color ?: 0) }
config.titleTextColor?.let { tvTitle.setTextColor(it) }
//设置标题透明度
config.titleTextAlpha?.let { tvTitle.alpha = it }
//设置返回按钮颜色
config.navBackColorToken?.let { navBack.changeTextColor { textUIColorToken = getString(it) } }
config.navBackColor?.let { navBack.setTextColor(it) }
//设置confirm按钮是否可点
config.confirmEnabled?.let { rightConfirmBtn.isEnabled = it }
//设置confirm按钮的文字
config.confirmContent?.let { rightConfirmBtn.text = it }
//设置contract us按钮是否可点
config.contractUSEnabled?.let { contractUsBtn.isEnabled = it }
//设置iconfont按钮样式
when (config.iconButtonType) {
IconButtonType.ON_PIC -> {
navBack.setButtonStyle(buttonName = IconButtonView.NavButton_OnPic)
rightIconBtn1.setButtonStyle(buttonName = IconButtonView.NavButton_OnPic)
rightIconBtn2.setButtonStyle(buttonName = IconButtonView.NavButton_OnPic)
}
else -> {
navBack.setButtonStyle(buttonName = IconButtonView.NavButton)
rightIconBtn1.setButtonStyle(buttonName = IconButtonView.NavButton)
rightIconBtn2.setButtonStyle(buttonName = IconButtonView.NavButton)
}
}
navBack.expand(expendSize, expendSize)
}
val setBackClick = {
setOnClick(navBack) {
if (config.onNavBackClick != null) {
config.onNavBackClick?.invoke()
} else {
(context.findActivityContext() as? Activity)?.onBackPressed()
}
}
}
when (config.navBtnType) {
//navBack是返回按钮
NavBtnType.BACK -> {
navBack.setText(R.string.icon_arrow_left_border)
setBackClick.invoke()
navBack.isVisible = true
}
//navBack是关闭按钮
NavBtnType.ClOSE -> {
navBack.setText(R.string.icon_close)
setBackClick.invoke()
navBack.isVisible = true
}
//navBack是向下关闭按钮
NavBtnType.DOWN -> {
navBack.setText(R.string.icon_arrow_down_border)
setBackClick.invoke()
navBack.isVisible = true
}
//navBack是自定义按钮
NavBtnType.CUSTOM -> {
if (config.onNavBackIconToken != null) {
navBack.setText(config.onNavBackIconToken!!)
navBack.isVisible = true
setBackClick.invoke()
} else {
navBack.isInvisible = true
}
}
NavBtnType.INVISIBLE -> {
navBack.isInvisible = true
}
NavBtnType.NONE -> {
navBack.isVisible = false
}
}
with(contractUsBtn) {
if (config.contractUsBtnText != null) {
isVisible = true
val expandX = 24.dp
val expandY = 10.dp
expand(expandX, expandY)
if (config.contractUsBtnText != null) {
setText(config.contractUsBtnText ?: 0)
}
setOnClick(this) {
config.onContractUsBtnBlock?.invoke(this)
}
} else {
isVisible = false
setOnClickListener(null)
}
}
with(rightConfirmBtn) {
if (config.showConfirmBtn == true) {
isVisible = true
setOnClick(this) {
config.onConfirmBtnBlock?.invoke(this)
}
} else {
isVisible = false
setOnClickListener(null)
}
}
with(rightIconBtn1) {
if (config.showRightIconBtn1 != null) {
isVisible = true
setText(config.showRightIconBtn1!!)
expand(expendSize, expendSize)
setOnClick(this) {
config.onRightIconBtnBlock1?.invoke(this)
}
} else {
isVisible = false
setOnClickListener(null)
}
}
with(rightIconBtn2) {
if (config.showRightIconBtn2 != null) {
isVisible = true
setText(config.showRightIconBtn2!!)
changeTextColor {
textUIColorToken = if (config.rightIconColorBtn2 != null) {
context.getString(config.rightIconColorBtn2!!)
} else {
context.getString(R.string.color_txt_primary_normal)
}
}
expand(expendSize, expendSize)
setOnClick(this) {
config.onRightIconBtnBlock2?.invoke(this)
}
} else {
isVisible = false
setOnClickListener(null)
}
}
with(rightBtn) {
if (config.showRightBtn != null) {
isVisible = true
setText(config.showRightBtn!!)
setOnClick(this) {
config.onRightBtnBlock?.invoke(this)
}
} else {
isVisible = false
setOnClickListener(null)
}
}
}
}
}

View File

@ -0,0 +1,47 @@
package com.remax.visualnovel.app.di
import com.remax.visualnovel.api.factory.ServiceFactory
import com.remax.visualnovel.api.service.BookService
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.UserService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* hilt注入service请求
*/
@Module
@InstallIn(SingletonComponent::class)
object ApiServiceModule {
@Singleton
@Provides
fun userService() = create<UserService>()
@Singleton
@Provides
fun loginService() = create<LoginService>()
@Singleton
@Provides
fun messageService() = create<MessageService>()
@Singleton
@Provides
fun dictService() = create<DictService>()
@Singleton
@Provides
fun bookService() = create<BookService>()
private inline fun <reified T> create(): T {
return ServiceFactory.createService()
}
}

View File

@ -0,0 +1,31 @@
package com.remax.visualnovel.app.di
import androidx.lifecycle.ViewModelProvider
import com.remax.visualnovel.app.base.app.AppViewModelFactory
import com.remax.visualnovel.app.base.app.CommonApplicationProxy
import com.remax.visualnovel.app.viewmodel.AppGlobalViewModel
import com.remax.visualnovel.app.viewmodel.AppIMViewModel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* [AppIMViewModel] 提供者
*/
@InstallIn(SingletonComponent::class)
@Module
object ViewModelModule {
@Singleton
@Provides
fun provideAppIMViewModel(factory: AppViewModelFactory) =
ViewModelProvider(CommonApplicationProxy.viewModelStore, factory)[AppIMViewModel::class.java]
@Singleton
@Provides
fun provideAppGlobalViewModel(factory: AppViewModelFactory) =
ViewModelProvider(CommonApplicationProxy.viewModelStore, factory)[AppGlobalViewModel::class.java]
}

View File

@ -0,0 +1,18 @@
package com.remax.visualnovel.app.initializer
/**
* Created by HJW on 2023/5/11
*
* 启动类型
*/
enum class AppInitializerStartType {
/**
* 串行执行
*/
TYPE_SERIES,
/**
* 并发执行
*/
TYPE_PARALLEL,
}

View File

@ -0,0 +1,25 @@
package com.remax.visualnovel.app.initializer
import com.remax.visualnovel.app.initializer.di.AppInitializerPriority
/**
* Created by HJW on 2023/5/11
* 后续都需要依赖此框架
*/
interface AppInitializers {
/**
* 初始化代码
*/
fun init()
/**
* @return 初始化类型
*/
fun getStartType(): AppInitializerStartType = AppInitializerStartType.TYPE_SERIES
/**
* TYPE_SERIES 类型时权重越大越先执行
*/
fun widget(): Int = AppInitializerPriority.SERIES_1.priority
}

View File

@ -0,0 +1,41 @@
package com.remax.visualnovel.app.initializer
import android.app.Application
import android.util.Log
import com.remax.visualnovel.app.initializer.di.AppInitializerEntryPoint
import dagger.hilt.android.EntryPointAccessors
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Created by HJW on 2023/5/11
*/
class AppInitializersProvider @Inject constructor(private val application: Application) {
private val TAG = "AppInitializersProvider"
private val initializers: Set<AppInitializers> by lazy {
EntryPointAccessors.fromApplication(application, AppInitializerEntryPoint::class.java)
.getAppInitializers()
}
fun startInit() {
val seriesList = initializers.filter {
it.getStartType() == AppInitializerStartType.TYPE_SERIES
}.sortedByDescending { it.widget() }
val parallelList =
initializers.filter { it.getStartType() == AppInitializerStartType.TYPE_PARALLEL }
Log.d(TAG, "AppInitializersProvider 开始执行 并行")
parallelList.parallelStream().forEach {
MainScope().launch {
Log.d(TAG, "AppInitializersProvider $it 初始化模块协程开始执行: ${this.coroutineContext}")
it.init()
}
}
Log.d(TAG, "AppInitializersProvider 结束执行 并行")
seriesList.forEach {
it.init()
}
}
}

View File

@ -0,0 +1,18 @@
package com.remax.visualnovel.app.initializer.di
import com.remax.visualnovel.app.initializer.AppInitializers
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
/**
* Created by HJW on 2023/5/11
* 启动框架容器注解
*/
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AppInitializerEntryPoint {
fun getAppInitializers(): Set<AppInitializers>
}

View File

@ -0,0 +1,19 @@
package com.remax.visualnovel.app.initializer.di
/**
* Created by HJW on 2023/6/12
*/
enum class AppInitializerPriority(val priority: Int) {
/**
* 并发无优先级
*/
PARALLEL(1),
/**
* 串行数字越大优先级越高
*/
SERIES_1(1),
SERIES_2(2),
SERIES_3(3),
}

View File

@ -0,0 +1,42 @@
package com.remax.visualnovel.app.initializer.di
import android.app.Application
import com.remax.visualnovel.app.initializer.AppInitializers
import com.remax.visualnovel.app.initializer.impl.ActivityLifecycleInitializer
import com.remax.visualnovel.app.initializer.impl.LocalDataInitializer
import com.remax.visualnovel.app.initializer.impl.RouterInitializer
import com.remax.visualnovel.app.initializer.impl.SystemInitializer
import com.remax.visualnovel.app.initializer.impl.ThirdInitializer
import com.remax.visualnovel.app.initializer.impl.UserInitializer
import com.remax.visualnovel.app.viewmodel.AppGlobalViewModel
import com.remax.visualnovel.app.viewmodel.AppIMViewModel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
/**
* Created by HJW on 2023/5/11
*
* 添加容器注入
*/
@Module
@InstallIn(SingletonComponent::class)
object AppInitializersModule {
@Provides
fun provideAppInitializers(application: Application,
appIMViewModel: AppIMViewModel,
appGlobalViewModel: AppGlobalViewModel
): Set<AppInitializers> = setOf(
// FirebaseInitializer(application), TODO- add firebase support later
UserInitializer(application),
//JsInitializer(application),
LocalDataInitializer(application),
RouterInitializer(application),
ThirdInitializer(application),
ActivityLifecycleInitializer(application, appIMViewModel),
SystemInitializer(application, appGlobalViewModel)
)
}

View File

@ -0,0 +1,27 @@
package com.remax.visualnovel.app.initializer.impl
import android.app.Application
import com.remax.visualnovel.app.initializer.AppInitializers
import com.remax.visualnovel.app.initializer.di.AppInitializerPriority
import com.remax.visualnovel.utils.datastore.IDataStoreOwner
import com.tencent.mmkv.MMKV
/**
* Created by HJW on 2023/5/11
* 本地数据保存相关初始化
*/
class LocalDataInitializer(val application: Application) : AppInitializers {
override fun init() {
//初始化mmkv
val dir = application.filesDir.absolutePath + "/mmkv_epal"
MMKV.initialize(application, dir)
//初始化datastore
IDataStoreOwner.application = application
}
override fun widget(): Int {
return AppInitializerPriority.SERIES_3.priority
}
}

View File

@ -0,0 +1,26 @@
package com.remax.visualnovel.app.initializer.impl
import android.app.Application
import com.alibaba.android.arouter.launcher.ARouter
import com.remax.visualnovel.BuildConfig
import com.remax.visualnovel.app.initializer.AppInitializers
import com.remax.visualnovel.app.initializer.di.AppInitializerPriority
/**
* Created by HJW on 2023/5/11
* 路由相关初始化
*/
class RouterInitializer(val application: Application) : AppInitializers {
override fun init() {
if (BuildConfig.DEBUG) {
ARouter.openLog()
ARouter.openDebug()
}
ARouter.init(application)
}
override fun widget(): Int {
return AppInitializerPriority.SERIES_3.priority
}
}

View File

@ -0,0 +1,47 @@
package com.remax.visualnovel.app.initializer.impl
import android.app.ActivityManager
import android.app.Application
import android.content.Context
import android.os.Build
import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.ProcessLifecycleOwner
import com.remax.visualnovel.app.ProcessLifecycleObserver
import com.remax.visualnovel.app.initializer.AppInitializers
import com.remax.visualnovel.app.initializer.di.AppInitializerPriority
import com.remax.visualnovel.app.viewmodel.AppGlobalViewModel
/**
* Created by HJW on 2023/5/11
* 系统相关初始化
*/
class SystemInitializer(val application: Application, val appGlobalViewModel: AppGlobalViewModel) : AppInitializers {
override fun init() {
//监听application生命周期
ProcessLifecycleOwner.get().lifecycle.addObserver(ProcessLifecycleObserver)
ProcessLifecycleObserver.setAppGlobalViewModel(appGlobalViewModel)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
//启用矢量图兼容
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
var processName = ""
(application.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager)
?.runningAppProcesses
?.asSequence()
?.forEach { processInfo ->
if (processInfo.pid == android.os.Process.myPid()) {
processName = processInfo.processName
}
}
if (processName != application.packageName) {
WebView.setDataDirectorySuffix(processName)
}
}
}
override fun widget(): Int {
return AppInitializerPriority.SERIES_2.priority
}
}

View File

@ -0,0 +1,153 @@
package com.remax.visualnovel.app.initializer.impl
import android.app.Application
import android.content.Context
import android.net.http.HttpResponseCache
import android.os.Environment
import android.text.TextUtils
import com.chad.library.adapter.base.module.LoadMoreModuleConfig
import com.remax.visualnovel.BuildConfig
import com.remax.visualnovel.app.delegate.ToolbarViewDelegate
import com.remax.visualnovel.app.initializer.AppInitializers
import com.remax.visualnovel.app.widget.CustomLoadMoreView
import com.remax.visualnovel.app.widget.LoadMoreFooter
import com.remax.visualnovel.app.widget.RefreshHeader
import com.remax.visualnovel.utils.NotLoggingTree
import com.dylanc.loadingstateview.LoadingStateView
import com.github.boybeak.skbglobal.SoftKeyboardGlobal
import com.github.sahasbhop.apngview.ApngImageLoader
import com.lzf.easyfloat.EasyFloat
import com.pengxr.modular.eventbus.facade.launcher.IEventListener
import com.pengxr.modular.eventbus.facade.launcher.ModularEventBus
import com.pengxr.modular.eventbus.facade.template.BaseEvent
import com.scwang.smart.refresh.layout.SmartRefreshLayout
import timber.log.Timber
import java.io.File
import java.io.IOException
/**
* Created by HJW on 2023/5/11
* 三方库相关初始化
*/
class ThirdInitializer(val application: Application) : AppInitializers {
override fun init() {
ApngImageLoader.getInstance().init(application)
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
} else {
Timber.plant(NotLoggingTree())
}
SoftKeyboardGlobal.install(application, false)
LoadingStateView.setViewDelegatePool {
register(ToolbarViewDelegate())
}
/**
* SVGA http缓存
*/
val cacheDir = File(application.cacheDir, "http")
HttpResponseCache.install(cacheDir, 1024 * 1024 * 200)
EasyFloat.init(application, BuildConfig.DEBUG)
LoadMoreModuleConfig.defLoadMoreView = CustomLoadMoreView()
//设置全局默认配置(优先级最低,会被其他设置覆盖)
SmartRefreshLayout.setDefaultRefreshInitializer { _, layout -> //全局设置(优先级最低)
layout.setEnableLoadMore(false)
layout.setEnableAutoLoadMore(false)
layout.setEnableOverScrollDrag(true)
layout.setEnableOverScrollBounce(true)
layout.setEnableLoadMoreWhenContentNotFull(false)
layout.setEnableScrollContentWhenRefreshed(true)
layout.setHeaderMaxDragRate(3.0f)
}
SmartRefreshLayout.setDefaultRefreshHeaderCreator { context, layout -> //全局设置header
layout.setEnableHeaderTranslationContent(true)
RefreshHeader(context)
}
SmartRefreshLayout.setDefaultRefreshFooterCreator { context, layout -> //全局设置footer
layout.setEnableFooterTranslationContent(true)
LoadMoreFooter(context)
}
ModularEventBus.debug(BuildConfig.DEBUG)
.throwNullEventException(false)
.setEventListener(object : IEventListener {
override fun <T> onEventPost(eventName: String, event: BaseEvent<T>, data: T?) {
Timber.d("ModularEventBus发送事件 eventName: $eventName, event=$event data=$data")
}
})
//NIMClient.initV2(application, getSDKOptions(application))
initLocalNimString()
}
/*private fun getSDKOptions(context: Context): SDKOptions {
val options = SDKOptions()
with(options) {
this.appKey = "2d6abc076f434fc52320c7118de5fead"
enableV2CloudConversation = true
//在线多端同步未读数
sessionReadAck = true
// 采用异步加载SDK
asyncInitSDK = true
//禁止后台进程唤醒UI进程
disableAwake = true
useXLog = true
enableBackOffReconnectStrategy = true
checkManifestConfig = BuildConfig.DEBUG
loginCustomTag = ClientType.Android.toString()
notifyStickTopSession = true
mixPushConfig = MixPushConfig().apply {
fcmCertificateName = "VisualNovel"
}
if (BuildConfig.DEBUG) {
sdkStorageRootPath = getAppCacheDir(context) + "/nim"
}
}
return options
}*/
/**
* 配置 APP 保存图片/语音/文件/log等数据的目录
* 这里示例用SD卡的应用扩展存储目录
*/
private fun getAppCacheDir(context: Context): String? {
var storageRootPath: String? = null
try {
// SD卡应用扩展存储区(APP卸载后该目录下被清除用户也可以在设置界面中手动清除)请根据APP对数据缓存的重要性及生命周期来决定是否采用此缓存目录.
// 该存储区在API 19以上不需要写权限即可配置 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="18"/>
if (context.externalCacheDir != null) {
storageRootPath = context.externalCacheDir!!.canonicalPath
}
} catch (e: IOException) {
e.printStackTrace()
}
if (TextUtils.isEmpty(storageRootPath)) {
// SD卡应用公共存储区(APP卸载后该目录不会被清除下载安装APP后缓存数据依然可以被加载。SDK默认使用此目录),该存储区域需要写权限!
storageRootPath = Environment.getExternalStorageDirectory().toString() + "/" + context.packageName
}
return storageRootPath
}
private fun initLocalNimString() {
/*NimStrings.DEFAULT.apply {
status_bar_multi_messages_incoming = application.getString(R.string.status_bar_multi_messages_incoming)
status_bar_ticker_text = application.getString(R.string.status_bar_ticker_text)
status_bar_image_message = application.getString(R.string.status_bar_image_message)
status_bar_audio_message = application.getString(R.string.status_bar_audio_message)
status_bar_video_message = application.getString(R.string.status_bar_video_message)
status_bar_file_message = application.getString(R.string.status_bar_file_message)
status_bar_location_message = application.getString(R.string.status_bar_location_message)
status_bar_notification_message = application.getString(R.string.status_bar_notification_message)
status_bar_avchat_message = application.getString(R.string.status_bar_avchat_message)
status_bar_tip_message = application.getString(R.string.status_bar_tip_message)
status_bar_custom_message = application.getString(R.string.status_bar_custom_message)
status_bar_unsupported_message = application.getString(R.string.status_bar_unsupported_message)
status_bar_hidden_message_content = application.getString(R.string.status_bar_hidden_message_content)
status_bar_hidden_message_title = application.getString(R.string.status_bar_hidden_message_title)
}*/
}
}

View File

@ -0,0 +1,20 @@
package com.remax.visualnovel.app.initializer.impl
import android.app.Application
import com.remax.visualnovel.manager.login.LoginManager
import com.remax.visualnovel.app.initializer.AppInitializers
/**
* Created by HJW on 2023/5/11
* user相关初始化
*/
class UserInitializer(val application: Application) : AppInitializers {
/**
* 初始化必须放在本地数据存储框架初始化之后才行
*/
override fun init() {
LoginManager.init()
}
}

View File

@ -0,0 +1,29 @@
package com.remax.visualnovel.app.initializer.utils
import com.google.android.gms.tasks.Task
import com.google.firebase.messaging.FirebaseMessaging
import timber.log.Timber
/**
* Created by HJW on 2022/10/18
*/
object FirebaseHelper {
fun getToken(tokenCallback: ((String?) -> Unit)? = null) {
FirebaseMessaging.getInstance().token.addOnCompleteListener { task: Task<String?> ->
try {
Timber.d("firebase CompleteListener isSuccessful : %s", task.isSuccessful)
if (task.isSuccessful && task.result != null) {
val token = task.result
Timber.d("firebase token : %s", token)
tokenCallback?.invoke(token)
} else {
tokenCallback?.invoke(null)
}
} catch (e: Exception) {
Timber.d("firebase token exception: %s", e.localizedMessage)
tokenCallback?.invoke(null)
}
}
}
}

View File

@ -0,0 +1,37 @@
package com.remax.visualnovel.app.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.remax.visualnovel.manager.login.LoginManager
import com.remax.visualnovel.repository.api.MessageRepository
import com.remax.visualnovel.repository.api.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Created by HJW on 2022/11/1
*/
@HiltViewModel
class AppGlobalViewModel @Inject constructor(
application: Application,
private val userRepository: UserRepository,
private val messageRepository: MessageRepository
) : AndroidViewModel(application) {
/**
* 更新firebase设备码到后端推送用
*/
fun updateTerminal(terminalCode: String?) {
if (LoginManager.isLogin) {
viewModelScope.launch {
// userRepository.updateTerminal(terminalCode)
}
}
}
}

View File

@ -0,0 +1,32 @@
package com.remax.visualnovel.app.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.google.gson.Gson
import com.remax.visualnovel.utils.TimeUtils
/*import com.netease.nimlib.sdk.NIMClient
import com.netease.nimlib.sdk.Observer
import com.netease.nimlib.sdk.StatusCode
import com.netease.nimlib.sdk.msg.MsgServiceObserve
import com.netease.nimlib.sdk.msg.constant.MsgTypeEnum
import com.netease.nimlib.sdk.msg.model.IMMessage*/
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.abs
/**
* Created by HJW on 2022/10/18
* [Application]生命周期内的[AndroidViewModel]
*/
@HiltViewModel
class AppIMViewModel @Inject constructor(application: Application) : AndroidViewModel(application),
DefaultLifecycleObserver {
}

View File

@ -0,0 +1,16 @@
package com.remax.visualnovel.app.viewmodel.base
import com.remax.visualnovel.app.viewmodel.base.UserViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* Created by HJW on 2022/10/28
*
* 应用相关viewmodel的父类
*/
@HiltViewModel
open class AppViewModel @Inject constructor() : UserViewModel() {
}

View File

@ -0,0 +1,16 @@
package com.remax.visualnovel.app.viewmodel.base
import androidx.lifecycle.ViewModel
/**
* Created by HJW on 2022/10/27
*/
open class BaseViewModel : ViewModel() {
open fun onStart() {
}
open fun onDestroy() {
}
}

View File

@ -0,0 +1,28 @@
package com.remax.visualnovel.app.viewmodel.base
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* Created by HJW on 2022/10/28
*
* 钱包支付相关父类
*/
@HiltViewModel
open class PayViewModel @Inject constructor() : BaseViewModel() {
// @Inject
// lateinit var walletRepository: WalletRepository
//
// @Inject
// lateinit var payRepository: PayRepository
//
// private val _walletFlow = MutableSharedFlow<Response<Wallet>>()
// val walletFlow = _walletFlow.asSharedFlow()
//
// suspend fun getMyWallet(): Response<Wallet> {
// return walletRepository.getMyWallet().apply { _walletFlow.emit(this) }
// }
//
// suspend fun checkOut(tradeNo: String) = payRepository.checkOut(tradeNo)
}

View File

@ -0,0 +1,72 @@
package com.remax.visualnovel.app.viewmodel.base
import com.remax.visualnovel.R
import com.remax.visualnovel.app.base.app.CommonApplicationProxy
import com.remax.visualnovel.entity.response.User
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.repository.api.LoginRepository
import com.remax.visualnovel.repository.api.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
/**
* Created by HJW on 2022/10/28
*
* User相关viewmodel的父类
*/
@HiltViewModel
open class UserViewModel @Inject constructor() : BaseViewModel() {
@Inject
lateinit var userRepository: UserRepository
private val _userInfoFlow = MutableStateFlow<Response<User>>(Response())
val userInfoFlow: StateFlow<Response<User>> = _userInfoFlow.asStateFlow()
suspend fun getMyBaseInfo(): Response<User> {
return userRepository.getMyBaseInfo().also { _userInfoFlow.value = it }
}
/**
* 公共返回很多接口操作后需要再次请求用户数据
*/
protected suspend fun returnUserResponse(response: Response<*>): Response<User> {
return if (response.isApiSuccess) {
getMyBaseInfo()
} else {
ApiFailedResponse(response.errorCode, response.errorMsg)
}
}
//suspend fun getNimInfo() = userRepository.getNimInfo()
@Inject
lateinit var loginRepository: LoginRepository
/**
* 公共返回检查nickname是否存在
*/
suspend fun <T> checkNickname(
nickName: String?,
exUserId: String? = null,
apiCall: (suspend () -> Response<T>)? = null
): Response<T> {
val checkRes = loginRepository.checkUserNickname(nickName, exUserId)
return if (checkRes.isApiSuccess) {
if (checkRes.data == true) {
ApiFailedResponse("", CommonApplicationProxy.application.getString(R.string.nickname_exist_error))
} else {
apiCall?.invoke() ?: ApiSuccessResponse()
}
} else {
ApiFailedResponse(checkRes.errorCode, checkRes.errorMsg)
}
}
}

View File

@ -0,0 +1,40 @@
package com.remax.visualnovel.app.widget
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.chad.library.adapter.base.loadmore.BaseLoadMoreView
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import com.remax.visualnovel.R
import com.remax.visualnovel.extension.setSize
/**
* Created by HJW on 2020/9/18
*/
class CustomLoadMoreView(private val showLoading: Boolean = true) : BaseLoadMoreView() {
override fun getLoadComplete(holder: BaseViewHolder): View {
return holder.getView(R.id.load_more_load_complete_view)
}
override fun getLoadEndView(holder: BaseViewHolder): View {
return holder.getView(R.id.load_more_load_end_view)
}
override fun getLoadFailView(holder: BaseViewHolder): View {
return holder.getView(R.id.load_more_load_fail_view)
}
override fun getLoadingView(holder: BaseViewHolder): View {
val loadingView = holder.getView<View>(R.id.load_more_loading_view)
if (!showLoading) {
loadingView.setSize(height = 0)
}
return loadingView
}
override fun getRootView(parent: ViewGroup): View {
// 整个 LoadMore 布局
return LayoutInflater.from(parent.context).inflate(R.layout.view_load_more_common, parent, false)
}
}

View File

@ -0,0 +1,113 @@
package com.remax.visualnovel.app.widget
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import com.chad.library.adapter.base.BaseQuickAdapter
import com.drake.brv.PageRefreshLayout
import com.remax.visualnovel.R
import com.remax.visualnovel.configs.NovelApplication
import com.remax.visualnovel.databinding.LayoutEmptyBinding
import com.remax.visualnovel.extension.setOnClick
import com.remax.visualnovel.utils.spannablex.utils.dp
fun PageRefreshLayout.setEmptyText(
@StringRes emptyTextResId: Int = 0,
@DrawableRes emptyIcon: Int = R.mipmap.icon_new_empty,
topMargin: Int? = null,
) {
emptyLayout = R.layout.layout_empty
stateEnabled = true
onEmpty {
LayoutEmptyBinding.bind(this).apply {
if (emptyIcon != 0) {
ivEmpty.setImageResource(emptyIcon)
ivEmpty.isVisible = true
}
if (emptyTextResId != 0) {
tvEmpty.setText(emptyTextResId)
tvEmpty.isVisible = true
}
topMargin?.let {
root.run {
setPaddingRelative(paddingStart, topMargin.dp, paddingEnd, paddingBottom)
}
}
}
}
}
fun BaseQuickAdapter<*, *>.setMyEmptyView(
@StringRes emptyTextResId: Int = 0,
@DrawableRes emptyIcon: Int = R.mipmap.icon_new_empty,
topMargin: Int? = null,
@StringRes btnText: Int = 0,
btnInvoke: (() -> Unit)? = null
) {
NovelApplication.getCurrentActivity()?.let { activity ->
setEmptyView(
EmptyView(activity).createEmptyView(emptyTextResId, emptyIcon, topMargin, btnText, btnInvoke)
)
}
}
class EmptyView(private val context: Context) {
private val viewBind: LayoutEmptyBinding = LayoutEmptyBinding.inflate(LayoutInflater.from(context))
fun createEmptyView(
@StringRes emptyTextResId: Int = 0,
@DrawableRes emptyIcon: Int = R.mipmap.icon_new_empty,
topMargin: Int? = null,
@StringRes btnText: Int = 0,
btnInvoke: (() -> Unit)? = null
): View {
setEmptyView(emptyIcon, emptyTextResId)
topMargin?.let {
setMarginTop(it.dp)
}
if (btnText != 0) {
setBtn(btnText, btnInvoke)
}
return viewBind.root
}
private fun setEmptyView(@DrawableRes emptyIcon: Int = R.mipmap.icon_new_empty, @StringRes emptyTextResId: Int = 0): EmptyView {
with(viewBind) {
ivEmpty.isVisible = false
emptyIcon.takeIf { it != 0 }?.let {
ivEmpty.setImageResource(it)
ivEmpty.isVisible = true
}
tvEmpty.isVisible = false
emptyTextResId.takeIf { it != 0 }?.let {
tvEmpty.text = context.getString(emptyTextResId)
tvEmpty.isVisible = true
}
}
return this
}
private fun setBtn(@StringRes btnText: Int, btnInvoke: (() -> Unit)? = null): EmptyView {
with(viewBind.btnEmptyMessage) {
isVisible = true
text = context.getString(btnText)
setOnClick(this) {
btnInvoke?.invoke()
}
}
return this
}
private fun setMarginTop(topMargin: Int): EmptyView {
viewBind.root.run {
setPaddingRelative(paddingStart, topMargin, paddingEnd, paddingBottom)
}
return this
}
}

View File

@ -0,0 +1,92 @@
package com.remax.visualnovel.app.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.remax.visualnovel.R;
import com.scwang.smart.refresh.layout.api.RefreshKernel;
import com.scwang.smart.refresh.layout.api.RefreshLayout;
import com.scwang.smart.refresh.layout.constant.RefreshState;
import com.scwang.smart.refresh.layout.constant.SpinnerStyle;
import com.scwang.smart.refresh.layout.util.SmartUtil;
public class LoadMoreFooter extends LinearLayout implements com.scwang.smart.refresh.layout.api.RefreshFooter {
public LoadMoreFooter(Context context) {
this(context, null);
}
public LoadMoreFooter(Context context, @Nullable AttributeSet attrs) {
super(context, attrs, 0);
View.inflate(context, R.layout.load_more_loading_view, this);
setMinimumHeight(SmartUtil.dp2px(50));
}
@Override
public boolean setNoMoreData(boolean noMoreData) {
this.setVisibility(noMoreData ? View.GONE : View.VISIBLE);
return true;
}
@NonNull
@Override
public View getView() {
return this;
}
@NonNull
@Override
public SpinnerStyle getSpinnerStyle() {
return SpinnerStyle.Translate;
}
@Override
public void setPrimaryColors(int... colors) {
}
@Override
public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) {
}
@Override
public void onMoving(boolean isDragging, float percent, int offset, int height, int maxDragHeight) {
}
@Override
public void onReleased(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {
}
@Override
public void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {
}
@Override
public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) {
return 0;
}
@Override
public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {
}
@Override
public boolean isSupportHorizontalDrag() {
return false;
}
@Override
public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) {
}
}

View File

@ -0,0 +1,40 @@
package com.remax.visualnovel.app.widget
import android.content.Context
import com.remax.visualnovel.databinding.DialogLoadingBinding
import com.remax.visualnovel.widget.dialoglib.LBindingDialog
class LoadingDialog {
private lateinit var dialog: LBindingDialog<DialogLoadingBinding>
private var context: Context? = null
fun build(context: Context): LoadingDialog {
this.context = context
dialog = LBindingDialog(context,DialogLoadingBinding::inflate)
dialog.with()
.setCenter()
.setBgRadius(4)
.setWidth(80)
.setHeight(80)
dialog.setCancelable(false)
dialog.setCanceledOnTouchOutside(false)
return this
}
fun getDialog(): LBindingDialog<DialogLoadingBinding> {
return dialog
}
fun show() {
if (this::dialog.isInitialized) {
dialog.show()
}
}
fun dismiss() {
if (this::dialog.isInitialized) {
dialog.dismiss()
}
}
}

View File

@ -0,0 +1,109 @@
package com.remax.visualnovel.app.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.airbnb.lottie.LottieAnimationView;
import com.remax.visualnovel.R;
import com.scwang.smart.refresh.layout.api.RefreshKernel;
import com.scwang.smart.refresh.layout.api.RefreshLayout;
import com.scwang.smart.refresh.layout.constant.RefreshState;
import com.scwang.smart.refresh.layout.constant.SpinnerStyle;
import com.scwang.smart.refresh.layout.util.SmartUtil;
public class RefreshHeader extends LinearLayout implements com.scwang.smart.refresh.layout.api.RefreshHeader {
private final LottieAnimationView mProgressView;//刷新动画视图
public RefreshHeader(Context context) {
this(context, null);
}
public RefreshHeader(Context context, @Nullable AttributeSet attrs) {
super(context, attrs, 0);
setGravity(Gravity.CENTER);
mProgressView = new LottieAnimationView(context);
mProgressView.setAnimation(R.raw.single_ring);
mProgressView.setSafeMode(true);
mProgressView.setRepeatCount(-1);
addView(mProgressView, SmartUtil.dp2px(26), SmartUtil.dp2px(26));
setMinimumHeight(SmartUtil.dp2px(30));
}
@NonNull
@Override
public View getView() {
return this;
}
@NonNull
@Override
public SpinnerStyle getSpinnerStyle() {
return SpinnerStyle.Translate;
}
@Override
public void setPrimaryColors(int... colors) {
}
@Override
public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) {
}
@Override
public void onMoving(boolean isDragging, float percent, int offset, int height, int maxDragHeight) {
if (isDragging){
float currPer = percent / 3.0f;
if (currPer > 1.0f) currPer = 1.0f;
float frameF = mProgressView.getMaxFrame() * currPer;
int frame = (int) (frameF);
mProgressView.setFrame(frame);
mProgressView.invalidate();
}
}
@Override
public void onReleased(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {
mProgressView.setRepeatCount(-1);
mProgressView.setProgress(0f);
mProgressView.playAnimation();
}
@Override
public void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {
}
@Override
public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) {
mProgressView.postDelayed(new Runnable() {
@Override
public void run() {
mProgressView.cancelAnimation();
}
},1100);
return 1000;
}
@Override
public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {
}
@Override
public boolean isSupportHorizontalDrag() {
return false;
}
@Override
public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) {
}
}

View File

@ -0,0 +1,147 @@
package com.remax.visualnovel.app.widget.tips
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.widget.PopupWindow
import com.remax.visualnovel.R
import com.remax.visualnovel.databinding.PopwindowFeedsBackTipsBinding
import com.remax.visualnovel.entity.request.AIFeedback
import com.remax.visualnovel.extension.setOnClick
class TipsFeedbackWindow {
private var popupWindow: PopupWindow? = null
fun build(
context: Context,
optType: Int,
clickCallback: (Int, optType: Int) -> Unit
): TipsFeedbackWindow {
val view = LayoutInflater.from(context).inflate(R.layout.popwindow_feeds_back_tips, null)
popupWindow = PopupWindow(context).apply {
isFocusable = true
contentView = view
isOutsideTouchable = true
setBackgroundDrawable(ColorDrawable())
}
var currOptType = optType
PopwindowFeedsBackTipsBinding.bind(view).run {
fun changeOptType() {
val likeColorToken = context.getString(R.string.color_primary_variant_normal)
val normalColorToken = context.getString(R.string.color_txt_primary_normal)
val iconSize = 20
val iconPadding = 16
fun updateLikeDislikeIcons(
likeIconRes: String,
likeColor: String,
dislikeIconRes: String,
dislikeColor: String,
) {
like.setIconFontDrawable(
likeIconRes,
iconColorToken = likeColor,
iconSize = iconSize,
iconPadding = iconPadding
)
dislike.setIconFontDrawable(
dislikeIconRes,
iconColorToken = dislikeColor,
iconSize = iconSize,
iconPadding = iconPadding
)
}
when (currOptType) {
AIFeedback.LIKE -> updateLikeDislikeIcons(
context.getString(R.string.icon_post_recommend_fill),
likeColorToken,
context.getString(R.string.icon_post_notrecommend),
normalColorToken
)
AIFeedback.DISLIKE -> updateLikeDislikeIcons(
context.getString(R.string.icon_post_recommend),
normalColorToken,
context.getString(R.string.icon_post_notrecommend_fill),
likeColorToken
)
else -> updateLikeDislikeIcons(
context.getString(R.string.icon_post_recommend),
normalColorToken,
context.getString(R.string.icon_post_notrecommend),
normalColorToken
)
}
when (currOptType) {
AIFeedback.LIKE -> {
like.setIconFontDrawable(
context.getString(R.string.icon_post_recommend_fill),
iconColorToken = likeColorToken,
iconSize = iconSize,
iconPadding = iconPadding
)
dislike.setIconFontDrawable(
context.getString(R.string.icon_post_notrecommend),
iconColorToken = normalColorToken,
iconSize = iconSize,
iconPadding = iconPadding
)
}
}
}
changeOptType()
setOnClick(copy, like, dislike) {
when (this) {
copy -> {
clickCallback.invoke(0, currOptType)
popupWindow?.dismiss()
}
like -> {
currOptType = if (currOptType == AIFeedback.LIKE) AIFeedback.NONE else AIFeedback.LIKE
changeOptType()
clickCallback.invoke(1, currOptType)
}
dislike -> {
currOptType = if (currOptType == AIFeedback.DISLIKE) AIFeedback.NONE else AIFeedback.DISLIKE
changeOptType()
clickCallback.invoke(2, currOptType)
}
}
}
}
return this
}
fun showAsDropDown(view: View?) {
if (popupWindow!!.isShowing) {
popupWindow!!.dismiss()
} else {
popupWindow!!.showAsDropDown(view)
}
}
fun showAsDropDown(view: View?, xoff: Int, yoff: Int) {
if (popupWindow!!.isShowing) {
popupWindow!!.dismiss()
} else {
popupWindow!!.showAsDropDown(view, xoff, yoff)
}
}
fun showAtLocation(parent: View?, gravity: Int, x: Int, y: Int) {
if (popupWindow!!.isShowing) {
popupWindow!!.dismiss()
} else {
popupWindow!!.showAtLocation(parent, gravity, x, y)
}
}
}

View File

@ -0,0 +1,92 @@
package com.remax.visualnovel.app.widget.tips
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.annotation.StringRes
import com.remax.visualnovel.R
import com.remax.visualnovel.extension.setOnClick
import com.remax.visualnovel.extension.setSize
import com.remax.visualnovel.utils.spannablex.utils.dp
import com.remax.visualnovel.widget.ui.IconFontTextView
import com.remax.visualnovel.widget.uitoken.changeTextStyle
class TipsMoreWindow {
data class TipsMoreUIData(
@StringRes val titleRes: Int,
@StringRes val iconRes: Int,
)
private var popupWindow: PopupWindow? = null
fun build(
context: Context,
tipsData: List<TipsMoreUIData>?,
clickCallback: (TipsMoreUIData) -> Unit = {}
): TipsMoreWindow {
val view = LayoutInflater.from(context).inflate(R.layout.popwindow_btn_tips, null)
popupWindow = PopupWindow(context).apply {
isFocusable = true
contentView = view
isOutsideTouchable = true
setBackgroundDrawable(ColorDrawable())
}
val group = view.findViewById<LinearLayout>(R.id.group)
group.removeAllViews()
tipsData?.forEach { item ->
val iconView = IconFontTextView(context)
group.addView(iconView)
iconView.apply {
gravity = Gravity.START
setSize(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
setPadding(8.dp, 12.dp, 8.dp, 12.dp)
changeTextStyle {
textUITextToken = context.getString(R.string.txt_label_l)
textUIColorToken = context.getString(R.string.color_txt_primary_normal)
}
setIconFontDrawable(
startIconFont = context.getString(item.iconRes),
iconColorToken = context.getString(R.string.color_txt_primary_normal),
iconSize = 20,
iconPadding = 16
)
setText(item.titleRes)
setOnClick(this) {
popupWindow?.dismiss()
clickCallback(item)
}
}
}
return this
}
fun showAsDropDown(view: View?) {
if (popupWindow!!.isShowing) {
popupWindow!!.dismiss()
} else {
popupWindow!!.showAsDropDown(view)
}
}
fun showAsDropDown(view: View?, xoff: Int, yoff: Int) {
if (popupWindow!!.isShowing) {
popupWindow!!.dismiss()
} else {
popupWindow!!.showAsDropDown(view, xoff, yoff)
}
}
fun showAtLocation(parent: View?, gravity: Int, x: Int, y: Int) {
if (popupWindow!!.isShowing) {
popupWindow!!.dismiss()
} else {
popupWindow!!.showAtLocation(parent, gravity, x, y)
}
}
}

View File

@ -0,0 +1,76 @@
package com.remax.visualnovel.app.widget.tips;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupWindow;
import android.widget.TextView;
import com.remax.visualnovel.R;
import com.remax.visualnovel.extension.ViewExtKt;
public class TipsPopWindow {
private PopupWindow popupWindow;
public TipsPopWindow build(Context context, String content, int width) {
View view = LayoutInflater.from(context).inflate(R.layout.popwindow_tips, null);
popupWindow = new PopupWindow(context);
popupWindow.setFocusable(true);
popupWindow.setContentView(view);
if (width != ViewGroup.LayoutParams.MATCH_PARENT) {
ViewExtKt.setMargin(view, 0, 0, 0, 0);
TextView tv = view.findViewById(R.id.tvContent);
textWidth = tv.getPaint().measureText(content);
}
popupWindow.setWidth(width);
popupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
popupWindow.setOutsideTouchable(true);
popupWindow.setBackgroundDrawable(new ColorDrawable());
TextView tvContent = view.findViewById(R.id.tvContent);
tvContent.setText(content);
return this;
}
private float textWidth;
public float getTextWidth() {
return textWidth;
}
public TipsPopWindow build(Context context, String content) {
return build(context, content, ViewGroup.LayoutParams.MATCH_PARENT);
}
public TipsPopWindow build(Context context, int resId) {
return build(context, context.getString(resId));
}
public void showAsDropDown(View view) {
if (popupWindow.isShowing()) {
popupWindow.dismiss();
} else {
popupWindow.showAsDropDown(view);
}
}
public void showAsDropDown(View view, int xoff, int yoff) {
if (popupWindow.isShowing()) {
popupWindow.dismiss();
} else {
popupWindow.showAsDropDown(view, xoff, yoff);
}
}
public void showAtLocation(View parent, int gravity, int x, int y) {
if (popupWindow.isShowing()) {
popupWindow.dismiss();
} else {
popupWindow.showAtLocation(parent, gravity, x, y);
}
}
}

View File

@ -0,0 +1,60 @@
package com.remax.visualnovel.app.widget.tips
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.widget.PopupWindow
import android.widget.TextView
import androidx.annotation.StringRes
import com.remax.visualnovel.R
import com.remax.visualnovel.widget.ui.SwitchView
class TipsSwitchWindow {
private var popupWindow: PopupWindow? = null
fun build(
context: Context,
@StringRes tips: Int,
isChecked: Boolean = false,
switchCallback: (SwitchView,Boolean) -> Unit
): TipsSwitchWindow {
val view = LayoutInflater.from(context).inflate(R.layout.popwindow_switch_tips, null)
popupWindow = PopupWindow(context).apply {
isFocusable = true
contentView = view
isOutsideTouchable = true
setBackgroundDrawable(ColorDrawable())
}
view.findViewById<TextView>(R.id.tvContent).setText(tips)
view.findViewById<SwitchView>(R.id.switchView).run {
this.isChecked = isChecked
setPressChanged { switchCallback(this,it) }
}
return this
}
fun showAsDropDown(view: View?) {
if (popupWindow!!.isShowing) {
popupWindow!!.dismiss()
} else {
popupWindow!!.showAsDropDown(view)
}
}
fun showAsDropDown(view: View?, xoff: Int, yoff: Int) {
if (popupWindow!!.isShowing) {
popupWindow!!.dismiss()
} else {
popupWindow!!.showAsDropDown(view, xoff, yoff)
}
}
fun showAtLocation(parent: View?, gravity: Int, x: Int, y: Int) {
if (popupWindow!!.isShowing) {
popupWindow!!.dismiss()
} else {
popupWindow!!.showAtLocation(parent, gravity, x, y)
}
}
}

View File

@ -0,0 +1,60 @@
package com.remax.visualnovel.configs
import android.app.Activity
import android.content.res.Configuration
import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication
import com.remax.visualnovel.app.base.app.ApplicationProxy
import com.remax.visualnovel.app.base.app.CommonApplicationProxy
import com.remax.visualnovel.app.initializer.AppInitializersProvider
import com.remax.visualnovel.utils.StatusBarUtils
import com.tencent.mmkv.MMKV
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject
@HiltAndroidApp
class NovelApplication : MultiDexApplication() {
companion object {
private var currentActivity: WeakReference<Activity>? = null
fun setCurrentActivity(activity: Activity?) {
currentActivity = if (activity == null) null else WeakReference(activity)
}
fun getCurrentActivity(): Activity? {
return currentActivity?.get()
}
}
private val proxies = listOf<ApplicationProxy>(CommonApplicationProxy)
@Inject
lateinit var appInitializersProvider: AppInitializersProvider
override fun onCreate() {
super.onCreate()
MultiDex.install(this)
proxies.forEach { it.onCreate(this) }
appInitializersProvider.startInit()
}
override fun onTerminate() {
MMKV.onExit()
super.onTerminate()
proxies.forEach {
it.onTerminate()
}
}
// 系统资源配置发生更改回调,例如主题模式,需要重新刷新多语言
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
StatusBarUtils.resetNavBarHeight()
Timber.d("onConfigurationChanged ${newConfig.locales[0]}")
}
}

View File

@ -0,0 +1,13 @@
package com.remax.visualnovel.constant
/**
* Created by HJW on 2021/12/7
*/
class AppConstant {
companion object {
const val ANDROID = "android"
const val APP_CLIENT = "ANDROID"
}
}

View File

@ -0,0 +1,18 @@
package com.remax.visualnovel.constant
import com.remax.visualnovel.BuildConfig
class AppStatus {
companion object {
/**
* 是否是生产环境
*/
val isProduct
get() = BuildConfig.FLAVOR == "product"
}
}

View File

@ -0,0 +1,15 @@
package com.remax.visualnovel.constant
import androidx.annotation.StringRes
import com.remax.visualnovel.R
/**
* 性别枚举
*/
enum class Gender(val value: Int, @StringRes val txtRes: Int) {
MALE(0, R.string.male),
FEMALE(1, R.string.female),
NONCONFORMING(2, R.string.nonconforming),
OTHER(2, R.string.other),
}

View File

@ -0,0 +1,24 @@
package com.remax.visualnovel.constant
/**
* Created by HJW on 2023/8/25
*/
class LockTypeConstant {
companion object {
const val PUBLIC = 1
const val PRIVATE = 2
/**
* 图片公开
*/
fun isOpen(pubType: Int?) = pubType != PRIVATE
/**
* 图片是否解锁
*/
fun isUnLock(lockType: String?) = lockType != LOCK
const val LOCK = "LOCK"
const val UNLOCK = "UNLOCK"
}
}

View File

@ -0,0 +1,27 @@
package com.remax.visualnovel.constant
/**
* Created by HJW on 2025/7/21
*/
enum class StatusCode(val code:String) {
/**
* 登录过期
*/
TOKEN_EXPIRED("10050001"),
NO_ALBUM_PERMISSION("10010011"),
UNUSED_PURCHASE_TOKEN("1019"), //无效的支付凭据
AI_USER_NOT_EXIST("10010012"),
//余额不足
INSUFFICIENT_BALANCE("INSUFFICIENT_BALANCE"),
/**
* 前段自定义code
*/
UPLOAD_FILE_VIOLATION("112233")
}

View File

@ -0,0 +1,24 @@
package com.remax.visualnovel.entity.imbean.raw
import com.remax.visualnovel.entity.model.base.BasePhoto
/**
* Created by HJW on 2025/8/21
*/
data class CustomAlbumData(
var url: String,
var width: Int,
var height: Int,
var unlockPrice: Long?,
val albumId: Long?
) : BasePhoto() {
val type = CustomRawData.IMAGE
var messageServerId: String? = null
override fun paramId(): Long {
return albumId ?: url.hashCode().toLong()
}
}

View File

@ -0,0 +1,41 @@
package com.remax.visualnovel.entity.imbean.raw
import com.remax.visualnovel.R
import com.remax.visualnovel.app.base.app.CommonApplicationProxy
import com.remax.visualnovel.widget.imagepicker.utils.PDateUtil
/**
* Created by HJW on 2025/8/21
*/
data class CustomCallData(
val type: String,
val callType: String,
// 通话总时长
val duration: Long,
) {
companion object {
const val CALL_CANCEL = "CALL_CANCEL"
const val CALL_END = "CALL_END"
}
val callTxt: String?
get() = when (callType) {
CALL_CANCEL -> {
CommonApplicationProxy.application.getString(R.string.call_canceled)
}
CALL_END -> {
"${CommonApplicationProxy.application.getString(R.string.call_duration)} ${
PDateUtil.formatTime(
CommonApplicationProxy.application,
duration
)
}"
}
else -> {
null
}
}
}

View File

@ -0,0 +1,12 @@
package com.remax.visualnovel.entity.imbean.raw
/**
* Created by HJW on 2025/8/23
*/
data class CustomGiftData(
val giftId: Int,
val giftName: String,
val giftIcon: String,
val giftNum: Int,
val title: String,
)

View File

@ -0,0 +1,17 @@
package com.remax.visualnovel.entity.imbean.raw
/**
* Created by HJW on 2025/8/27
*/
data class CustomLevelChangeData(
val type: String,
val title: String,
val heartbeatLevel: String,
val heartbeatLevelName: String,
val heartbeatLevelNum: Int,
val heartbeatVal: Double,
) {
val isLevelUp: Boolean
get() = type == CustomRawData.HEARTBEAT_LEVEL_UP
}

View File

@ -0,0 +1,36 @@
package com.remax.visualnovel.entity.imbean.raw
/**
* Created by HJW on 2025/8/21
*/
data class CustomRawData(
val type: String,
val url: String,
val width: Int,
val height: Int
) {
companion object {
// 发送图片
const val IMAGE = "IMAGE"
// IM发送礼物
const val GIFT = "IM_SEND_GIFT"
//心动等级升级
const val HEARTBEAT_LEVEL_UP = "HEARTBEAT_LEVEL_UP"
//心动等级降级
const val HEARTBEAT_LEVEL_DOWN = "HEARTBEAT_LEVEL_DOWN"
// IM通话结束
const val CALL = "CALL"
// IM通话中分数变化
const val VOICE_CHAT_EMOTION_SCORE = "VOICE_CHAT_EMOTION_SCORE"
//余额不足 关闭语音电话
const val INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE"
}
}

View File

@ -0,0 +1,8 @@
package com.remax.visualnovel.entity.imbean.raw
/**
* Created by HJW on 2025/8/27
*/
data class CustomScoreData(
val score: Double
)

View File

@ -0,0 +1,19 @@
package com.remax.visualnovel.entity.model
import com.remax.visualnovel.widget.imageviewer.adapter.ItemType
import com.remax.visualnovel.widget.imageviewer.core.Photo
data class MyImgData(
val viewerId: Long,
val url: String?,
val subsampling: Boolean = false
) : Photo {
override fun id(): Long = viewerId
override fun itemType(): Int {
return when {
subsampling -> ItemType.SUBSAMPLING
else -> ItemType.PHOTO
}
}
}

View File

@ -0,0 +1,21 @@
package com.remax.visualnovel.entity.model.base
import com.remax.visualnovel.widget.imageviewer.adapter.ItemType
import com.remax.visualnovel.widget.imageviewer.core.Photo
/**
* Created by HJW on 2025/7/21
*/
abstract class BasePhoto : Photo {
abstract fun paramId(): Long
override fun id(): Long {
return paramId()
}
override fun itemType(): Int {
return ItemType.PHOTO
}
}

View File

@ -0,0 +1,30 @@
package com.remax.visualnovel.entity.request
import com.remax.visualnovel.constant.AppConstant
import com.remax.visualnovel.utils.AppUtils
/**
* Created by HJW on 2023/10/19
*/
data class PlatformAccountVerifyDTO(
/**
* 三方账号验证用
*/
val thirdToken: String? = null,
val thirdType: String? = null,
val appClient: String = AppConstant.APP_CLIENT,
val deviceCode: String = AppUtils.getAndroidID(),
/**
* common
*/
val authCode: String? = null,
)
data class CompleteUserInfoInput(
var nickname: String? = null,
var sex: Int? = null,
var birthDay: Long? = null,
var headImage: String? = null,
var exUserId: String? = null,
)

View File

@ -0,0 +1,32 @@
package com.remax.visualnovel.entity.response
import android.os.Parcelable
import com.remax.visualnovel.entity.model.base.BasePhoto
import kotlinx.parcelize.Parcelize
/**
* Created by HJW on 2025/7/21
*/
@Parcelize
data class AppearanceImage(
var imageUrl: String? = null,
var imageWidth: Int? = null,
var imageHeight: Int? = null,
var status: String = PENDING,
var select: Boolean = false,
var isPlaying: Boolean = false,
var unlockPrice: Long = 0,
var tempId: Long = 0,
) : BasePhoto(), Parcelable {
override fun paramId(): Long = tempId
companion object {
const val NSFW = "NSFW"
const val COMPLETED = "COMPLETED"
const val FAILED = "FAILED"
const val PENDING = "PENDING"
}
}

View File

@ -0,0 +1,11 @@
package com.remax.visualnovel.entity.response
/**
* Created by HJW on 2025/8/14
*/
data class Book(
val aiId: String,
val birthday: Long,
val characterName: String,
val headImg: String
)

View File

@ -0,0 +1,96 @@
package com.remax.visualnovel.entity.response
import android.os.Parcelable
import com.remax.visualnovel.extension.calculateAge
import com.remax.visualnovel.extension.getNimAccountId
import kotlinx.parcelize.Parcelize
/**
* Created by HJW on 2025/7/17
*/
@Parcelize
data class Character(
var aiId: String? = null,
var userId: String? = null,
var sex: Int? = null, // 0,男;1,女;2,自定义
var permission: Int? = null, //权限 1: 公开 2私密
var nickname: String? = null,
var idCard: String? = null,
var headImg: String? = null,
var birthday: Long? = null,
var showBirthday: Long? = null,
var roleCode: String? = null,
var roleName: String? = null,
var characterCode: String? = null,
var characterName: String? = null,
var tagCode: String? = null,
var tagName: String? = null,
var introduction: String? = null, // 人物简介 >= 10 字符 <= 300 字符
var imageUrl: String? = null,
var homeImageUrl: String? = null, // 主页头图
var imageWidth: Int? = null,
var imageHeight: Int? = null,
var aiUserExt: CharacterExt? = null,
var liked: Boolean? = null, //是否点过赞
var likedNum: Long? = null,
var chatNum: Long? = null,
var conversationNum: Long? = null,
var coinNum: Long? = null,
//在IM中用
val dialoguePrologue: String? = null,
val dialoguePitch: String? = null,
val dialogueSpeechRate: String? = null,
val voiceType: String? = null,
var backgroundImg: String? = null,
var isDefaultBackground: Boolean? = null,
var isMember: Boolean? = null,
var isHaveChatted: Boolean? = null, //是否聊过天
var isDelChatted: Boolean? = null, //是否删除过会话
var isAutoPlayVoice: Int? = null, //自动播放语音开关 1开 0
//var aiUserHeartbeatRelation: HeartbeatRelation? = null,
//var chatBubble: ChatBubble? = null,
//排行榜使用
var rankNo: Int? = null,
var heartbeatValTotal: Double? = null,
var giftCoinNum: Int? = null,
//首页滑动卡片使用
var isLimit: Boolean? = null,
var likedCount: Long? = null,
var heartbeatVal: Double? = null,
var character: String? = null,
var role: String? = null,
var tag: String? = null,
var isSecret: Boolean? = null,
//var albumList: List<Album>? = null,
) : Parcelable {
companion object {
const val UM_FREE = 1
const val UM_PAID = 2
}
val age: Int
get() = (birthday ?: 0L).calculateAge()
val nimAccountId: String
get() = aiId.getNimAccountId(true)
}
@Parcelize
data class CharacterExt(
var profile: String? = null, //人物设定 >= 10 字符 <= 4000 字符
var userProfile: String? = null, //人物设定 >= 10 字符 <= 4000 字符
var dialogueStyle: String? = null, // 对话风格 >= 10 字符 <= 300 字符
var userDialogueStyle: String? = null, // 对话风格 >= 10 字符 <= 300 字符
var dialoguePrologue: String? = null, // 开场白 >= 10 字符 <= 150 字符
var dialogueTimbreCode: String? = null, // 对话音色Code
var dialoguePitch: String? = null, // 对话-音高
var dialogueSpeechRate: String? = null, // 对话-语速
var imageStyleCode: String? = null, //形象风格code
var imageDesc: String? = null, //形象描述 >= 10 字符 <= 500 字符
var imageReferenceUrl: String? = null, // 形象参考
) : Parcelable

View File

@ -0,0 +1,18 @@
package com.remax.visualnovel.entity.response
import com.remax.visualnovel.entity.model.base.BasePhoto
/**
* Created by HJW on 2025/8/18
*/
data class ChatBackground(
val backgroundId: Int?,
val imgUrl: String,
var isDefault: Boolean,
var select: Boolean = false,
var isSelected: Boolean? = null,
) : BasePhoto() {
override fun paramId(): Long {
return imgUrl.hashCode().toLong()
}
}

View File

@ -0,0 +1,22 @@
package com.remax.visualnovel.entity.response
/**
* Created by HJW on 2023/10/19
*/
data class PlatformAccountVerify(
val authType: String,
val optType: String,
val authCode: String,
val token: String?,
) {
val isLogin
get() = !token.isNullOrBlank()
companion object {
const val OPT_LOGIN = "L"
const val OPT_REGISTER = "R"
const val AUTH_VC = "VC"
const val AUTH_PD = "PD"
}
}

View File

@ -0,0 +1,27 @@
package com.remax.visualnovel.entity.response
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Created by HJW on 2025/7/11
*/
@Parcelize
data class User(
val userId: String,
val idCard: String,
var birthday: Long?,
var nickname: String?,
var headImage: String?,
var sex: Int?,
val cpUserInfo: Boolean?,
val isMember: Boolean?,
val thirdEmail: String?,
val thirdNickname: String?,
val thirdType: String?,
// 可创建AI数量
var canCreateAiCount: Int,
var createdAiCount: Int,
) : Parcelable

View File

@ -0,0 +1,19 @@
package com.remax.visualnovel.entity.response.base
import kotlinx.parcelize.IgnoredOnParcel
/**
* Created by HJW on 2025/7/21
*/
open class BaseVoice {
open fun id(): String = ""
open fun url(): String = ""
open fun filePathName(): String = ""
@IgnoredOnParcel
var isPlaying: Boolean = false
@IgnoredOnParcel
var isLoading: Boolean = false
}

View File

@ -0,0 +1,92 @@
package com.remax.visualnovel.entity.response.base
import com.google.gson.annotations.SerializedName
import com.remax.visualnovel.app.base.app.CommonApplicationProxy
import com.remax.visualnovel.extension.toast
/**
* Created by HJW on 2022/10/27
*/
open class Response<T>(
@SerializedName(value = "content")
val data: T? = null,
open val errorCode: String = "",
open val errorMsg: String = "",
val status: String = successCode
) {
companion object {
const val successCode = "OK"
/**
* zip打包的错误error封装
* new
*/
inline fun <reified T> createZipFailResponse(vararg data: Response<*>): ApiFailedResponse<T> {
val failedResponse = ApiFailedResponse<T>()
for (t in data) {
if (!t.isApiSuccess) {
failedResponse.errorCode = t.errorCode
failedResponse.errorMsg = t.errorMsg
break
}
}
return failedResponse
}
}
val isOk: Boolean
get() = status == successCode
val isApiSuccess: Boolean
get() =
this is ApiSuccessResponse || this is ApiEmptyResponse
/**
* 将返回结果分为成功和失败2个高阶函数
*
* 使用inline修饰使2个参数可以调用外部函数return
*/
inline fun transformResult(apiSuccessCallback: ((T?) -> Unit) = {}, apiFailedCallback: ((Response<T>) -> Unit) = {}): Response<T> {
if (isApiSuccess) {
apiSuccessCallback.invoke(data)
} else {
apiFailedCallback.invoke(this)
}
return this
}
}
inline fun <reified T> Response<T>.parseData(listenerBuilder: (ResultBuilder<T>.() -> Unit), showToast: Boolean = false) {
val listener = ResultBuilder<T>().also(listenerBuilder)
when (this) {
is ApiSuccessResponse -> listener.onSuccess(this.response)
is ApiEmptyResponse -> listener.onSuccess(null)
is ApiFailedResponse -> {
listener.onFailed(this.errorCode, this.errorMsg)
listener.onFailedWithData(this.data)
if (showToast) {
CommonApplicationProxy.application.toast(errorMsg)
}
}
}
listener.onComplete()
}
class ResultBuilder<T> {
var onSuccess: (data: T?) -> Unit = {}
var onFailed: (errorCode: String, errorMsg: String) -> Unit = { _, _ ->
}
var onFailedWithData: (errorData: T?) -> Unit = {}
var onComplete: () -> Unit = {}
}
data class ApiSuccessResponse<T>(val response: T? = null) : Response<T>(data = response)
class ApiEmptyResponse<T> : Response<T>()
data class ApiFailedResponse<T>(override var errorCode: String = "", override var errorMsg: String = "", val errorData: T? = null) :
Response<T>(data = errorData, errorCode = errorCode, errorMsg = errorMsg)

View File

@ -0,0 +1,14 @@
package com.remax.visualnovel.event.model
/**
* Created by HJW on 2023/10/18
*/
data class OnLoginEvent(val status: Int) {
fun isLogin(): Boolean = status == LOGIN
companion object {
const val LOGIN = 1 //登录成功
const val LOGOUT = 2 //登出成功
}
}

View File

@ -0,0 +1,9 @@
package com.remax.visualnovel.event.model
/**
* Created by HJW on 2023/10/25
*/
data class OnPlayVoiceEvent(
val isStart: Boolean,
val id: String = ""
)

View File

@ -0,0 +1,25 @@
package com.remax.visualnovel.event.model.tab
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Created by HJW on 2023/4/18
*/
@Parcelize
enum class MainTab(val index: Int, val checkLogin: Boolean = true) : Parcelable {
/**
* 主页的子fragment
*/
TAB_BOOKS(0, false),
TAB_MANGAS(1, false),
TAB_ACTORS(2, false),
TAB_HISTORY(3, false),
}
@Parcelize
enum class ContactTab(val index: Int) : Parcelable {
MESSAGE(0), FRIEND(1)
}

View File

@ -0,0 +1,9 @@
package com.remax.visualnovel.event.model.tab
/**
* Created by HJW on 2023/10/23
*
* jumpItem: 主页 tab切换
*/
data class OnTabChangedEvent(val jumpItem: MainTab, val contactTab: ContactTab? = null)

View File

@ -0,0 +1,31 @@
package com.remax.visualnovel.event.modular
import com.remax.visualnovel.event.model.OnPlayVoiceEvent
import com.remax.visualnovel.event.model.tab.OnTabChangedEvent
import com.pengxr.modular.eventbus.facade.annotation.EventGroup
/**
* Created by HJW on 2023/5/22
* UI操作的事件
*/
@EventGroup(moduleName = "UI", autoClear = true)
interface UIEvents {
/**
* 首页底部tab 双击时回到顶部操作
*/
fun mainScrollToTop()
fun onNetworkConnect()
/**
* 主页tab切换
*/
fun onHomeTabChanged(): OnTabChangedEvent
/**
* 播放语音开始/结束
* @return Integer
*/
fun onSkillVoiceEvent(): OnPlayVoiceEvent
}

Some files were not shown because too many files have changed in this diff Show More