Compare commits

..

17 Commits

771 changed files with 56248 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,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

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,310 @@
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")
buildConfigString("API_SHARK", "https://test-shark.xxxxx.ai")
buildConfigString("API_COW", "https://test-cow.xxxxx.ai")
buildConfigString("API_PIGEON", "https://test-pigeon.xxxx.ai")
buildConfigString("API_LION", "https://test-lion.xxxx.ai")
buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge")
buildConfigString("RTC_APP_ID", "689ade491323ae01797818e0-XXX-TODO")
}
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")
buildConfigString("API_SHARK", "https://test-shark.xxxxx.ai")
buildConfigString("API_COW", "https://test-cow.xxxxx.ai")
buildConfigString("API_PIGEON", "https://test-pigeon.xxxx.ai")
buildConfigString("API_LION", "https://test-lion.xxxx.ai")
buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge")
buildConfigString("RTC_APP_ID", "689ade491323ae01797818e0-XXX-TODO")
}
}
}
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)
//s3图片上传 oss
implementation(Deps.awsS3)
implementation(Deps.awsCore)
// 网易 云信
implementation(Deps.nimBase)
implementation(Deps.nimPush)
//内购 / 充值
implementation(Deps.billing)
// RTC : 实时通信
implementation(Deps.BytePlusRTC)
implementation(project(mapOf("path" to ":loadingstateview")))
implementation(project(mapOf("path" to ":loadingstateview-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)
}
}

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="com.android.providers.media.MediaProvider" />
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.FLASHLIGHT" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_USER_PRESENT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:name=".configs.NovelApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".ui.splash.SplashActivity"
android:theme="@style/AppTheme.Launcher"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.main.MainActivity"
android:exported="false" >
</activity>
<activity
android:name=".ui.search.SearchActivity"
android:exported="false" >
</activity>
<activity
android:name=".ui.discussion.DiscussionActivity"
android:exported="false" >
</activity>
<activity
android:name=".ui.checkin.CheckInActivity"
android:exported="false" >
</activity>
<activity
android:name=".ui.chat.ChatActivity"
android:exported="false" >
</activity>
</application>
</manifest>

Binary file not shown.

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,76 @@
package com.remax.visualnovel.api.interceptor.util;
import java.nio.charset.StandardCharsets;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import timber.log.Timber;
public class AES {
private static final String KEY_SUFFIX = "90e8kDQUIWpXg8Jp";
/**
* For a 128-bit AES key you need 16 bytes.
* For a 256-bit AES key you need 32 bytes.
*/
public static String KEY;
/**
* IV length: must be 16 bytes long
*/
public static final String IV = "sdf4ddfsFD86Vdf2";
private AES() {
}
public static void genKey(String token) {
//token加密最多截取前32位
String encodeStr = (token.length() > 32 ? token.substring(0, 32) : token) + KEY_SUFFIX;
KEY = Md5.encode(encodeStr).toUpperCase();
Timber.d("genKey: %s", KEY);
}
public static String encrypt(String data) {
return encrypt(data, KEY, IV);
}
public static String desEncrypt(String data) {
return desEncrypt(data, KEY, IV);
}
public static String encrypt(String data, String key, String initVector) {
try {
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes(StandardCharsets.UTF_8));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(data.getBytes());
return Base64.encode(encrypted);
} catch (Exception ex) {
ex.printStackTrace();
return data;
}
}
public static String desEncrypt(String data, String key, String initVector) {
try {
byte[] encrypted = Base64.decode(data);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(initVector.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);
byte[] original = cipher.doFinal(encrypted);
return new String(original);
} catch (Exception e) {
e.printStackTrace();
return data;
}
}
}

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,222 @@
package com.remax.visualnovel.api.service
import com.remax.visualnovel.BuildConfig
import com.remax.visualnovel.entity.request.AIGenerate
import com.remax.visualnovel.entity.request.AIGenerateImage
import com.remax.visualnovel.entity.request.AIHeadImgRequest
import com.remax.visualnovel.entity.request.AIIDRequest
import com.remax.visualnovel.entity.request.AlbumCreate
import com.remax.visualnovel.entity.request.AlbumDTO
import com.remax.visualnovel.entity.request.CardRequest
import com.remax.visualnovel.entity.request.ChatAlbum
import com.remax.visualnovel.entity.request.ClassificationRequest
import com.remax.visualnovel.entity.request.Gift
import com.remax.visualnovel.entity.request.QueryAlbumDTO
import com.remax.visualnovel.entity.request.SimpleCountDTO
import com.remax.visualnovel.entity.response.Album
import com.remax.visualnovel.entity.response.AlbumCreateCountOutput
import com.remax.visualnovel.entity.response.AppearanceImage
import com.remax.visualnovel.entity.response.Character
import com.remax.visualnovel.entity.response.ContentRes
import com.remax.visualnovel.entity.response.ExploreInfo
import com.remax.visualnovel.entity.response.MeetSdOutput
import com.remax.visualnovel.entity.response.Pageable
import com.remax.visualnovel.entity.response.base.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface AIService {
/**
* 卡片上报绑定
*/
@POST("/web/meet/bd")
suspend fun cardBind(@Body request: AIIDRequest): Response<Character>
/**
* 卡片被喜欢推荐
*/
@POST("/web/meet/rc")
suspend fun cardLiked(): Response<Album>
/**
* 卡片上报
*/
@POST("/web/meet/sd")
suspend fun reportCard(@Body request: CardRequest): Response<MeetSdOutput>
/**
* 获取首页卡片列表
*/
@POST("/web/home/rm-list")
suspend fun getHomeCard(@Body request: ClassificationRequest): Response<List<Character>>
/**
* 获取单个首页卡片
*/
@POST(" /web/home/meet-detail")
suspend fun getHomeCardDetail(@Body request: AIIDRequest): Response<Character>
/**
* 获取分类列表
*/
@POST("/web/home/classification-list")
suspend fun getClassificationList(@Body request: ClassificationRequest): Response<List<Character>>
/**
* 获取榜单
*/
@POST("/web/rank/heartbeat")
suspend fun getHeartbeatRank(): Response<List<Character>>
/**
* 获取榜单
*/
@POST("/web/rank/gift")
suspend fun getGiftRank(): Response<List<Character>>
/**
* 获取榜单
*/
@POST("/web/rank/chat")
suspend fun getChatRank(): Response<List<Character>>
/**
* 获取发现页顶部数据
*/
@POST("/web/explore/info")
suspend fun getExploreInfo(): Response<ExploreInfo>
/**
* 解锁加密图片
*/
@POST("/web/ai-user/unlock-album-img")
suspend fun unlockAlbum(@Body dto: ChatAlbum): Response<Album>
/**
* 解锁秘密爱慕者
*/
@POST("/web/meet/unlock")
suspend fun unlockSecret(@Body dto: AIIDRequest): Response<Album>
/**
* 设置当前图片价格
*/
@POST("/web/ai-user/set-album-unlock-price")
suspend fun setAlbumUnlockPrice(@Body dto: AlbumDTO): Response<Any>
/**
* 删除AI角色
*/
@POST("/web/ai-user/del")
suspend fun deleteAICharacter(@Body request: Character): Response<Any>
/**
* 设置当前默认图片
*/
@POST("/web/ai-user/set-default-album")
suspend fun setAlbumDefault(@Body dto: AlbumDTO): Response<Any>
@POST("/web/ai-user/create-edit")
suspend fun createOrEditAICharacter(@Body request: Character): Response<Character>
@POST("/web/ai-user/edit-head-img")
suspend fun editAIAvatar(@Body request: AIHeadImgRequest): Response<Any>
@POST(BuildConfig.API_COW + "/web/gen/user-content-v1")
suspend fun generateAICharacter(@Body request: AIGenerate): Response<ContentRes>
/**
* 编辑时获取我的ai角色信息
*/
@POST("/web/ai-user/get-my-ai-user/info")
suspend fun getAICharacter(@Body request: Character): Response<Character>
/**
* 访问AI个人主页时获取信息
*/
@POST("/web/ai-user-search/base-info")
suspend fun getAICharacterProfile(@Body request: Character): Response<Character>
/**
* 访问AI的统计信息
*/
@POST("/web/ai-user/stat")
suspend fun getAICharacterStat(@Body request: Character): Response<Character>
/**
* 修改点赞状态
*/
@POST("/web/ai-user/like-or-cancel")
suspend fun setAILikeOrCancel(@Body request: AlbumDTO): Response<Any>
/**
* 喜欢或取消喜欢相片
*/
@POST("/web/album/like_or_cancel")
suspend fun setLikeOrDislike(@Body dto: AlbumDTO): Response<Any>
/**
* 删除相片
*/
@POST("/web/ai-user/album-del")
suspend fun deleteAlbum(@Body dto: AlbumDTO): Response<Any>
/**
* 批量添加图片到相册
*/
@POST("/web/ai-user/batch-add-album")
suspend fun addAlbum(@Body dto: AlbumCreate): Response<Any>
/**
* 获取创作次数
*/
@POST("/web/user/get-user-create-count")
suspend fun getAlbumCreateCount(): Response<AlbumCreateCountOutput>
/**
* 购买创作次数
*/
@POST("/web/ai/buy-create-image-count")
suspend fun buyAlbumCreateCount(@Body dto: SimpleCountDTO): Response<Any>
/**
* 批量添加图片到聊天背景
*/
@POST("/web/chat-background/batch-add")
suspend fun addChatBackground(@Body dto: AlbumCreate): Response<Any>
/**
* 获取相册 分页
*/
@POST("/web/ai-user/album-list")
suspend fun getAlbumList(@Body dto: QueryAlbumDTO): Response<Pageable<Album>>
/**
* 获取用户礼物 分页
*/
@POST("/web/ai-user-gift/list")
suspend fun getUserGiftList(@Body dto: QueryAlbumDTO): Response<Pageable<Gift>>
/**
* AI一键生成-创建生成人物形象图片任务
*/
@POST(BuildConfig.API_COW + "/web/gen/image-ct")
suspend fun generateImageBatch(@Body request: AIGenerateImage): Response<AIGenerateImage>
/**
* AI一键生成-删除图片生成任务
*/
@POST(BuildConfig.API_COW + "/web/gen/del")
suspend fun generateImageBatchDel(@Body request: AIGenerateImage): Response<Any>
/**
* AI一键生成-轮询查询图片生成结果
*/
@POST(BuildConfig.API_COW + "/web/gen/image-pl")
suspend fun generateImageBatchQuery(@Body request: AIGenerateImage): Response<List<AppearanceImage>>
}

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,159 @@
package com.remax.visualnovel.api.service
import com.remax.visualnovel.BuildConfig
import com.remax.visualnovel.entity.request.AIFeedback
import com.remax.visualnovel.entity.request.AIIDRequest
import com.remax.visualnovel.entity.request.AIIsShowDTO
import com.remax.visualnovel.entity.request.ChatAlbum
import com.remax.visualnovel.entity.request.ChatSetting
import com.remax.visualnovel.entity.request.HeartbeatBuy
import com.remax.visualnovel.entity.request.RTCRequest
import com.remax.visualnovel.entity.request.SearchPage
import com.remax.visualnovel.entity.request.SimpleDataDTO
import com.remax.visualnovel.entity.request.VoiceTTS
import com.remax.visualnovel.entity.response.Album
import com.remax.visualnovel.entity.response.Character
import com.remax.visualnovel.entity.response.ChatBackground
import com.remax.visualnovel.entity.response.ChatSet
import com.remax.visualnovel.entity.response.Friends
import com.remax.visualnovel.entity.response.HeartbeatLevelOutput
import com.remax.visualnovel.entity.response.Pageable
import com.remax.visualnovel.entity.response.Token
import com.remax.visualnovel.entity.response.VoiceASR
import com.remax.visualnovel.entity.response.base.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface ChatService {
/**
* 发送开场白消息
*/
@POST("/web/chat/send-dialogue-prologue-message")
suspend fun sendDialogueMsg(@Body request: AIIDRequest): Response<Any>
/**
* 获取IM中AI的基础信息
*/
@POST("/web/ai-user-search/im-base-info")
suspend fun getIMAICharacterProfile(@Body request: Character): Response<Character>
/**
* 访问解锁加密图片
*/
@POST("/web/ai-user/view-unlock-album-img")
suspend fun viewAlbumImg(@Body request: ChatAlbum): Response<Album>
/**
* 关系列表
*/
@POST("/web/ai-user/heartbeat-relation-list")
suspend fun getMyFriends(@Body request: SearchPage): Response<Pageable<Friends>>
@POST("/web/ai-user/heartbeat-rank")
suspend fun getMyFriendRank(): Response<Double>
/**
* 生成提示词
*/
@POST(BuildConfig.API_COW + "/web/gen/sup-content-v2")
suspend fun getPrompts(@Body request: AIIDRequest): Response<List<String>>
/**
* AI回话点赞/点踩
*/
@POST(BuildConfig.API_PIGEON + "/web/fb/v1")
suspend fun aiFeedback(@Body request: AIFeedback): Response<Any>
/**
* 获取RTC
*/
@POST(BuildConfig.API_COW + "/web/voice-chat/gen-rtc-tk")
suspend fun getRTCToken(@Body request: RTCRequest): Response<Token>
/**
* 操作通话
*/
@POST(BuildConfig.API_COW + "/web/voice-chat/opt")
suspend fun voiceChatOpt(@Body request: RTCRequest): Response<Any>
/**
* 获取聊天背景列表
*/
@POST("/web/chat-background/list")
suspend fun getChatBackgroundList(@Body request: AIIDRequest): Response<List<ChatBackground>>
/**
* 获取聊天设置
*/
@POST("/web/chat-set/get-my")
suspend fun getChatSetting(@Body request: ChatSetting): Response<ChatSet>
/**
* 修改聊天设置
*/
@POST("/web/chat-set/set")
suspend fun setChatSetting(@Body request: ChatSet): Response<Any>
/**
* 修改聊天气泡
*/
@POST("/web/chat-set/set-chat-bubble")
suspend fun setChatBubble(@Body request: ChatSetting): Response<Any>
/**
* 修改聊天模型
*/
@POST("/web/chat-set/set-chat-model")
suspend fun setChatModel(@Body request: ChatSetting): Response<Any>
/**
* 修改是否自动播放语音
*/
@POST("/web/chat-set/auto-play-voice")
suspend fun setChatAutoPlay(@Body request: ChatSetting): Response<Any>
/**
* 修改聊天背景图
*/
@POST("/web/chat-background/set-background")
suspend fun setChatBackground(@Body request: ChatSetting): Response<Any>
/**
* 删除聊天背景图
*/
@POST("/web/chat-background/del")
suspend fun deleteChatBackground(@Body request: ChatSetting): Response<Any>
/**
* 展示心动关系开关
*/
@POST("/web/ai-user/heartbeat-relation-switch")
suspend fun relationSwitch(@Body request: AIIsShowDTO): Response<Any>
/**
* 语音转文本
*/
@POST(BuildConfig.API_COW + "/web/voice/asr-v2")
suspend fun voiceASR(@Body request: SimpleDataDTO): Response<VoiceASR>
/**
* 生成语音
*/
@POST(BuildConfig.API_COW + "/web/voice/tts-v2")
suspend fun voiceTTS(@Body request: VoiceTTS): Response<String>
/**
* 获取心动等级
*/
@POST("/web/ai-user/heartbeat-level")
suspend fun getHeartbeatLevel(@Body request: Character): Response<HeartbeatLevelOutput>
/**
* 购买心动值
*/
@POST("/web/ai-user/buy-heartbeat-val")
suspend fun buyHeartbeatVal(@Body request: HeartbeatBuy): Response<Any>
}

View File

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

View File

@ -0,0 +1,39 @@
package com.remax.visualnovel.api.service
import com.remax.visualnovel.BuildConfig
import com.remax.visualnovel.entity.request.ImgCheckDTO
import com.remax.visualnovel.entity.request.S3TypeDTO
import com.remax.visualnovel.entity.request.SimpleContentDTO
import com.remax.visualnovel.entity.response.BucketBean
import com.remax.visualnovel.entity.response.base.Response
import retrofit2.http.Body
import retrofit2.http.POST
/**
* OSS文件上传
*
*/
interface OssService {
/**
* 获取aws s3 bucket信息
*/
@POST(BuildConfig.API_SHARK + "/web/file/sts-tk")
suspend fun getS3Bucket(@Body dto: S3TypeDTO): Response<BucketBean>
/**
* 图片鉴黄
*/
@POST(BuildConfig.API_SHARK + "/web/file/check")
suspend fun checkS3Img(
@Body imgCheckDTO: ImgCheckDTO
): Response<Any>
/**
* 关键字校验
*/
@POST("/web/check_text")
suspend fun checkText(
@Body simpleContentDTO: SimpleContentDTO
): Response<Any>
}

View File

@ -0,0 +1,87 @@
package com.remax.visualnovel.api.service
import com.remax.visualnovel.BuildConfig
import com.remax.visualnovel.entity.request.ChargeOrderDTO
import com.remax.visualnovel.entity.request.ChargeProductDTO
import com.remax.visualnovel.entity.request.ChargeProductInfo
import com.remax.visualnovel.entity.request.SearchPage
import com.remax.visualnovel.entity.request.SubPriceDTO
import com.remax.visualnovel.entity.request.ValidateTransactionDTO
import com.remax.visualnovel.entity.response.ChargeOrder
import com.remax.visualnovel.entity.response.Membership
import com.remax.visualnovel.entity.response.SubPrice
import com.remax.visualnovel.entity.response.Transaction
import com.remax.visualnovel.entity.response.UserSubInfo
import com.remax.visualnovel.entity.response.Wallet
import com.remax.visualnovel.entity.response.base.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface PayService {
/**
* 获取我的流水
*/
@POST(BuildConfig.API_LION + "/web/pay/account/bill-list")
suspend fun getTransactionList(@Body request: SearchPage): Response<Transaction>
/**
* 获取我的钱包
*/
@POST(BuildConfig.API_LION + "/web/pay/account/wallet")
suspend fun getMyWallet(): Response<Wallet>
/**
* 获取充值产品
*/
@POST(BuildConfig.API_LION + "/web/pay/config/charge-product-list")
suspend fun getChargeProducts(
@Body dto: ChargeProductDTO = ChargeProductDTO()
): Response<ChargeProductInfo>
/**
* 获取vip订阅价格列表
*/
@POST(BuildConfig.API_LION + "/web/pay/config/sub-product-list")
suspend fun getSubPriceList(
@Body subPriceDTO: SubPriceDTO = SubPriceDTO()
): Response<List<SubPrice>>
/**
* 会员特权列表
*/
@POST(BuildConfig.API_LION + "/web/member/detail")
suspend fun getVipPrivilegeList(): Response<Membership>
/**
* 创建一个订单
*/
@POST(BuildConfig.API_LION +"/web/pay/trade/pre-charge-google")
suspend fun createOrder(
@Body dto: ChargeOrderDTO
): Response<ChargeOrder>
/**
* 验证支付是否成功
*/
@POST(BuildConfig.API_LION +"/web/pay/webhooks/google/v2")
suspend fun validateTranslation(
@Body dto: ValidateTransactionDTO
): Response<Any>
/**
* 验证订阅是否成功
*/
@POST(BuildConfig.API_LION +"/web/pay/subscribe/upload-google-receipt")
suspend fun uploadGoogleReceipt(
@Body dto: ValidateTransactionDTO
): Response<String>
/**
* 订阅/升级VIP前查询订阅信息
*/
@POST(BuildConfig.API_LION +"/web/pay/appStore/getUserSubscription")
suspend fun checkSubInfo(): Response<UserSubInfo>
}

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,17 @@
package com.remax.visualnovel.app
import androidx.lifecycle.LifecycleOwner
interface AbsView : LifecycleOwner{
fun showLoading()
fun hideLoading()
fun showToast(text: String?)
fun showToast(resId: Int)
}

View File

@ -0,0 +1,47 @@
package com.remax.visualnovel.app
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.viewbinding.ViewBinding
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
/**
* Created by HJW on 2021/10/19
*/
abstract class BaseBindingQuickAdapter<T, out VB : ViewBinding>(
private val inflate: (LayoutInflater, ViewGroup, Boolean) -> VB,
layoutResId: Int = -1,
data: MutableList<T>? = null
) :
BaseQuickAdapter<T, BaseBindingQuickAdapter.BaseBindingHolder>(layoutResId, data) {
override fun onCreateDefViewHolder(parent: ViewGroup, viewType: Int) =
BaseBindingHolder(inflate(LayoutInflater.from(parent.context), parent, false))
class BaseBindingHolder(private val binding: ViewBinding) : BaseViewHolder(binding.root) {
constructor(itemView: View) : this(ViewBinding { itemView })
@Suppress("UNCHECKED_CAST")
fun <VB : ViewBinding> getViewBinding() = binding as VB
var extraObj: Any? = null
}
/**
* 带左右滑动的item收起
*/
// fun resetDeleteView(position: Int) {
// try {
// (getViewByPosition(
// position + headerLayoutCount,
// R.id.easySwipeMenuLayout
// ) as? EasySwipeMenuLayout)?.resetStatus()
// } catch (e: Exception) {
// Timber.d("resetDeleteView position:$position Exception:${e.localizedMessage}")
// }
// }
}

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,230 @@
package com.remax.visualnovel.app.activityresultapi
import android.content.Intent
import android.text.TextUtils
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.alibaba.android.arouter.core.LogisticsCenter
import com.alibaba.android.arouter.exception.NoRouteFoundException
import com.alibaba.android.arouter.facade.Postcard
import com.alibaba.android.arouter.facade.callback.InterceptorCallback
import com.alibaba.android.arouter.facade.callback.NavigationCallback
import com.alibaba.android.arouter.facade.enums.RouteType
import com.alibaba.android.arouter.facade.service.DegradeService
import com.alibaba.android.arouter.facade.service.InterceptorService
import com.alibaba.android.arouter.facade.service.PretreatmentService
import com.alibaba.android.arouter.launcher.ARouter
import com.remax.visualnovel.app.initializer.impl.ActivityLifecycleInitializer
/**
* 获取activityResultLauncher
*/
fun FragmentActivity.activityResultLauncher(): XActivityResultContract<Intent, ActivityResult>? {
val activityKey = intent.getStringExtra(ActivityLifecycleInitializer.KEY_ACTIVITY_RESULT_API)
return if (TextUtils.isEmpty(activityKey)) null else ActivityLifecycleInitializer.resultLauncherMap[activityKey]
}
/**
* 在Activity使用registerForActivityResult
*/
@JvmOverloads
fun FragmentActivity.registerForActivityResult(
intent: Intent,
activityResultCallback: ActivityResultCallback<ActivityResult>? = null
) {
activityResultLauncher()?.launch(intent, activityResultCallback)
}
/**
* 在Activity使用registerForActivityResult
*/
@JvmOverloads
inline fun <reified T : FragmentActivity> FragmentActivity.registerForActivityResult(
intentExtra: (intent: Intent) -> Unit = {},
activityResultCallback: ActivityResultCallback<ActivityResult>? = null
) {
val intent = Intent(this, T::class.java)
intentExtra(intent)
registerForActivityResult(intent, activityResultCallback)
}
/**
* 在Fragment使用registerForActivityResult
*/
@JvmOverloads
fun Fragment.registerForActivityResult(
intent: Intent,
activityResultCallback: ActivityResultCallback<ActivityResult>? = null
) {
requireActivity().activityResultLauncher()?.launch(intent, activityResultCallback)
}
/**
* 在Fragment使用registerForActivityResult
*/
@JvmOverloads
inline fun <reified T : FragmentActivity> Fragment.registerForActivityResult(
intentExtra: (intent: Intent) -> Unit = {},
activityResultCallback: ActivityResultCallback<ActivityResult>? = null
) {
val intent = Intent(this.requireActivity(), T::class.java)
intentExtra(intent)
registerForActivityResult(intent, activityResultCallback)
}
/**
* Activity中ARouter导航
* @param [activity] activity
* @param [activityResultCallback] 返回数据回调
*/
fun Postcard.navigation(
activity: FragmentActivity?,
activityResultCallback: ActivityResultCallback<ActivityResult>
): Any? {
return navigation(activity, null, activityResultCallback)
}
/**
* Fragment中ARouter导航
* @param [fragment] Fragment
* @param [activityResultCallback] 返回数据回调
*/
fun Postcard.navigation(
fragment: Fragment?,
activityResultCallback: ActivityResultCallback<ActivityResult>
): Any? {
return navigation(fragment?.requireActivity(), null, activityResultCallback)
}
/**
* Fragment中ARouter导航
* @param [fragment] Fragment
* @param [callback] 回调
* @param [activityResultCallback] 返回数据回调
*/
fun Postcard.navigation(
fragment: Fragment?,
callback: NavigationCallback?,
activityResultCallback: ActivityResultCallback<ActivityResult>
): Any? {
return navigation(fragment?.requireActivity(), callback, activityResultCallback)
}
/**
* Activity中ARouter导航
* @param [activity] Fragment
* @param [callback] 回调
* @param [activityResultCallback] 返回数据回调
*/
fun Postcard.navigation(
activity: FragmentActivity?,
callback: NavigationCallback?,
activityResultCallback: ActivityResultCallback<ActivityResult>
): Any? {
if (activity == null) {
return null
}
val _postcard = this
val pretreatmentService = ARouter.getInstance().navigation(PretreatmentService::class.java)
if (null != pretreatmentService && !pretreatmentService.onPretreatment(activity, this)) {
return null
}
try {
LogisticsCenter.completion(_postcard)
} catch (ex: NoRouteFoundException) {
debugLog(activity, path, group)
if (null != callback) {
callback.onLost(_postcard)
} else {
val degradeService = ARouter.getInstance().navigation(DegradeService::class.java)
degradeService?.onLost(activity, _postcard)
}
return null
}
callback?.onFound(_postcard)
val interceptorService = ARouter.getInstance().navigation(InterceptorService::class.java)
if (!isGreenChannel && interceptorService != null) {
interceptorService.doInterceptions(_postcard, object : InterceptorCallback {
override fun onContinue(postcard: Postcard?) {
_navigation(activity, _postcard, activityResultCallback)
}
override fun onInterrupt(exception: Throwable?) {
callback?.onInterrupt(_postcard)
}
})
} else {
return _navigation(activity, this, activityResultCallback)
}
return null
}
/**
* Debug模式下日志打印
*/
private fun debugLog(activity: FragmentActivity, path: String?, group: String?) {
if (ARouter.debuggable()) {
// Show friendly tips for user.
activity.runOnUiThread {
Toast.makeText(
activity,
"There's no route matched!Path = [${path}]Group = [${group}]",
Toast.LENGTH_LONG
).show()
}
}
}
private fun _navigation(
activity: FragmentActivity,
postcard: Postcard,
activityResultCallback: ActivityResultCallback<ActivityResult>,
): Any? {
return when (postcard.type) {
RouteType.ACTIVITY -> {
val intent = Intent(activity, postcard.destination)
postcard.extras?.let { intent.putExtras(it) }
if (postcard.flags != -1) {
intent.flags = postcard.flags
}
postcard.action?.let { intent.action = postcard.action }
activity.runOnUiThread {
//适配动画
if ((postcard.enterAnim != -1 && postcard.exitAnim != -1)) {
activity.overridePendingTransition(postcard.enterAnim, postcard.exitAnim)
}
activity.registerForActivityResult(intent, activityResultCallback)
}
null
}
RouteType.PROVIDER -> {
postcard.provider
}
RouteType.FRAGMENT -> {
val fragmentMeta = postcard.destination
try {
val instance = fragmentMeta.getConstructor().newInstance()
if (instance is Fragment) {
instance.arguments = postcard.extras
}
instance
} catch (ex: Exception) {
ex.printStackTrace()
}
}
else -> {
null
}
}
}

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,67 @@
package com.remax.visualnovel.app.di
import com.remax.visualnovel.api.factory.ServiceFactory
import com.remax.visualnovel.api.service.AIService
import com.remax.visualnovel.api.service.BookService
import com.remax.visualnovel.api.service.ChatService
import com.remax.visualnovel.api.service.DictService
import com.remax.visualnovel.api.service.LoginService
import com.remax.visualnovel.api.service.MessageService
import com.remax.visualnovel.api.service.OssService
import com.remax.visualnovel.api.service.PayService
import com.remax.visualnovel.api.service.UserService
import dagger.Module
import dagger.Provides
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>()
@Singleton
@Provides
fun aiService() = create<AIService>()
@Singleton
@Provides
fun ossService() = create<OssService>()
@Singleton
@Provides
fun payService() = create<PayService>()
@Singleton
@Provides
fun chatService() = create<ChatService>()
private inline fun <reified T> create(): T {
return ServiceFactory.createService()
}
}

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,101 @@
package com.remax.visualnovel.app.initializer.impl
import android.app.Activity
import android.app.Application
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.FragmentActivity
import com.remax.visualnovel.app.activityresultapi.XActivityResultContract
import com.remax.visualnovel.app.initializer.AppInitializers
import com.remax.visualnovel.app.viewmodel.AppIMViewModel
import com.remax.visualnovel.configs.NovelApplication
import com.remax.visualnovel.utils.StatusBarUtils
import timber.log.Timber
/**
* Created by HJW on 2023/5/11
*/
class ActivityLifecycleInitializer(val application: Application, private val appIMViewModel: AppIMViewModel) : AppInitializers {
override fun init() {
application.registerActivityLifecycleCallbacks(AppLifecycleCallbacks(appIMViewModel))
}
inner class AppLifecycleCallbacks constructor(private val appIMViewModel: AppIMViewModel) : Application.ActivityLifecycleCallbacks {
private var activityCount = 0
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity is ActivityResultCaller) {
//生成一个Key
val activityKey = activity.javaClass.simpleName + System.currentTimeMillis()
//添加一个默认ActivityResultLauncher
val resultLauncher =
XActivityResultContract(activity, ActivityResultContracts.StartActivityForResult())
//把生成的Key放到intent中作为每一个Activity的唯一标识
activity.intent.putExtra(KEY_ACTIVITY_RESULT_API, activityKey)
//存放到Map中
resultLauncherMap[activityKey] = resultLauncher
}
}
override fun onActivityStarted(activity: Activity) {
activityCount++
}
override fun onActivityPaused(activity: Activity) {
NovelApplication.setCurrentActivity(null)
}
override fun onActivityResumed(activity: Activity) {
Timber.d("currentActivity:${activity::class.java.name}")
NovelApplication.setCurrentActivity(activity)
if (StatusBarUtils.statusBarHeight == 0) {
StatusBarUtils.getStatusBarHeight(activity)
}
//TODO - check pay info later
/*if (activity !is WelcomeActivity) {
GooglePayManager.checkProductDetails()
}*/
}
override fun onActivityStopped(activity: Activity) {
activityCount--
}
override fun onActivityDestroyed(activity: Activity) {
if (activity is FragmentActivity) {
val activityKey = activity.intent.getStringExtra(KEY_ACTIVITY_RESULT_API)
if (!TextUtils.isEmpty(activityKey)) {
resultLauncherMap[activityKey]?.unregister()
//移除activity的resultLauncher
resultLauncherMap.remove(activityKey)
}
}
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
}
companion object {
/**
* 保存ActivityResultApi的Key
*/
const val KEY_ACTIVITY_RESULT_API = "activityResultApi"
/**
* 保存activity和Fragment的resultLauncher
*/
val resultLauncherMap: MutableMap<String, XActivityResultContract<Intent, ActivityResult>> =
mutableMapOf()
}
}

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,271 @@
package com.remax.visualnovel.app.viewmodel.base
import android.graphics.BitmapFactory
import com.amazonaws.auth.BasicSessionCredentials
import com.amazonaws.mobileconnectors.s3.transferutility.TransferListener
import com.amazonaws.mobileconnectors.s3.transferutility.TransferState
import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility
import com.amazonaws.services.s3.AmazonS3Client
import com.amazonaws.services.s3.S3ClientOptions
import com.amazonaws.services.s3.model.ObjectMetadata
import com.remax.visualnovel.R
import com.remax.visualnovel.app.base.app.CommonApplicationProxy
import com.remax.visualnovel.constant.StatusCode
import com.remax.visualnovel.entity.request.ImgCheckDTO
import com.remax.visualnovel.entity.response.BucketBean
import com.remax.visualnovel.entity.response.base.ApiFailedResponse
import com.remax.visualnovel.entity.response.base.ApiSuccessResponse
import com.remax.visualnovel.entity.response.base.Response
import com.remax.visualnovel.extension.resumeWithActive
import com.remax.visualnovel.repository.api.OssRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import java.io.File
import javax.inject.Inject
/**
* Created by HJW on 2022/11/9
*
* oss上传相关
*/
@HiltViewModel
open class OssViewModel @Inject constructor() : UserViewModel() {
@Inject
lateinit var ossRepository: OssRepository
data class LoadFileData(
var isSuccess: Boolean,
val isViolation: Boolean = false,
val errorMsg: String = "",
var urlPath: String = "",
var filePath: String = "",
var width: Int = 0,
var height: Int = 0
)
data class FileUpLoadRes(
val loadFileData: LoadFileData,
val fileOption: FileOption? = null,
)
data class FileOption(
val path: String,
val urlPath: String,
val ossType: String,
val width: Int,
val height: Int
)
/**
* 请求oss的token
*/
suspend fun getBucketToken(postfix: String, ossType: String): Response<BucketBean> {
return ossRepository.getS3Bucket(ossType, postfix)
}
/**
* 挂起函数上传图片
* @param filePath String
* @param ossType String
* @param isImg Boolean
* @param checkNSFW Boolean 是否鉴黄
* @param checkRealPerson Boolean 是否鉴定真人
* @param checkKid Boolean 是否鉴定儿童
* @return Response<LoadFileData> 封装成服务器返回一致类型处理
*/
suspend fun ossUploadFile(
filePath: String,
ossType: String,
isImg: Boolean = true,
checkNSFW: Boolean = true,
checkRealPerson: Boolean = false,
checkKid: Boolean = false,
token: BucketBean? = null
): Response<LoadFileData> {
/**
* 获取S3 STS Token对象
*/
var s3BucketRes = token
if (s3BucketRes == null) {
val postfix = if (filePath.isNotEmpty()) filePath.substring(filePath.lastIndexOf(".") + 1) else "png"
val getTokenRes = getBucketToken(postfix, ossType)
//请求s3 token失败
if (!getTokenRes.isApiSuccess) {
return ApiFailedResponse(
errorMsg = getTokenRes.errorMsg,
errorData = createNormalErrorFileData(filePath).loadFileData
)
} else {
s3BucketRes = getTokenRes.data!!
}
}
val uploadRes = uploadFile(s3BucketRes, isImg, filePath, ossType)
//上传图片失败
if (!uploadRes.loadFileData.isSuccess) {
return ApiFailedResponse(errorMsg = uploadRes.loadFileData.errorMsg, errorData = uploadRes.loadFileData)
}
//如果不是图片 或者 不需要鉴黄、鉴定真人、鉴定儿童,直接返回成功结果
if (!isImg || (!checkNSFW && !checkRealPerson && !checkKid)) {
return ApiSuccessResponse(uploadRes.loadFileData)
}
val fileOption = uploadRes.fileOption!!
val checkDTO = ImgCheckDTO(fileOption.ossType, fileOption.path)
//鉴黄
val checkNSFWRes = if (checkNSFW) ossRepository.checkS3Img(checkDTO) else ApiSuccessResponse()
return when {
checkNSFWRes.isApiSuccess -> {
ApiSuccessResponse(uploadRes.loadFileData)
}
else -> {
ApiFailedResponse(StatusCode.UPLOAD_FILE_VIOLATION.code, checkNSFWRes.errorMsg, uploadRes.loadFileData)
}
}
}
/**
* 包装上传失败的实体
* @param filePath String 本地图片地址
* @return FileUpLoadRes
*/
private fun createNormalErrorFileData(filePath: String) = FileUpLoadRes(
LoadFileData(
isSuccess = false,
errorMsg = CommonApplicationProxy.application.getString(R.string.upload_error),
filePath = filePath
)
)
/**
* 协程处理亚马逊上传图片
*
* 使用带取消回调的协程当上传回调时需要判断协程isActive以防报错崩溃
* @param stsToken BucketBean 授权信息
* @param isImg Boolean 是否是图片
* @param filePath String 本地地址
* @param ossType String 上传类型
* @return FileUpLoadRes 返回结果封装
*/
private suspend fun uploadFile(
stsToken: BucketBean,
isImg: Boolean,
filePath: String,
ossType: String,
) = suspendCancellableCoroutine {
it.invokeOnCancellation { _ ->
it.resumeWithActive(createNormalErrorFileData(filePath))
}
val awsCreds = BasicSessionCredentials(
stsToken.accessKeyId,
stsToken.accessKeySecret,
stsToken.securityToken
)
val uploadClient = AmazonS3Client(
awsCreds,
com.amazonaws.regions.Region.getRegion(stsToken.region)
).apply {
setS3ClientOptions(
S3ClientOptions.builder()
.setAccelerateModeEnabled(false)
.build()
)
}
val transferUtility = TransferUtility.builder()
.s3Client(uploadClient)
.context(CommonApplicationProxy.application)
.build()
val fileName = filePath.substring(filePath.lastIndexOf("/") + 1)
val path = if (stsToken.path.endsWith("*")) {
stsToken.path.replace("*", fileName)
} else {
stsToken.path
}
val urlPath = if (stsToken.urlPath.endsWith("*")) {
stsToken.urlPath.replace("*", fileName)
} else {
stsToken.urlPath
}
Timber.d("oss上传 - AmazonS3 Token path:$path urlPath$urlPath")
val obj = ObjectMetadata()
obj.addUserMetadata("x-amz-tagging", "temp=1")
val transferListener = object :
TransferListener {
override fun onStateChanged(id: Int, state: TransferState?) {
Timber.d("oss上传 - AmazonS3 onStateChanged:$state")
Timber.d("oss上传 - 协程状态 isActive: ${it.isActive} isCancelled: ${it.isCancelled} isCompleted: ${it.isCompleted}")
when (state) {
TransferState.COMPLETED -> {
//此方法是上传图片完成后再打标签
// uploadClient.setObjectTagging(SetObjectTaggingRequest(stsToken.bucket, stsToken.path, ObjectTagging(listOf(Tag("temp", "1")))))
if (it.isActive) {
if (isImg) {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
val wid = options.outWidth
val hei = options.outHeight
val res = FileUpLoadRes(
LoadFileData(
isSuccess = true,
urlPath = urlPath,
width = wid,
height = hei,
filePath = filePath
),
FileOption(path, urlPath, ossType, wid, hei)
)
it.resumeWithActive(res)
} else {
val res = FileUpLoadRes(
LoadFileData(
isSuccess = true,
urlPath = urlPath,
filePath = filePath
)
)
it.resumeWithActive(res)
}
}
}
TransferState.FAILED, TransferState.CANCELED -> {
it.resumeWithActive(createNormalErrorFileData(filePath))
}
else -> {
}
}
}
override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {
Timber.d("oss上传 - AmazonS3 onProgressChanged - bytesTotal:${bytesTotal} - bytesCurrent:${bytesCurrent}")
}
override fun onError(id: Int, ex: Exception?) {
Timber.d("oss上传 - AmazonS3 onError:${ex?.localizedMessage} - id:$id")
}
}
when {
filePath.isNotEmpty() -> {
val file = File(filePath)
if (!file.exists()) {
it.cancel()
}
Timber.d("oss上传 - 上传文件大小 ${file.length() / 1024}")
transferUtility.upload(stsToken.bucket, path, file, obj)
.setTransferListener(transferListener)
}
}
}
suspend fun checkText(content: String): Response<Any> = ossRepository.checkText(content)
}

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,65 @@
package com.remax.visualnovel.configs
import android.app.Activity
import android.content.Context
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 lateinit var instance: NovelApplication
fun appContext(): Context = instance
}
private val proxies = listOf<ApplicationProxy>(CommonApplicationProxy)
@Inject
lateinit var appInitializersProvider: AppInitializersProvider
override fun onCreate() {
super.onCreate()
MultiDex.install(this)
instance = 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,12 @@
package com.remax.visualnovel.entity.imbean
import com.remax.visualnovel.entity.imbean.voice.IMVoice
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
/**
* Created by HJW on 2025/8/19
*/
data class IMAIInMessage(
override var message: V2NIMMessage?,
val imVoice: IMVoice
) : IMMessageWrapper(type = IN_TEXT_TYPE)

View File

@ -0,0 +1,10 @@
package com.remax.visualnovel.entity.imbean
import com.remax.visualnovel.entity.response.Character
/**
* Created by HJW on 2025/8/19
*/
data class IMBaseInfoMessage(
val character: Character?
) : IMMessageWrapper(type = BASE_INFO)

View File

@ -0,0 +1,12 @@
package com.remax.visualnovel.entity.imbean
import com.remax.visualnovel.entity.imbean.raw.CustomCallData
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
/**
* Created by HJW on 2025/8/19
*/
data class IMCallMessage(
override var message: V2NIMMessage?,
val call: CustomCallData?
) : IMMessageWrapper(type = OUT_CALL_TYPE)

View File

@ -0,0 +1,12 @@
package com.remax.visualnovel.entity.imbean
import com.remax.visualnovel.entity.imbean.raw.CustomGiftData
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
/**
* Created by HJW on 2025/8/19
*/
data class IMGiftMessage(
override var message: V2NIMMessage?,
val gift: CustomGiftData?
) : IMMessageWrapper(type = OUT_GIFT_TYPE)

View File

@ -0,0 +1,12 @@
package com.remax.visualnovel.entity.imbean
import com.remax.visualnovel.entity.imbean.raw.CustomAlbumData
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
/**
* Created by HJW on 2025/8/19
*/
data class IMInImageMessage(
override var message: V2NIMMessage?,
val albumData: CustomAlbumData?
) : IMMessageWrapper(type = IN_IMAGE_TYPE)

View File

@ -0,0 +1,12 @@
package com.remax.visualnovel.entity.imbean
import com.remax.visualnovel.entity.imbean.raw.CustomLevelChangeData
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
/**
* Created by HJW on 2025/8/19
*/
data class IMLevelMessage(
override var message: V2NIMMessage?,
val level: CustomLevelChangeData?
) : IMMessageWrapper(type = HEART_BEAT_CHANGED_TYPE)

View File

@ -0,0 +1,44 @@
package com.remax.visualnovel.entity.imbean
import com.remax.visualnovel.manager.nim.FetchResult
import com.remax.visualnovel.manager.nim.LoadStatus
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
/**
* Created by HJW on 2020/9/28
*/
open class IMMessageWrapper(
open var message: V2NIMMessage? = null,
var type: Int = OUT_TEXT_TYPE,
) {
/**
* 表示该消息后是否要加一条AI输入中的消息
*/
var aiIsSending: Boolean = true
var fetchType: FetchResult.FetchType = FetchResult.FetchType.Init
var loadStatus: LoadStatus = LoadStatus.Success
companion object {
const val BASE_INFO = 0
// ai回复中
const val INPUT_ING = 1
const val OUT_TEXT_TYPE = 2
const val IN_TEXT_TYPE = 3
const val OUT_IMAGE_TYPE = 4
const val IN_IMAGE_TYPE = 5
const val OUT_GIFT_TYPE = 6
const val OUT_CALL_TYPE = 7
/**
* 心动等级升级/降级
*/
const val HEART_BEAT_CHANGED_TYPE = 8
}
}

View File

@ -0,0 +1,12 @@
package com.remax.visualnovel.entity.imbean
import com.remax.visualnovel.entity.imbean.raw.CustomRawData
import com.netease.nimlib.sdk.v2.message.V2NIMMessage
/**
* Created by HJW on 2025/8/19
*/
data class IMOutImageMessage(
override var message: V2NIMMessage?,
val customRawData: CustomRawData?
) : IMMessageWrapper(type = OUT_IMAGE_TYPE)

View File

@ -0,0 +1,19 @@
package com.remax.visualnovel.entity.imbean
import com.netease.nimlib.sdk.v2.conversation.model.V2NIMConversation
import com.netease.nimlib.sdk.v2.utils.V2NIMConversationIdUtil
/**
* Created by HJW on 2020/10/9
*/
data class RecentContactWrapper(
var recentContact: V2NIMConversation
) {
val aiId: String
get() {
val targetId = V2NIMConversationIdUtil.conversationTargetId(recentContact.conversationId)
return targetId.substring(0, targetId.indexOf("@"))
}
}

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