diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..3d207df 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 946741c..07a0971 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -58,7 +58,7 @@ android {
manifestPlaceholders.putAll(mapOf("UMENG_CHANNEL" to name))
}
manifestPlaceholders.putAll(mapOf(
- "GETUI_APPID" to "40qbPjPkYs7TnVAYCX0Ig6",
+ "GETUI_APPID" to (project.findProperty("GETUI_APPID") as? String ?: ""),
"GT_INSTALL_CHANNEL" to "general",
))
}
@@ -145,11 +145,33 @@ dependencies {
implementation(libs.face.detection)
implementation(libs.android.gif.drawable)
implementation(libs.gif.encoder)
- implementation("com.caverock:androidsvg:1.4")
- implementation("io.github.lucksiege:pictureselector:v3.11.2")
- // 压缩库 (可选,建议长图拼接前先压缩防止OOM)
- implementation("io.github.lucksiege:compress:v3.11.2")
- implementation("io.github.leavesczy:matisse:2.3.0")
- implementation("com.github.moyuruaizawa:cropify:0.5.2")
+ //implementation("com.caverock:androidsvg:1.4")
+ implementation(libs.pictureselector)
+ implementation(libs.compress)
+ implementation(libs.matisse)
+ implementation(libs.cropify)
+ //noinspection GradleDynamicVersion
+ api("com.alipay.sdk:alipaysdk-android:+@aar")
+ implementation(libs.wechat.sdk) //微信
+ //Retrofit 依赖
+ implementation(libs.retrofit)
+ implementation(libs.retrofit.kotlin.serialization)
+ implementation (libs.retrofit.converter.gson)
+ implementation(libs.okhttp)
+ implementation(libs.okhttp.logging.interceptor)
+
+ //友盟
+ implementation (libs.umeng.umsdk.common)// 必选
+ implementation (libs.umeng.umsdk.asms)// 必选
+ implementation (libs.umeng.umsdk.apm) // U-APM包依赖(必选)
+ implementation (libs.umeng.umsdk.share.core)//分享核心库,必选
+ implementation (libs.umeng.umsdk.share.wx) //微信完整版
+ //分包
+ implementation (libs.tencent.helper) //腾讯分包
+ implementation (files("libs/channelsdk-0.2.2.aar")) //快手分包
+ implementation (files("libs/humesdk-1.0.0.aar")) //巨量分包
+
+ implementation(libs.android.cn.oaid) //获取手机设备id
+ implementation(libs.fastaes) //解密
}
\ No newline at end of file
diff --git a/app/libs/channelsdk-0.2.2.aar b/app/libs/channelsdk-0.2.2.aar
new file mode 100644
index 0000000..93ce247
Binary files /dev/null and b/app/libs/channelsdk-0.2.2.aar differ
diff --git a/app/libs/humesdk-1.0.0.aar b/app/libs/humesdk-1.0.0.aar
new file mode 100644
index 0000000..2db4091
Binary files /dev/null and b/app/libs/humesdk-1.0.0.aar differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1611682..3dd512f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:exported="true"
+ android:launchMode="singleTask">
@@ -56,6 +73,22 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/BaseApplication.kt b/app/src/main/java/com/img/rabbit/BaseApplication.kt
index af420a6..4b03f44 100644
--- a/app/src/main/java/com/img/rabbit/BaseApplication.kt
+++ b/app/src/main/java/com/img/rabbit/BaseApplication.kt
@@ -4,7 +4,11 @@ import android.app.Application
import android.util.Log
import com.img.rabbit.utils.NetworkMonitor
import com.g.gysdk.GYManager
+import com.img.rabbit.config.Constants
import com.tencent.mmkv.MMKV
+import com.umeng.analytics.MobclickAgent
+import com.umeng.commonsdk.UMConfigure
+import com.umeng.socialize.PlatformConfig
class BaseApplication : Application() {
@@ -18,6 +22,21 @@ class BaseApplication : Application() {
initMMKV()
// 初始化个推SDK
initGeTuiOneKeyLogin()
+ // 初始化友盟
+ initUM()
+ }
+
+ /**
+ * 初始化友盟
+ */
+ private fun initUM() {
+ UMConfigure.setLogEnabled(true)
+
+ PlatformConfig.setFileProvider("${BuildConfig.APPLICATION_ID}.fileprovider")
+ PlatformConfig.setWeixin(Constants.WechatAppId, Constants.WechatAppSecret)
+ MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO)
+
+ UMConfigure.setProcessEvent(true)
}
private fun initGeTuiOneKeyLogin() {
diff --git a/app/src/main/java/com/img/rabbit/MainActivity.kt b/app/src/main/java/com/img/rabbit/MainActivity.kt
index 7d3086e..1423157 100644
--- a/app/src/main/java/com/img/rabbit/MainActivity.kt
+++ b/app/src/main/java/com/img/rabbit/MainActivity.kt
@@ -1,5 +1,6 @@
package com.img.rabbit
+import android.app.Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -7,11 +8,21 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -21,23 +32,43 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.viewmodel.compose.viewModel
+import com.img.rabbit.config.Constants
+import com.img.rabbit.config.Constants.agreementUrl
+import com.img.rabbit.config.Constants.privacyUrl
import com.img.rabbit.pages.LoginScreen
+import com.img.rabbit.pages.LoginScreenType
import com.img.rabbit.pages.MainScreen
+import com.img.rabbit.provider.storage.PreferenceUtil
+import com.img.rabbit.utils.ChannelUtils
+import com.img.rabbit.utils.UrlLinkUtils.openAgreement
import com.img.rabbit.viewmodel.GeneralViewModel
import com.img.rabbit.viewmodel.LoginViewModel
import com.img.rabbit.viewmodel.SplashViewModel
+import com.umeng.analytics.MobclickAgent
+import com.umeng.commonsdk.UMConfigure
import kotlinx.coroutines.delay
+import kotlin.system.exitProcess
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 必须在 super.onCreate 之前调用
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
+ initUM()
// 启用Edge-to-Edge模式(沉浸模式)
enableEdgeToEdge()
@@ -46,6 +77,15 @@ class MainActivity : ComponentActivity() {
val splashViewModel: SplashViewModel = viewModel()
val generalViewModel: GeneralViewModel = viewModel()
val loginViewModel: LoginViewModel = viewModel()
+ val context = LocalContext.current
+ var showSplash by remember { mutableStateOf(false) }
+
+ //获取服务器时间
+ generalViewModel.getServerTime()
+ // 获取用户配置
+ loginViewModel.requestUserConfig()
+ //初始化微信登录
+ loginViewModel.initWXApi(this)
// 设置启动页显示条件
splashScreen.setKeepOnScreenCondition {
@@ -53,26 +93,69 @@ class MainActivity : ComponentActivity() {
}
AppTheme {
- SplashScreenContent {
- val token = generalViewModel.kv.decodeString("token")
- // 未登录,显示登录页
- if (token?.isNotEmpty() == false && !loginViewModel.isLogin.value) {
- // 显示登录页
- LoginScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel)
- } else {
- //已登录,显示主页面
- MainScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel)
+ SplashScreenContent{
+ //未同意提示政策弹窗
+ if (generalViewModel.agreementStatus.value == false){
+ //同意继续
+ PrivacyPolicyScreen(
+ viewModel = loginViewModel,
+ ) { isAllowPrivacyPolicy ->
+ if (isAllowPrivacyPolicy) {
+ generalViewModel.setIsAgreement(true)
+ showSplash = true
+ } else {
+ // 不同意隐私协议政策,直接退出应用
+ (context as MainActivity).finish()
+ // 强制退出应用进程
+ exitProcess(0)
+ }
+ }
+ }else{
+ showSplash = true
+ }
+
+ if(showSplash){
+ val token = PreferenceUtil.getAccessToken()
+ // 未登录,显示登录页
+ if (token.isNullOrEmpty() && !loginViewModel.isLogin.value) {
+ // 同意隐私协议政策,检验是否有一键登录权限
+ loginViewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen ->
+ if (isAllowShowOneKeyScreen) {
+ loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY
+ } else {
+ // 检验是否有一键登录权限失败,显示验证码登录
+ loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA
+ }
+ }
+
+ // 显示登录页
+ LoginScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel, isVisibilityBreak = false)
+ } else {
+ //已登录,显示主页面
+ MainScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel)
+ }
}
}
}
// 模拟加载过程,2秒后关闭启动页
LaunchedEffect(Unit) {
- delay(100L)
+ delay(500L)
splashViewModel.setLoading(false)
}
}
}
+
+
+
+ /**
+ * 初始化友盟
+ */
+ private fun initUM() {
+ UMConfigure.preInit(applicationContext, Constants.UmengAppkey, ChannelUtils.getChannel(applicationContext))
+ UMConfigure.init(this, Constants.UmengAppkey, ChannelUtils.getChannel(applicationContext), UMConfigure.DEVICE_TYPE_PHONE, "")
+ MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO)
+ }
}
@@ -128,6 +211,212 @@ fun SplashScreenContent(
}
+@Composable
+private fun PrivacyPolicyScreen(viewModel: LoginViewModel, onAgreementChange: (Boolean) -> Unit) {
+ val context = LocalContext.current
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ){
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color(0xCC000000))
+ ){
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 38.dp, vertical = 213.dp)
+ .align(Alignment.Center)
+ .background(Color.White, shape = RoundedCornerShape(26.dp))
+ ){
+ Image(
+ painter = painterResource(id = R.mipmap.ic_privacy_policy_top_mask),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxSize(),
+ alignment = Alignment.TopCenter
+ )
+
+ Column(
+ modifier = Modifier
+ .padding(top = 36.dp)
+ .align(Alignment.TopCenter)
+ ) {
+ Text(
+ text = "用户协议与隐私政策",
+ modifier = Modifier
+ .wrapContentSize()
+ .align(Alignment.CenterHorizontally),
+ fontWeight = FontWeight.Normal,
+ fontSize = 18.sp,
+ color = Color(0xFF1A1A1A)
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(24.dp)
+ )
+
+ val agreement = "请您务必审慎阅读、充分理解《服务协议》与《隐私政策》各条款,包括但不限于:为了更好的向您提供服务,我们需要访问您的相册、相机等。您可以阅读《隐私政策》了解详细信息。如果您同意,请点击下面同意按钮开始接受我们的服务。"
+ val annotatedText = buildAnnotatedString {
+ append(agreement)
+ val startIndexForService = agreement.indexOf("《服务协议》")
+ val startIndexPrivacy1 = agreement.indexOf("《隐私政策》")
+ val startIndexPrivacy2 = agreement.lastIndexOf("《隐私政策》")
+
+ val serviceLength = "《服务协议》".length
+ val privacyLength = "《服务协议》".length
+
+ // 高亮显示 "《服务协议》"
+ addStyle(
+ style = SpanStyle(
+ color = Color(0xFF0066CC),
+ textDecoration = TextDecoration.None
+ ),
+ start = startIndexForService, // "《服务协议》" 开始下标13
+ end = startIndexForService + serviceLength // "《服务协议》" 结束下标19
+ )
+ addStringAnnotation(
+ tag = "AGREEMENT",
+ annotation = "service_agreement",
+ start = startIndexForService,
+ end = startIndexForService + serviceLength
+ )
+
+ // 高亮显示 "《隐私政策》"
+ addStyle(
+ style = SpanStyle(
+ color = Color(0xFF0066CC),
+ textDecoration = TextDecoration.None
+ ),
+ start = startIndexPrivacy1, // "《隐私政策》" 开始下标20
+ end = startIndexPrivacy1 + privacyLength // "《隐私政策》" 结束下标26
+ )
+ addStringAnnotation(
+ tag = "PRIVACY",
+ annotation = "privacy_policy",
+ start = startIndexPrivacy1,
+ end = startIndexPrivacy1 + privacyLength
+ )
+
+ // 高亮显示 "《隐私政策》"
+ addStyle(
+ style = SpanStyle(
+ color = Color(0xFF0066CC),
+ textDecoration = TextDecoration.None
+ ),
+ start = startIndexPrivacy2, // "《隐私政策》" 开始下标
+ end = startIndexPrivacy2 + privacyLength // "《隐私政策》" 结束下标
+ )
+ addStringAnnotation(
+ tag = "PRIVACY",
+ annotation = "privacy_policy",
+ start = startIndexPrivacy2,
+ end = startIndexPrivacy2 + privacyLength
+ )
+ }
+
+ ClickableText(
+ text = annotatedText,
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .align(Alignment.CenterHorizontally)
+ .padding(horizontal = 12.dp),
+ style = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ color = Color(0xFF1A1A1A)
+ ),
+ onClick = { offset ->
+ val agreementAnnotation = annotatedText.getStringAnnotations("AGREEMENT", offset, offset).firstOrNull()
+ val privacyAnnotation = annotatedText.getStringAnnotations("PRIVACY", offset, offset).firstOrNull()
+
+ when {
+ agreementAnnotation != null -> {
+ openAgreement(context, "服务协议", agreementUrl, false)
+ }
+ privacyAnnotation != null -> {
+ openAgreement(context, "隐私政策", privacyUrl, false)
+ }
+ }
+ }
+ )
+
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .align(Alignment.BottomCenter)
+ ) {
+ //同意按钮,用户协议与隐私政策
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(start = 33.dp, end = 33.dp)
+ .background(
+ Color(0xFF252525),
+ shape = RoundedCornerShape(359.dp),
+ )
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ onAgreementChange(true)
+ viewModel.setIsPolicyAgreement(true)
+ }
+ ) {
+ Text(
+ "同意",
+ color = Color(0xFFC2FF43),
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier
+ .wrapContentWidth()
+ .wrapContentHeight()
+ .padding(vertical = 12.dp)
+ .align(Alignment.Center)
+ )
+ }
+ //不同按钮,意用户协议与隐私政策
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(start = 33.dp, end = 33.dp)
+ .background(
+ Color(0x00000000),
+ shape = RoundedCornerShape(359.dp),
+ )
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ onAgreementChange(false)
+ viewModel.setIsPolicyAgreement(false)
+ }
+ ) {
+ Text(
+ "不同意",
+ color = Color(0xFFAAAAAA),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier
+ .wrapContentWidth()
+ .wrapContentHeight()
+ .padding(vertical = 12.dp)
+ .align(Alignment.Center)
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
@Preview(showBackground = true)
@Composable
diff --git a/app/src/main/java/com/img/rabbit/WebViewActivity.kt b/app/src/main/java/com/img/rabbit/WebViewActivity.kt
new file mode 100644
index 0000000..c1c0a00
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/WebViewActivity.kt
@@ -0,0 +1,38 @@
+package com.img.rabbit
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+
+class WebViewActivity : AppCompatActivity() {
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+
+ // 启用Edge-to-Edge模式(沉浸模式)
+ enableEdgeToEdge()
+
+ setContentView(R.layout.layout_web)
+
+ val webView: WebView = findViewById(R.id.webView)
+ val ivWebBreak: ImageView = findViewById(R.id.iv_web_break)
+ val ivWebTitle: TextView = findViewById(R.id.iv_web_title)
+
+ val title = intent.getStringExtra("title") ?: ""
+ val url = intent.getStringExtra("url") ?: ""
+
+ ivWebBreak.setOnClickListener { finish() }
+ ivWebTitle.text = title
+
+ webView.settings.javaScriptEnabled = true
+ webView.webViewClient = WebViewClient()
+ webView.loadUrl(url)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/bean/UserInfo.kt b/app/src/main/java/com/img/rabbit/bean/UserInfo.kt
deleted file mode 100644
index fe4922f..0000000
--- a/app/src/main/java/com/img/rabbit/bean/UserInfo.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.img.rabbit.bean
-
-data class UserInfo(
- val id: Int,
- val name: String,
- val login: Boolean,
-)
diff --git a/app/src/main/java/com/img/rabbit/bean/local/AlipayBean.kt b/app/src/main/java/com/img/rabbit/bean/local/AlipayBean.kt
new file mode 100644
index 0000000..28a12f7
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/local/AlipayBean.kt
@@ -0,0 +1,14 @@
+package com.img.rabbit.bean.local
+
+data class AlipayBean(
+ val resultStatus: String?, // 状态码:9000-成功,6001-取消,4000-失败
+ val result: String?, // 包含 auth_code 等关键信息
+ val memo: String? // 提示语
+)
+fun Map.toAlipayResult(): AlipayBean {
+ return AlipayBean(
+ resultStatus = this["resultStatus"],
+ result = this["result"],
+ memo = this["memo"]
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/bean/ClothingBean.kt b/app/src/main/java/com/img/rabbit/bean/local/ClothingBean.kt
similarity index 86%
rename from app/src/main/java/com/img/rabbit/bean/ClothingBean.kt
rename to app/src/main/java/com/img/rabbit/bean/local/ClothingBean.kt
index bd26ebd..aec2e3d 100644
--- a/app/src/main/java/com/img/rabbit/bean/ClothingBean.kt
+++ b/app/src/main/java/com/img/rabbit/bean/local/ClothingBean.kt
@@ -1,4 +1,4 @@
-package com.img.rabbit.bean
+package com.img.rabbit.bean.local
data class ClothingBean(
//衣服索引(区分男女)
diff --git a/app/src/main/java/com/img/rabbit/bean/local/ErrorBean.kt b/app/src/main/java/com/img/rabbit/bean/local/ErrorBean.kt
new file mode 100644
index 0000000..d935ab3
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/local/ErrorBean.kt
@@ -0,0 +1,3 @@
+package com.img.rabbit.bean.local
+
+data class ErrorBean(var code: String,var message: String)
diff --git a/app/src/main/java/com/img/rabbit/bean/FormatBean.kt b/app/src/main/java/com/img/rabbit/bean/local/FormatBean.kt
similarity index 70%
rename from app/src/main/java/com/img/rabbit/bean/FormatBean.kt
rename to app/src/main/java/com/img/rabbit/bean/local/FormatBean.kt
index 8d5c80c..35a3281 100644
--- a/app/src/main/java/com/img/rabbit/bean/FormatBean.kt
+++ b/app/src/main/java/com/img/rabbit/bean/local/FormatBean.kt
@@ -1,4 +1,4 @@
-package com.img.rabbit.bean
+package com.img.rabbit.bean.local
data class FormatBean(
//格式id
diff --git a/app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt b/app/src/main/java/com/img/rabbit/bean/local/HairstyleBean.kt
similarity index 86%
rename from app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt
rename to app/src/main/java/com/img/rabbit/bean/local/HairstyleBean.kt
index 3079eeb..b74a162 100644
--- a/app/src/main/java/com/img/rabbit/bean/HairstyleBean.kt
+++ b/app/src/main/java/com/img/rabbit/bean/local/HairstyleBean.kt
@@ -1,4 +1,4 @@
-package com.img.rabbit.bean
+package com.img.rabbit.bean.local
data class HairstyleBean(
//发型索引(区分男女)
diff --git a/app/src/main/java/com/img/rabbit/bean/LongImageBean.kt b/app/src/main/java/com/img/rabbit/bean/local/LongImageBean.kt
similarity index 89%
rename from app/src/main/java/com/img/rabbit/bean/LongImageBean.kt
rename to app/src/main/java/com/img/rabbit/bean/local/LongImageBean.kt
index d83ab6a..5e6ae9d 100644
--- a/app/src/main/java/com/img/rabbit/bean/LongImageBean.kt
+++ b/app/src/main/java/com/img/rabbit/bean/local/LongImageBean.kt
@@ -1,4 +1,4 @@
-package com.img.rabbit.bean
+package com.img.rabbit.bean.local
import android.graphics.Bitmap
import android.net.Uri
diff --git a/app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt b/app/src/main/java/com/img/rabbit/bean/local/OnekeyPreLogin.kt
similarity index 94%
rename from app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt
rename to app/src/main/java/com/img/rabbit/bean/local/OnekeyPreLogin.kt
index 4708b1a..fcf1323 100644
--- a/app/src/main/java/com/img/rabbit/bean/OnekeyPreLogin.kt
+++ b/app/src/main/java/com/img/rabbit/bean/local/OnekeyPreLogin.kt
@@ -1,4 +1,4 @@
-package com.img.rabbit.bean
+package com.img.rabbit.bean.local
import kotlinx.serialization.Serializable
diff --git a/app/src/main/java/com/img/rabbit/bean/ResizeBean.kt b/app/src/main/java/com/img/rabbit/bean/local/ResizeBean.kt
similarity index 81%
rename from app/src/main/java/com/img/rabbit/bean/ResizeBean.kt
rename to app/src/main/java/com/img/rabbit/bean/local/ResizeBean.kt
index e8259ce..e0d8a5b 100644
--- a/app/src/main/java/com/img/rabbit/bean/ResizeBean.kt
+++ b/app/src/main/java/com/img/rabbit/bean/local/ResizeBean.kt
@@ -1,4 +1,4 @@
-package com.img.rabbit.bean
+package com.img.rabbit.bean.local
data class ResizeBean(
//尺寸id
diff --git a/app/src/main/java/com/img/rabbit/bean/local/UserInfo.kt b/app/src/main/java/com/img/rabbit/bean/local/UserInfo.kt
new file mode 100644
index 0000000..b74e058
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/local/UserInfo.kt
@@ -0,0 +1,9 @@
+package com.img.rabbit.bean.local
+
+data class UserInfo(
+ val user_id: Int,
+ val name: String,
+ val avater: String,
+ val token: String,
+ val login: Boolean,
+)
diff --git a/app/src/main/java/com/img/rabbit/bean/local/WxBean.kt b/app/src/main/java/com/img/rabbit/bean/local/WxBean.kt
new file mode 100644
index 0000000..efad729
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/local/WxBean.kt
@@ -0,0 +1,6 @@
+package com.img.rabbit.bean.local
+
+data class WxBean(
+ val code: String,
+ val state: String
+)
diff --git a/app/src/main/java/com/img/rabbit/bean/response/AlipayParamEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/AlipayParamEntity.kt
new file mode 100644
index 0000000..57b712d
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/response/AlipayParamEntity.kt
@@ -0,0 +1,8 @@
+package com.img.rabbit.bean.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class AlipayParamEntity(
+ val param: String = ""
+)
diff --git a/app/src/main/java/com/img/rabbit/bean/response/CaptchaCodeEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/CaptchaCodeEntity.kt
new file mode 100644
index 0000000..d832972
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/response/CaptchaCodeEntity.kt
@@ -0,0 +1,8 @@
+package com.img.rabbit.bean.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CaptchaCodeEntity(
+ val timestamp: String = ""
+)
diff --git a/app/src/main/java/com/img/rabbit/bean/response/ConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/ConfigEntity.kt
new file mode 100644
index 0000000..2a0587a
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/response/ConfigEntity.kt
@@ -0,0 +1,47 @@
+package com.img.rabbit.bean.response
+
+import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
+
+@Serializable
+class ConfigEntity {
+ @SerializedName("client.popup.display") //显示开关控制
+ var popupConfig: PopupConfigEntity? = null
+
+ @SerializedName("client.time.setting") //弹窗时间控制
+ var popupTimeConfig: PopupTimeConfigEntity? = null
+
+ @SerializedName("client.weixin.open.appid") //微信appid
+ var wxAppId: String = ""
+
+ @SerializedName("client.version.upgrade") //版本更新
+ var versionEntity: VersionEntity? = null
+
+ @SerializedName("client.weixin.share") //微信分享
+ var wxShareEntity: WxShareEntity? = null
+
+ @SerializedName("client.guide.enable")
+ var guideEnable: Boolean? = true //是否开启引导页
+
+ @SerializedName("client.pay.agreement") //是否显示支付协议
+ var payAgreementEnable: Boolean? = true
+
+ @SerializedName("client.login.type") //登录方式
+ var loginType: List? = emptyList()
+
+ @SerializedName("client.ad.switch") //广告总开关
+ var adSwitch: Boolean = false
+
+ @SerializedName("client.service.phone") //客服电话
+ var servicePhoneList: List = emptyList()
+
+ @SerializedName("client.chatwarning") //聊天安全提示
+ var chatWarning: String? = null
+
+ @SerializedName("client.travel.ad") //聊天安全提示
+ val travelAd: List = emptyList()
+
+ // 圈子-顶部banner占位图配置
+ @SerializedName("client.team.ad") //聊天安全提示
+ val teamAd: List = emptyList()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/bean/response/PopupConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/PopupConfigEntity.kt
new file mode 100644
index 0000000..ac66fdc
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/response/PopupConfigEntity.kt
@@ -0,0 +1,28 @@
+package com.img.rabbit.bean.response
+
+import kotlinx.serialization.Serializable
+
+
+@Serializable
+data class PopupConfigEntity(
+ val android_alipay: Boolean = true,
+ val android_wxpay: Boolean = true,
+ val guest_payment: Boolean = false,
+ val honor_tip_double_install: Boolean = false,
+ val huawei_map: Boolean = false,
+ val nonvip_search_results: Boolean = true,
+ val search_phone: Boolean = true,
+ val sos_entry: Boolean = true,
+ val timelimit_discount: Boolean = true,
+ val vip_back_popup: Boolean = true,
+ val vivo_login_before_payment: Boolean = false,
+ val chat_non_vip_Banned: Boolean = false,
+ val hot_group: Boolean = false,
+ val group_list: Boolean = false,
+ val search_location: Boolean = true,
+ val service_display: Boolean = true,
+ val home_tip_double_install: Boolean = false,
+ // Vip优惠全屏页面,默认不显示:从进入程序开始计算到弹出框的时间(当pay_pop=true时生效,需要PopupTimeConfigEntity下的pay_pop_time进行倒计时)
+ val pay_pop: Boolean = false,
+
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/bean/response/PopupTimeConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/PopupTimeConfigEntity.kt
new file mode 100644
index 0000000..512386d
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/response/PopupTimeConfigEntity.kt
@@ -0,0 +1,17 @@
+package com.img.rabbit.bean.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class PopupTimeConfigEntity(
+ val buy_vip_tip: Long = 45L,
+ val search_phone: Long = 7L,
+ val signup_tip: Long = 45L,
+ val min_discount: Long = 28L,
+ val min_member_tip: Long = 10000L,
+ val order_item_top: Int = 3,
+ /// 进入APP后是否弹出开通会员界面弹出,默认20s后显示
+ val pay_pop_time: Long = 20L,
+ //// 默认支付类型,1支付宝,2微信
+ val pay_pop_type: Int = 1,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/bean/response/UserConfigEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/UserConfigEntity.kt
new file mode 100644
index 0000000..c6a0017
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/response/UserConfigEntity.kt
@@ -0,0 +1,14 @@
+package com.img.rabbit.bean.response
+
+import kotlinx.serialization.Serializable
+
+
+@Serializable
+data class UserConfigEntity(
+ var token: String = "",
+ var temp: Boolean = false,
+ var name: String = "",
+ var user_id: String = "",
+ var nowtime: String = "",
+ var config: ConfigEntity? = null
+)
diff --git a/app/src/main/java/com/img/rabbit/bean/response/UserEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/UserEntity.kt
new file mode 100644
index 0000000..db9d590
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/response/UserEntity.kt
@@ -0,0 +1,14 @@
+package com.img.rabbit.bean.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class UserEntity {
+ val user_id: String = ""
+ val name: String = ""
+ val avater: String = ""
+ val token: String = ""
+
+ //是否登录,有本地登录后写入(非接口字段)
+ var isLogin: Boolean = false
+}
diff --git a/app/src/main/java/com/img/rabbit/bean/response/VersionEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/VersionEntity.kt
new file mode 100644
index 0000000..6f73feb
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/response/VersionEntity.kt
@@ -0,0 +1,15 @@
+package com.img.rabbit.bean.response
+
+import kotlinx.serialization.Serializable
+
+
+@Serializable
+class VersionEntity(
+ var description: String = "",
+ var force: Boolean = false,
+ var last_version_force: String = "",
+ var title: String = "",
+ var url: String = "",
+ var app_size: String = "",
+ var version: String = ""
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/bean/response/WxShareEntity.kt b/app/src/main/java/com/img/rabbit/bean/response/WxShareEntity.kt
new file mode 100644
index 0000000..5c3db64
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/bean/response/WxShareEntity.kt
@@ -0,0 +1,11 @@
+package com.img.rabbit.bean.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class WxShareEntity(
+ val content: String = "",
+ val image: String = "",
+ val link: String = "",
+ val title: String = ""
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt b/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt
index 015c715..040430b 100644
--- a/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt
+++ b/app/src/main/java/com/img/rabbit/components/DrawingBoard.kt
@@ -43,10 +43,10 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.img.rabbit.R
-import com.img.rabbit.bean.ClothingBean
-import com.img.rabbit.bean.FormatBean
-import com.img.rabbit.bean.HairstyleBean
-import com.img.rabbit.bean.ResizeBean
+import com.img.rabbit.bean.local.ClothingBean
+import com.img.rabbit.bean.local.FormatBean
+import com.img.rabbit.bean.local.HairstyleBean
+import com.img.rabbit.bean.local.ResizeBean
/**
* 底部画板(证件)选择器
diff --git a/app/src/main/java/com/img/rabbit/config/Common.kt b/app/src/main/java/com/img/rabbit/config/Common.kt
deleted file mode 100644
index 3dc9b14..0000000
--- a/app/src/main/java/com/img/rabbit/config/Common.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.img.rabbit.config
-
-object Common {
- const val privacyUrl = "https://www.baidu.com"
- const val agreementUrl = "https://www.baidu.com"
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/config/CommonData.kt b/app/src/main/java/com/img/rabbit/config/CommonData.kt
index 98a974c..4ff9b79 100644
--- a/app/src/main/java/com/img/rabbit/config/CommonData.kt
+++ b/app/src/main/java/com/img/rabbit/config/CommonData.kt
@@ -2,10 +2,10 @@ package com.img.rabbit.config
import androidx.compose.ui.graphics.Color
import com.img.rabbit.R
-import com.img.rabbit.bean.ClothingBean
-import com.img.rabbit.bean.FormatBean
-import com.img.rabbit.bean.HairstyleBean
-import com.img.rabbit.bean.ResizeBean
+import com.img.rabbit.bean.local.ClothingBean
+import com.img.rabbit.bean.local.FormatBean
+import com.img.rabbit.bean.local.HairstyleBean
+import com.img.rabbit.bean.local.ResizeBean
object CommonData {
//背景颜色
diff --git a/app/src/main/java/com/img/rabbit/config/Constants.kt b/app/src/main/java/com/img/rabbit/config/Constants.kt
new file mode 100644
index 0000000..cb33f07
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/config/Constants.kt
@@ -0,0 +1,19 @@
+package com.img.rabbit.config
+
+object Constants {
+ const val RELEASE_BASE_URL = "https://jitutu.batiao8.com" //release
+ const val DEBUG_BASE_URL = "https://jitutu.batiao8.com"
+ const val LOG_REQUEST = "RabbitRequest"
+ const val agreementUrl = "https://jitutu.batiao8.com/static/policy-jietutu/user.html"//用户协议
+ const val privacyUrl = "https://jitutu.batiao8.com/static/policy-jietutu/privacy-ios.html"//隐私政策
+
+ //const val getuiAppId = "40qbPjPkYs7TnVAYCX0Ig6"//个推appid (gradle.properties)
+ const val WechatAppId = "wx7d1a7d1507482cef"// 微信APPID
+ const val WechatAppSecret = ""//微信secret
+ const val UmengAppkey = ""//TODO 友盟appKey
+
+ //解密
+ const val AESDecrypt = "e4rOtnF8tJjtHO7ecZeJHN1rapED5ImB"
+ //加密字符
+ const val Signature = "xn08hYoizXhZ1zHP8DVqfCm2yHxPmhil"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/pages/LoginPage.kt b/app/src/main/java/com/img/rabbit/pages/LoginPage.kt
index 877d825..c111bb2 100644
--- a/app/src/main/java/com/img/rabbit/pages/LoginPage.kt
+++ b/app/src/main/java/com/img/rabbit/pages/LoginPage.kt
@@ -10,7 +10,6 @@ import android.view.LayoutInflater
import android.widget.CheckBox
import android.widget.TextView
import android.widget.Toast
-import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -28,7 +27,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
@@ -38,11 +36,13 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -64,6 +64,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
import com.img.rabbit.R
import com.img.rabbit.utils.AgreementTextHelper
import com.img.rabbit.viewmodel.GeneralViewModel
@@ -72,19 +73,25 @@ import com.g.gysdk.EloginActivityParam
import com.g.gysdk.GYManager
import com.g.gysdk.GYResponse
import com.g.gysdk.GyCallBack
-import com.img.rabbit.config.Common.agreementUrl
-import com.img.rabbit.config.Common.privacyUrl
+import com.img.rabbit.bean.local.toAlipayResult
+import com.img.rabbit.config.Constants.agreementUrl
+import com.img.rabbit.config.Constants.privacyUrl
import com.img.rabbit.pages.toolbar.TitleBar
+import com.img.rabbit.provider.storage.GlobalStateManager
+import com.img.rabbit.provider.storage.PreferenceUtil
+import com.img.rabbit.utils.StringUtils
import com.img.rabbit.utils.UrlLinkUtils.openAgreement
import kotlinx.coroutines.delay
+import org.json.JSONObject
+@SuppressLint("UnrememberedMutableState")
@Composable
-fun LoginScreen(navController: NavHostController? = null, generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel) {
+fun LoginScreen(navController: NavHostController? = null, generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel, isVisibilityBreak: Boolean) {
val context = LocalContext.current
- val scale = remember { Animatable(0f) }
val networkStatus by generalViewModel.networkStatus.observeAsState(initial = true)
var showNetworkDisconnected by remember { mutableStateOf(false) }
+
// 网络状态监听
LaunchedEffect(networkStatus) {
if (!networkStatus) {
@@ -94,8 +101,53 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
Log.w("NetworkStatus","网络已连接")
}
}
- Scaffold{
+ // 登录成功后,保存 token
+ LaunchedEffect(loginViewModel.loginState.value) {
+ if (loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token != null) {
+ //登录成功
+ PreferenceUtil.saveAccessToken(loginViewModel.loginState.value?.data?.token)
+ loginViewModel.setLogin(true)
+
+ Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show()
+ }else if(loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token == null){
+ //登录失败
+ loginViewModel.setLogin(false)
+ Log.w("LoginScreen","登录失败,无有效的Token")
+ Toast.makeText(context, "登录失败,请重新登录", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ var globalWxAuthorization by mutableStateOf(GlobalStateManager(context).globalWxAuthorizationFlow().collectAsState(initial = ""))
+ LaunchedEffect(globalWxAuthorization.value) {
+ globalWxAuthorization.value?.let { loginViewModel.requestWxLogin(it) }
+ }
+
+ LaunchedEffect(loginViewModel.authInfoForAlipay.value) {
+ if(loginViewModel.authInfoForAlipay.value.isEmpty()) return@LaunchedEffect
+ loginViewModel.loginWithAliPay(context){rawResult ->
+ Log.i("loginWithAliPay", "支付宝登录结果:$rawResult")
+ val alipayResult = rawResult.toAlipayResult()
+ // 处理支付宝登录结果
+ when (alipayResult.resultStatus) {
+ "9000" -> {
+ // 登录成功,result 字段中包含 auth_code
+ val authCode = StringUtils.parseAlipayResult(alipayResult.result ?: "")["auth_code"] ?: ""
+ loginViewModel.requestAlipayLogin(authCode)
+ }
+
+ "6001" -> {
+ "用户取消登录"
+ }
+
+ else -> {
+ alipayResult.memo ?: "登录失败"
+ }
+ }
+ }
+ }
+
+ Scaffold{
Box(
modifier = Modifier.fillMaxSize()
) {
@@ -109,78 +161,46 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
)
// 顶部栏
- TitleBar(navController = navController, paddingValues = it, title = "", showSave = false)
+ TitleBar(navController = navController, paddingValues = it, title = "", showSave = false, showBreak = isVisibilityBreak)
Column(
modifier = Modifier
.fillMaxSize()
) {
- when (loginViewModel.loginScreenType.value) {
- LoginScreenType.LOGIN_ONE_KEY -> {
- Box(
- modifier = Modifier.fillMaxSize()
- ) {
- // 检验是否有一键登录权限成功,显示一键登录页
+ Box(modifier = Modifier.fillMaxSize()){
+ when(loginViewModel.loginScreenType.value){
+ LoginScreenType.LOGIN_ONE_KEY -> {
+ //一键登录
OneKeyLoginScreen(context, loginViewModel, generalViewModel)
-
- // 其他登录方式
- Column (
- modifier = Modifier
- .fillMaxSize()
- .padding(top = 27.dp),
- verticalArrangement = Arrangement.Bottom
- ){
- OtherLoginBar(context = context, viewModel = loginViewModel)
- }
}
- }
- LoginScreenType.LOGIN_CAPTCHA -> {
- Box(
- modifier = Modifier.fillMaxSize()
- ) {
- // 显示验证码登录页
+ LoginScreenType.LOGIN_WX -> {
+ //微信登录
+ Box(modifier = Modifier.align(Alignment.Center).padding(bottom = 100.dp)){
+ WxLoginScreen(context, loginViewModel)
+ }
+
+ }
+ LoginScreenType.LOGIN_ALIPAY -> {
+ //支付宝登录
+ Box(modifier = Modifier.align(Alignment.Center).padding(bottom = 100.dp)){
+ AliPayLoginScreen(context, loginViewModel)
+ }
+
+ }
+ else -> {
+ //默认验证码登录
CaptchaLoginScreen(context, loginViewModel, generalViewModel)
-
-
- // 其他登录方式
- Column (
- modifier = Modifier
- .fillMaxSize()
- .padding(top = 27.dp),
- verticalArrangement = Arrangement.Bottom
- ){
- OtherLoginBar(context = context, viewModel = loginViewModel)
- }
}
}
- else -> {
- // 显示隐私协议政策(同意后才能继续登录)
- PrivacyPolicyScreen(viewModel = loginViewModel) { //isAllowPrivacyPolicy ->
- loginViewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen ->
- if (isAllowShowOneKeyScreen) {
- loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY
- } else {
- // 检验是否有一键登录权限失败,显示验证码登录
- loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA
- }
- }
- /*
- if (isAllowPrivacyPolicy) {
- // 同意隐私协议政策,检验是否有一键登录权限
- viewModel.oneKeyLoginForGeTuiSdk(context as Activity) { isAllowShowOneKeyScreen ->
- if (isAllowShowOneKeyScreen) {
- viewModel.loginScreenType.value = LoginScreenType.LOGIN_ONE_KEY
- } else {
- // 检验是否有一键登录权限失败,显示验证码登录
- viewModel.loginScreenType.value = LoginScreenType.LOGIN_CAPTCHA
- }
- }
- } else {
- // 不同意隐私协议政策,直接退出应用
- (context as MainActivity).onBackPressed()
- }
- */
- }
+
+ // 其他登录方式Bar
+ Column (
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 27.dp),
+ verticalArrangement = Arrangement.Bottom
+ ){
+ OtherLoginBar(context = context, viewModel = loginViewModel)
}
}
}
@@ -191,139 +211,21 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
}
if(showNetworkDisconnected){
if(!networkStatus){
- NetworkDisconnectedPage(onNetworkStatus = {
- if(it){
+ NetworkDisconnectedPage(onNetworkStatus = {isNetworkAvailable->
+ if(isNetworkAvailable){
Toast.makeText(context, "网络已连接", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(context, "网络已断开", Toast.LENGTH_SHORT).show()
}
- generalViewModel.setNetworkStatus(it)
+ generalViewModel.setNetworkStatus(isNetworkAvailable)
})
}
}
-
}
}
}
-
-@Composable
-private fun PrivacyPolicyScreen(viewModel: LoginViewModel, onAgreementChange: (Boolean) -> Unit) {
- val context = LocalContext.current
- Box(
- modifier = Modifier.fillMaxSize()
- ){
- // 其他登录方式
- Column (
- modifier = Modifier
- .fillMaxSize()
- .padding(top = 27.dp),
- verticalArrangement = Arrangement.Bottom
- ){
- OtherLoginBar(context = context, viewModel = viewModel)
- }
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(Color(0xCC000000))
- ){
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(horizontal = 38.dp, vertical = 213.dp)
- .align(Alignment.Center)
- .background(Color.White, shape = RoundedCornerShape(26.dp))
- ){
- Image(
- painter = painterResource(id = R.mipmap.ic_privacy_policy_top_mask),
- contentDescription = null,
- modifier = Modifier
- .fillMaxSize(),
- alignment = Alignment.TopCenter
- )
- Text(
- text = "用户协议与隐私政策",
- modifier = Modifier
- .padding(top = 54.dp)
- .align(Alignment.TopCenter),
- fontWeight = FontWeight.Normal,
- fontSize = 18.sp,
- color = Color(0xFF1A1A1A)
- )
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .wrapContentHeight()
- .align(Alignment.BottomCenter)
- ) {
- //同意按钮,用户协议与隐私政策
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .wrapContentHeight()
- .padding(start = 33.dp, end = 33.dp)
- .background(
- Color(0xFF252525),
- shape = RoundedCornerShape(359.dp),
- )
- .clickable(
- indication = null,
- interactionSource = remember { MutableInteractionSource() }
- ) {
- onAgreementChange(true)
- viewModel.setIsPolicyAgreement(true)
- }
- ) {
- Text(
- "同意",
- color = Color(0xFFC2FF43),
- fontSize = 16.sp,
- fontWeight = FontWeight.Bold,
- modifier = Modifier
- .wrapContentWidth()
- .wrapContentHeight()
- .padding(vertical = 12.dp)
- .align(Alignment.Center)
- )
- }
- //不同按钮,意用户协议与隐私政策
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .wrapContentHeight()
- .padding(start = 33.dp, end = 33.dp)
- .background(
- Color(0x00000000),
- shape = RoundedCornerShape(359.dp),
- )
- .clickable(
- indication = null,
- interactionSource = remember { MutableInteractionSource() }
- ) {
- onAgreementChange(false)
- viewModel.setIsPolicyAgreement(false)
- }
- ) {
- Text(
- "不同意",
- color = Color(0xFFAAAAAA),
- fontSize = 14.sp,
- fontWeight = FontWeight.Bold,
- modifier = Modifier
- .wrapContentWidth()
- .wrapContentHeight()
- .padding(vertical = 12.dp)
- .align(Alignment.Center)
- )
- }
- }
- }
- }
- }
-}
-
/**
* 验证码登录
*/
@@ -542,8 +444,8 @@ private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, gene
showToast = true
)
) {
- //TODO 请求验证码(请完善requestCaptcha函数)
- viewModel.requestCaptcha()
+ // 请求验证码(请完善requestCaptcha函数)
+ viewModel.requestCaptcha(viewModel.userName.value)
// 开始倒计时(倒计时应该在requestCaptcha完成后开始)
isCaptchaCountdown = true
@@ -597,12 +499,11 @@ private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, gene
showToast = true
)
) {
- //TODO 验证码登录请求
- Toast.makeText(context, "登录成功!", Toast.LENGTH_SHORT).show()
- //TODO 登录成功后,保存 token
- generalViewModel.kv.encode("token", "123232123231231")
- // 登录成功后,设置登录状态为 true
- viewModel.setLogin(true)
+ // 验证通过(通过验证码验证),请求登录
+ viewModel.requestLoginForCaptcha(
+ viewModel.userName.value,
+ viewModel.captcha.value
+ )
}
}
) {
@@ -685,11 +586,11 @@ private fun CaptchaLoginScreen(context: Context, viewModel: LoginViewModel, gene
when (annotation.tag) {
"USER_AGREEMENT" -> {
// 打开用户协议
- openAgreement(context, agreementUrl)
+ openAgreement(context = context, title = "用户协议", url = agreementUrl)
}
"PRIVACY_POLICY" -> {
// 打开隐私政策
- openAgreement(context, privacyUrl)
+ openAgreement(context = context, title = "隐私政策", url = privacyUrl)
}
}
}
@@ -783,9 +684,9 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel, gener
// 设置可点击的协议文本
AgreementTextHelper.setupAgreementTextView(agreementText, targets, agreementTextView, isUnderlineText = false) { agreementType ->
when (agreementType) {
- "serviceAgreement" -> openAgreement(context, privacyUrl)
- "userAgreement" -> openAgreement(context, agreementUrl)
- "privacyAgreement" -> openAgreement(context, privacyUrl)
+ "serviceAgreement" -> openAgreement(context = context, title = privacyName, url = privacyUrl)
+ "userAgreement" -> openAgreement(context = context, title = "用户协议", url = agreementUrl)
+ "privacyAgreement" -> openAgreement(context = context, title = "隐私政策", url = privacyUrl)
}
}
@@ -808,6 +709,357 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel, gener
}
}
+@Composable
+private fun WxLoginScreen(
+ context: Context,
+ viewModel: LoginViewModel,
+) {
+ Column {
+ Image(
+ painter = painterResource(id = R.mipmap.ic_launcher_logo),
+ contentDescription = null,
+ modifier = Modifier
+ .size(86.dp)
+ .align(Alignment.CenterHorizontally)
+ )
+ Text(
+ text = "截图兔",
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp,
+ color = Color(0xFF1A1A1A),
+ modifier = Modifier
+ .wrapContentWidth()
+ .wrapContentHeight()
+ .align(Alignment.CenterHorizontally)
+ )
+
+ Text(
+ text = "为了更好地为您提供服务,请先完成微信授权",
+ fontWeight = FontWeight.Bold,
+ fontSize = 12.sp,
+ color = Color(0xFFAAAAAA),
+ modifier = Modifier
+ .wrapContentWidth()
+ .wrapContentHeight()
+ .padding(start = 30.dp, end = 30.dp, top = 46.dp)
+ .align(Alignment.CenterHorizontally)
+ )
+
+ // 登录按钮
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(start = 30.dp, end = 30.dp, top = 8.dp)
+ .background(
+ Color(0xFF252525),
+ shape = RoundedCornerShape(359.dp),
+ )
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ // 启动微信验证,请求登录
+ if (viewModel.isPolicyAgreement.value) {
+ //TODO 打开微信登录
+ viewModel.loginWithWechat(context)
+ } else {
+ Toast.makeText(
+ context,
+ "请先同意用户协议和隐私政策",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ ) {
+ Row(
+ modifier = Modifier
+ .align(Alignment.Center)
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_wx_icon),
+ contentDescription = null,
+ modifier = Modifier
+ .size(32.dp)
+ .padding(start = 12.dp)
+ .align(Alignment.CenterVertically)
+ )
+
+ Text(
+ "微信授权登录",
+ color = Color(0xFFC2FF43),
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier
+ .padding(vertical = 12.dp)
+ )
+ }
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(start = 30.dp, end = 30.dp, top = 14.dp)
+ ) {
+ Checkbox(
+ checked = viewModel.isPolicyAgreement.value,
+ onCheckedChange = { isChecked ->
+ viewModel.setIsPolicyAgreement(isChecked)
+ },
+ modifier = Modifier
+ .size(16.dp)
+ .scale(0.35f)
+ .padding(start = 6.dp)
+ .background(
+ if (viewModel.isPolicyAgreement.value) Color(0xFF252525)
+ else Color.Transparent,
+ shape = RoundedCornerShape(36.dp)
+ )
+ .border(
+ width = 1.dp,
+ color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525)
+ else Color(0xFFCCCCCC),
+ shape = RoundedCornerShape(36.dp)
+ )
+ .align(Alignment.CenterVertically),
+ colors = androidx.compose.material3.CheckboxDefaults.colors(
+ checkedColor = Color.Transparent, // 隐藏默认背景
+ uncheckedColor = Color.Transparent, // 隐藏默认背景
+ checkmarkColor = Color.White
+ )
+
+ )
+ val annotatedText = buildAnnotatedString {
+ append("我已阅读并同意")
+
+ // 用户协议部分
+ pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement")
+ withStyle(style = SpanStyle(
+ color = Color(0xFF767676),
+ fontWeight = FontWeight.Bold,
+ //textDecoration = TextDecoration.Underline
+ )) {
+ append("《用户协议》")
+ }
+ pop()
+
+ append("和")
+
+ // 隐私政策部分
+ pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy")
+ withStyle(style = SpanStyle(
+ color = Color(0xFF767676),
+ fontWeight = FontWeight.Bold,
+ //textDecoration = TextDecoration.Underline
+ )) {
+ append("《隐私政策》")
+ }
+ pop()
+ }
+ ClickableText(
+ text = annotatedText,
+ onClick = { offset ->
+ annotatedText.getStringAnnotations(offset, offset)
+ .firstOrNull()?.let { annotation ->
+ when (annotation.tag) {
+ "USER_AGREEMENT" -> {
+ // 打开用户协议
+ openAgreement(context = context, title = "用户协议", url = agreementUrl)
+ }
+ "PRIVACY_POLICY" -> {
+ // 打开隐私政策
+ openAgreement(context = context, title = "隐私政策", url = privacyUrl)
+ }
+ }
+ }
+ },
+ style = androidx.compose.ui.text.TextStyle(
+ fontSize = 12.sp,
+ color = Color.Gray
+ ),
+ modifier = Modifier
+ .padding(start = 4.dp)
+ .align(Alignment.CenterVertically)
+ )
+ }
+ }
+}
+
+@Composable
+private fun AliPayLoginScreen(
+ context: Context,
+ viewModel: LoginViewModel,
+) {
+ Column{
+ Image(
+ painter = painterResource(id = R.mipmap.ic_launcher_logo),
+ contentDescription = null,
+ modifier = Modifier
+ .size(86.dp)
+ .align(Alignment.CenterHorizontally)
+ )
+ Text(
+ text = "截图兔",
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp,
+ color = Color(0xFF1A1A1A),
+ modifier = Modifier
+ .wrapContentWidth()
+ .wrapContentHeight()
+ .align(Alignment.CenterHorizontally)
+ )
+
+ Text(
+ text = "为了更好地为您提供服务,请先完成支付宝授权",
+ fontWeight = FontWeight.Bold,
+ fontSize = 12.sp,
+ color = Color(0xFFAAAAAA),
+ modifier = Modifier
+ .wrapContentWidth()
+ .wrapContentHeight()
+ .padding(start = 30.dp, end = 30.dp, top = 46.dp)
+ .align(Alignment.CenterHorizontally)
+ )
+ // 登录按钮
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(start = 30.dp, end = 30.dp, top = 8.dp)
+ .background(
+ Color(0xFF252525),
+ shape = RoundedCornerShape(359.dp),
+ )
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ // 启动支付宝验证,请求登录
+ if (viewModel.isPolicyAgreement.value) {
+ // 打开支付宝登录
+ viewModel.requestAliPayAuthParam()
+ } else {
+ Toast.makeText(
+ context,
+ "请先同意用户协议和隐私政策",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ ) {
+ Row(
+ modifier = Modifier
+ .align(Alignment.Center)
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_alipay_icon),
+ contentDescription = null,
+ modifier = Modifier
+ .size(32.dp)
+ .padding(start = 12.dp)
+ .align(Alignment.CenterVertically)
+ )
+
+ Text(
+ "支付宝授权登录",
+ color = Color(0xFFC2FF43),
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier
+ .padding(vertical = 12.dp)
+ )
+ }
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(start = 30.dp, end = 30.dp, top = 14.dp)
+ ) {
+ Checkbox(
+ checked = viewModel.isPolicyAgreement.value,
+ onCheckedChange = { isChecked ->
+ viewModel.setIsPolicyAgreement(isChecked)
+ },
+ modifier = Modifier
+ .size(16.dp)
+ .scale(0.35f)
+ .padding(start = 6.dp)
+ .background(
+ if (viewModel.isPolicyAgreement.value) Color(0xFF252525)
+ else Color.Transparent,
+ shape = RoundedCornerShape(36.dp)
+ )
+ .border(
+ width = 1.dp,
+ color = if (viewModel.isPolicyAgreement.value) Color(0xFF252525)
+ else Color(0xFFCCCCCC),
+ shape = RoundedCornerShape(36.dp)
+ )
+ .align(Alignment.CenterVertically),
+ colors = androidx.compose.material3.CheckboxDefaults.colors(
+ checkedColor = Color.Transparent, // 隐藏默认背景
+ uncheckedColor = Color.Transparent, // 隐藏默认背景
+ checkmarkColor = Color.White
+ )
+
+ )
+ val annotatedText = buildAnnotatedString {
+ append("我已阅读并同意")
+
+ // 用户协议部分
+ pushStringAnnotation(tag = "USER_AGREEMENT", annotation = "user_agreement")
+ withStyle(style = SpanStyle(
+ color = Color(0xFF767676),
+ fontWeight = FontWeight.Bold,
+ //textDecoration = TextDecoration.Underline
+ )) {
+ append("《用户协议》")
+ }
+ pop()
+
+ append("和")
+
+ // 隐私政策部分
+ pushStringAnnotation(tag = "PRIVACY_POLICY", annotation = "privacy_policy")
+ withStyle(style = SpanStyle(
+ color = Color(0xFF767676),
+ fontWeight = FontWeight.Bold,
+ //textDecoration = TextDecoration.Underline
+ )) {
+ append("《隐私政策》")
+ }
+ pop()
+ }
+ ClickableText(
+ text = annotatedText,
+ onClick = { offset ->
+ annotatedText.getStringAnnotations(offset, offset)
+ .firstOrNull()?.let { annotation ->
+ when (annotation.tag) {
+ "USER_AGREEMENT" -> {
+ // 打开用户协议
+ openAgreement(context = context, title = "用户协议", url = agreementUrl)
+ }
+ "PRIVACY_POLICY" -> {
+ // 打开隐私政策
+ openAgreement(context = context, title = "隐私政策", url = privacyUrl)
+ }
+ }
+ }
+ },
+ style = androidx.compose.ui.text.TextStyle(
+ fontSize = 12.sp,
+ color = Color.Gray
+ ),
+ modifier = Modifier
+ .padding(start = 4.dp)
+ .align(Alignment.CenterVertically)
+ )
+ }
+ }
+}
+
/**
* 一键登录页
* 至少包含号码栏(NumberTextview)、品牌露出(SloganTextview)、登录按钮(LoginButton)、隐私确认(PrivacyCheckbox)、隐私标题(PrivacyTextview)
@@ -993,7 +1245,7 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel) {
@Composable
private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) {
-
+ val scope = rememberCoroutineScope()
Column(
modifier = Modifier
.fillMaxWidth()
@@ -1057,8 +1309,20 @@ private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) {
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
- //TODO 打开微信登录
- Toast.makeText(context, "打开微信登录", Toast.LENGTH_SHORT).show()
+ /*
+ if (viewModel.isPolicyAgreement.value) {
+ //TODO 打开微信登录
+ viewModel.loginWithWechat(context)
+ } else {
+ Toast.makeText(
+ context,
+ "请先同意用户协议和隐私政策",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ */
+ // 微信登录
+ viewModel.loginScreenType.value = LoginScreenType.LOGIN_WX
}
)
Image(
@@ -1072,8 +1336,20 @@ private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) {
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
- //TODO 打开支付宝登录
- Toast.makeText(context, "打开支付宝登录", Toast.LENGTH_SHORT).show()
+ /*
+ if (viewModel.isPolicyAgreement.value) {
+ // 打开支付宝登录
+ viewModel.requestAliPayAuthParam()
+ } else {
+ Toast.makeText(
+ context,
+ "请先同意用户协议和隐私政策",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ */
+ // 支付宝登录
+ viewModel.loginScreenType.value = LoginScreenType.LOGIN_ALIPAY
}
)
Image(
@@ -1152,9 +1428,14 @@ private fun oneKeyLogin(
override fun onSuccess(response: GYResponse?) {
//TODO 登录成功,需要与后端交互
Log.i("OneKeyLogin", "onSuccess:$response")
- //TODO 登录成功后,保存 token
- generalViewModel.kv.encode("token", "123232123231231")
- viewModel.setLogin(true)
+ try {
+ val jsonObject = JSONObject(response?.msg?:"{}")
+ val data = jsonObject.getJSONObject("data")
+ val token = data.getString("token")
+ viewModel.requestOneKeyLogin(response?.gyuid?:"", token)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
}
override fun onFailed(p0: GYResponse?) {
@@ -1169,10 +1450,32 @@ enum class LoginScreenType {
LOGIN_NORMAL,
LOGIN_ONE_KEY,
LOGIN_CAPTCHA,
+ LOGIN_WX,
+ LOGIN_ALIPAY,
}
-@Preview
+@Preview(showBackground = true)
@Composable
private fun PreviewOneKeyLoginScreen() {
OneKeyLoginScreen(LocalContext.current, viewModel(), viewModel())
-}
\ No newline at end of file
+}
+
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewWxLoginScreen() {
+ WxLoginScreen(LocalContext.current, viewModel())
+}
+
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewAliPayLoginScreen() {
+ AliPayLoginScreen(LocalContext.current, viewModel())
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewLoginScreen() {
+ LoginScreen(navController = rememberNavController(), generalViewModel = viewModel(), loginViewModel = viewModel(), isVisibilityBreak = false)
+}
diff --git a/app/src/main/java/com/img/rabbit/pages/MainPage.kt b/app/src/main/java/com/img/rabbit/pages/MainPage.kt
index f8dca7d..37ed9d3 100644
--- a/app/src/main/java/com/img/rabbit/pages/MainPage.kt
+++ b/app/src/main/java/com/img/rabbit/pages/MainPage.kt
@@ -196,7 +196,8 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode
LoginScreen(
navController = navController,
generalViewModel = generalViewModel,
- loginViewModel = loginViewModel
+ loginViewModel = loginViewModel,
+ isVisibilityBreak = true
)
}
}
diff --git a/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt
index 51c4310..70efe43 100644
--- a/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt
+++ b/app/src/main/java/com/img/rabbit/pages/screen/MineScreen.kt
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -37,10 +38,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
+import coil3.compose.AsyncImage
import com.img.rabbit.R
+import com.img.rabbit.provider.storage.PreferenceUtil
import com.img.rabbit.viewmodel.GeneralViewModel
@Composable
@@ -50,6 +54,7 @@ fun MineScreen(
) {
val context = LocalContext.current
val vipMember by remember { mutableStateOf(false) }
+ val userInfo by remember { mutableStateOf(PreferenceUtil.loginUserInfo()) }
// 监听返回事件
val currentBackStackEntry = navController.currentBackStackEntry
@@ -62,6 +67,7 @@ fun MineScreen(
}
}
+
Box(
modifier = Modifier.fillMaxSize().background(Color(0xFFF9F9F9))
){
@@ -80,21 +86,41 @@ fun MineScreen(
Row(
modifier = Modifier.fillMaxWidth()
) {
- Image(
- painter = painterResource(id = R.mipmap.ic_user_avatar_default),
- contentDescription = null,
- contentScale = ContentScale.FillWidth,
- modifier = Modifier
- .size(64.dp)
- .clip(RoundedCornerShape(90.dp))
- .clickable(
- indication = null,
- interactionSource = remember { MutableInteractionSource() }
- ) {
- // 处理点击事件
- Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show()
- }
- )
+ if(userInfo == null){
+ Image(
+ painter = painterResource(id = R.mipmap.ic_user_avatar_default),
+ contentDescription = "用户头像",
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier
+ .size(64.dp)
+ .clip(RoundedCornerShape(90.dp))
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ // 处理点击事件
+ Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show()
+ }
+ )
+ }else{
+ AsyncImage(
+ model = userInfo?.avater,
+ contentDescription = "用户头像",
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier
+ .size(64.dp)
+ .clip(RoundedCornerShape(90.dp))
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) {
+ // 处理点击事件
+ Toast.makeText(context, "头像", Toast.LENGTH_SHORT).show()
+ },
+ fallback = painterResource(id = R.mipmap.ic_user_avatar_default),
+ error = painterResource(id = R.mipmap.ic_user_avatar_default)
+ )
+ }
Column(
modifier = Modifier
.padding(start = 16.dp)
@@ -103,28 +129,62 @@ fun MineScreen(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
- // 隐藏TabBar
- generalViewModel.setNavigationBarVisible(false)
- // 跳转登录页面
- navController.navigate("login")
+ if(userInfo == null){
+ // 隐藏TabBar
+ generalViewModel.setNavigationBarVisible(false)
+ // 跳转登录页面
+ navController.navigate("login")
+ } else {
+ //TODO 已登录,跳转个人信息页面
+ //navController.navigate("userInfo")
+ }
}
) {
Text(
- text = "登录/注册",
+ text = if(userInfo == null){ "登录/注册" }else{ userInfo?.name?:"" },
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF1A1A1A),
modifier = Modifier.wrapContentSize()
)
- Text(
- text = "登录体验更多功能哦~",
- fontSize = 14.sp,
- fontWeight = FontWeight.Bold,
- color = Color(0xFF767676),
+ Row(
modifier = Modifier
- .wrapContentSize()
.padding(top = 10.dp)
- )
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ){
+ // 点击复制ID
+ if (userInfo != null) {
+ val clipboardManager = android.content.Context.CLIPBOARD_SERVICE
+ val clipboard = context.getSystemService(clipboardManager) as android.content.ClipboardManager
+ val clip = android.content.ClipData.newPlainText("User ID", userInfo?.user_id)
+ clipboard.setPrimaryClip(clip)
+ Toast.makeText(context, "已复制到剪贴板", Toast.LENGTH_SHORT).show()
+ }
+ }
+ ){
+ Text(
+ text = if(userInfo == null){ "登录体验更多功能哦~" }else{ "ID:${userInfo?.user_id?:""}" },
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color(0xFF767676),
+ modifier = Modifier
+ .wrapContentSize()
+ )
+ Box(
+ modifier = Modifier
+ .size(4.dp)
+ )
+ Image(
+ painter = painterResource(id = R.mipmap.ic_copy),
+ contentDescription = null,
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier
+ .size(12.dp)
+ .align(Alignment.CenterVertically)
+ )
+ }
}
}
diff --git a/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt
index d2564f0..b26d1ae 100644
--- a/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt
+++ b/app/src/main/java/com/img/rabbit/pages/screen/make/CutoutScreen.kt
@@ -64,8 +64,8 @@ import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import coil3.compose.AsyncImage
import com.img.rabbit.R
-import com.img.rabbit.bean.ClothingBean
-import com.img.rabbit.bean.HairstyleBean
+import com.img.rabbit.bean.local.ClothingBean
+import com.img.rabbit.bean.local.HairstyleBean
import com.img.rabbit.components.AppearanceType
import com.img.rabbit.components.DrawingBoardPicker
import com.img.rabbit.config.CommonData.clothingForFemales
diff --git a/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt
index 4f3ca39..03e517b 100644
--- a/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt
+++ b/app/src/main/java/com/img/rabbit/pages/screen/make/LongImageScreen.kt
@@ -1,75 +1,46 @@
package com.img.rabbit.pages.screen.make
-import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
-import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.navigationBarsPadding
-import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.KeyboardArrowDown
-import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
-import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.NavController
-import com.img.rabbit.bean.LongImageBean
+import com.img.rabbit.bean.local.LongImageBean
import com.img.rabbit.pages.toolbar.TitleBar
import com.img.rabbit.utils.ExportFormat
import com.img.rabbit.utils.ImageUtils.getBitmapFromUri
diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt
index 5546738..fa11b97 100644
--- a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt
+++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AboutScreen.kt
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -34,8 +33,8 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.img.rabbit.R
-import com.img.rabbit.config.Common.agreementUrl
-import com.img.rabbit.config.Common.privacyUrl
+import com.img.rabbit.config.Constants.agreementUrl
+import com.img.rabbit.config.Constants.privacyUrl
import com.img.rabbit.pages.toolbar.TitleBar
import com.img.rabbit.utils.UrlLinkUtils.openAgreement
@@ -101,7 +100,7 @@ fun AboutScreen(navController: NavHostController) {
interactionSource = remember { MutableInteractionSource() }
) {
// 跳转用户协议页面
- openAgreement(context, agreementUrl)
+ openAgreement(context = context, title = "用户协议", url = agreementUrl)
},
verticalAlignment = Alignment.CenterVertically
) {
@@ -144,7 +143,7 @@ fun AboutScreen(navController: NavHostController) {
interactionSource = remember { MutableInteractionSource() }
) {
// 跳转隐私政策页面
- openAgreement(context, privacyUrl)
+ openAgreement(context = context, title = "隐私政策", url = privacyUrl)
},
verticalAlignment = Alignment.CenterVertically
) {
diff --git a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt
index 63db93b..03fab5c 100644
--- a/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt
+++ b/app/src/main/java/com/img/rabbit/pages/screen/mine/setting/AccountManagerScreen.kt
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Checkbox
-import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -40,7 +39,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.img.rabbit.R
-import com.img.rabbit.bean.UserInfo
+import com.img.rabbit.bean.local.UserInfo
import com.img.rabbit.pages.toolbar.TitleBar
@Composable
@@ -48,9 +47,9 @@ fun AccountManagerScreen(navController: NavHostController) {
val userList by remember {
mutableStateOf(
listOf(
- UserInfo(1, "张三", true),
- UserInfo(2, "李四", false),
- UserInfo(3, "王五", false),
+ UserInfo(1, "张三", "https://cdn.batiao8.com/jietutu/logo.png","",true),
+ UserInfo(2, "李四", "https://cdn.batiao8.com/jietutu/logo.png","",false),
+ UserInfo(3, "王五", "https://cdn.batiao8.com/jietutu/logo.png","",false),
)
)
}
diff --git a/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt b/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt
index d3ff909..bdcbc18 100644
--- a/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt
+++ b/app/src/main/java/com/img/rabbit/pages/toolbar/TitleBar.kt
@@ -31,7 +31,7 @@ import androidx.navigation.compose.rememberNavController
import com.img.rabbit.R
@Composable
-fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title: String? = "", showSave: Boolean = false, onSubmit: (() -> Unit)? = null) {
+fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title: String? = "", showSave: Boolean = false, showBreak: Boolean = true, onSubmit: (() -> Unit)? = null) {
Box(
modifier = Modifier.fillMaxWidth().padding(paddingValues)
){
@@ -40,17 +40,19 @@ fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title:
.fillMaxWidth().padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
- // 返回按钮
- Icon(
- painter = painterResource(id = R.mipmap.ic_back),
- contentDescription = "返回",
- modifier = Modifier
- .clickable(
- indication = null,
- interactionSource = remember { MutableInteractionSource() }
- ) { navController?.popBackStack() }
- .padding(end = 26.dp)
- )
+ if(showBreak){
+ // 返回按钮
+ Icon(
+ painter = painterResource(id = R.mipmap.ic_back),
+ contentDescription = "返回",
+ modifier = Modifier
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) { navController?.popBackStack() }
+ .padding(end = 26.dp)
+ )
+ }
Column(
modifier = Modifier.fillMaxWidth().weight(1f)
) {
diff --git a/app/src/main/java/com/img/rabbit/provider/api/ApiManager.kt b/app/src/main/java/com/img/rabbit/provider/api/ApiManager.kt
new file mode 100644
index 0000000..186c392
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/provider/api/ApiManager.kt
@@ -0,0 +1,110 @@
+@file:Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
+
+package com.img.rabbit.provider.api
+
+import com.img.rabbit.BuildConfig
+import com.img.rabbit.config.Constants
+import com.img.rabbit.provider.utils.HeaderInterceptor
+import com.img.rabbit.provider.utils.RequestInterceptor
+import com.img.rabbit.provider.utils.ResponseInterceptor
+import com.img.rabbit.provider.utils.getUnsafeOkHttpClient
+import com.img.rabbit.viewmodel.`interface`.ServiceVo
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+
+object ApiManager {
+ private lateinit var retrofit: Retrofit
+ private lateinit var unsafeRetrofit: Retrofit
+ lateinit var serviceVo: ServiceVo
+
+ init {
+ initialize()
+ }
+
+ // 初始化方法,抽离出来以便重新初始化
+ private fun initialize() {
+ // 获取基础URL并确保其包含http/https协议
+ val baseUrl = if (BuildConfig.DEBUG) {
+ Constants.DEBUG_BASE_URL
+ } else {
+ Constants.RELEASE_BASE_URL
+ }
+
+ if (!::retrofit.isInitialized) {
+ val client = OkHttpClient().newBuilder()
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(20, TimeUnit.SECONDS)
+ .writeTimeout(20, TimeUnit.SECONDS)
+ .addInterceptor(RequestInterceptor())
+ .addInterceptor(HeaderInterceptor())
+ .build()
+
+ retrofit = Retrofit.Builder()
+ .baseUrl(baseUrl)
+ .client(client)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ }
+
+ if (!::unsafeRetrofit.isInitialized) {
+ val client = getUnsafeOkHttpClient().newBuilder()
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(20, TimeUnit.SECONDS)
+ .writeTimeout(20, TimeUnit.SECONDS)
+ .addInterceptor(HeaderInterceptor())
+ .addInterceptor(RequestInterceptor())
+ .addInterceptor(ResponseInterceptor())
+ .build()
+
+ unsafeRetrofit = Retrofit.Builder()
+ .baseUrl(baseUrl)
+ .client(client)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ }
+
+ if (!::serviceVo.isInitialized) {
+ serviceVo = unsafeRetrofit.create(ServiceVo::class.java)
+ }
+ }
+
+ // 添加重新初始化方法,用于在baseUrl改变时调用
+ fun reinitialize() {
+ // 重置所有实例,使其在下一次访问时重新初始化
+ if (::retrofit.isInitialized) {
+ synchronized(this) {
+ // 标记为未初始化
+ @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+ (this as Object).getClass().getDeclaredField("retrofit").apply {
+ isAccessible = true
+ set(this@ApiManager, null)
+ }
+ }
+ }
+
+ if (::unsafeRetrofit.isInitialized) {
+ synchronized(this) {
+ @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+ (this as Object).getClass().getDeclaredField("unsafeRetrofit").apply {
+ isAccessible = true
+ set(this@ApiManager, null)
+ }
+ }
+ }
+
+ if (::serviceVo.isInitialized) {
+ synchronized(this) {
+ @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+ (this as Object).getClass().getDeclaredField("serviceVo").apply {
+ isAccessible = true
+ set(this@ApiManager, null)
+ }
+ }
+ }
+
+ // 重新初始化
+ initialize()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/provider/api/ResultVo.kt b/app/src/main/java/com/img/rabbit/provider/api/ResultVo.kt
new file mode 100644
index 0000000..8cf0621
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/provider/api/ResultVo.kt
@@ -0,0 +1,9 @@
+package com.img.rabbit.provider.api
+
+class ResultVo(val code: Int, val data: T) {
+ val status: Boolean
+ get() {
+ return code == 0
+ }
+ val message: String = ""
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/provider/storage/GlobalStateManager.kt b/app/src/main/java/com/img/rabbit/provider/storage/GlobalStateManager.kt
new file mode 100644
index 0000000..31550eb
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/provider/storage/GlobalStateManager.kt
@@ -0,0 +1,49 @@
+package com.img.rabbit.provider.storage
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+private val Context.storeData: DataStore by preferencesDataStore(name = "global_state")
+
+class GlobalStateManager(
+ private val context: Context
+) {
+ companion object {
+ private val GLOBAL_LOADING = booleanPreferencesKey("global_loading")
+ private val GLOBAL_WX_AUTHORIZATION = stringPreferencesKey("global_wx_authorization")
+ }
+
+ suspend fun storeGlobalLoading(value: Boolean) {
+ context.storeData.edit { preferences ->
+ preferences[GLOBAL_LOADING] = value
+ }
+ }
+ fun isGlobalLoadingFlow(): Flow {
+ return context.storeData.data.map {
+ preferences ->
+ preferences[GLOBAL_LOADING]
+ }
+ }
+
+
+
+ suspend fun storeGlobalWxAuthorization(value: String) {
+ context.storeData.edit { preferences ->
+ preferences[GLOBAL_WX_AUTHORIZATION] = value
+ }
+ }
+ fun globalWxAuthorizationFlow(): Flow {
+ return context.storeData.data.map {
+ preferences ->
+ preferences[GLOBAL_WX_AUTHORIZATION]
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/provider/storage/PreferenceUtil.kt b/app/src/main/java/com/img/rabbit/provider/storage/PreferenceUtil.kt
new file mode 100644
index 0000000..6defd3f
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/provider/storage/PreferenceUtil.kt
@@ -0,0 +1,93 @@
+package com.img.rabbit.provider.storage
+
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.img.rabbit.bean.response.UserEntity
+import com.tencent.mmkv.MMKV
+
+/**
+ * SharedPreferences工具类,用于简化数据持久化操作
+ */
+object PreferenceUtil {
+ private const val KEY_ACCESS_TOKEN = "access_token"
+ private const val KEY_X_TOKEN = "x_token"
+
+ private const val KEY_USER_INFO = "user_info"
+
+
+
+ // Gson实例
+ private val gson: Gson = GsonBuilder().create()
+
+ val kv by lazy { MMKV.defaultMMKV() }
+
+
+ //服务器时间和本地时间的偏移量
+ private var timeDiff = 0L
+
+
+ /**
+ * 保存AccessToken
+ */
+ fun saveAccessToken(token: String?) {
+ kv.encode(KEY_ACCESS_TOKEN, token)
+ }
+
+ /**
+ * 获取保存的AccessToken
+ */
+ fun getAccessToken(): String? {
+ return kv.decodeString(KEY_ACCESS_TOKEN, null)
+ }
+
+ fun saveXToken(token: String?) {
+ kv.encode(KEY_X_TOKEN, token)
+ }
+
+ fun getXToken(): String? {
+ return kv.decodeString(KEY_X_TOKEN, null)
+ }
+
+
+ fun getUserInfos(): MutableList? {
+ /**
+ *[{"user_id":"25","name":"手机用户0253","avater":"https://cdn.batiao8.com/jietutu/logo.png","token":"45731e27-d101-4ec3-975c-e665cf86a579"},{"user_id":"25","name":"手机用户0253","avater":"https://cdn.batiao8.com/jietutu/logo.png","token":"45731e27-d101-4ec3-975c-e665cf86a579"}]
+ */
+ return gson.fromJson(kv.decodeString(KEY_USER_INFO, "[]"), Array::class.java)?.toMutableList()
+ }
+
+ fun loginUserInfo(): UserEntity?{
+ return getUserInfos()?.find { it.isLogin }
+ }
+
+ fun saveUserInfo(userEntity: UserEntity) {
+ val userInfos = getUserInfos()?: mutableListOf()
+ val isContain = userInfos.find { it.user_id == userEntity.user_id } != null
+ if(!isContain){
+ userInfos.add(userEntity)
+ }
+
+ kv.encode(KEY_USER_INFO, gson.toJson(userInfos))
+ }
+
+ fun removeUserInfo(userEntity: UserEntity) {
+ val userInfos = getUserInfos()?: mutableListOf()
+ userInfos.removeIf { it.user_id == userEntity.user_id }
+ kv.encode(KEY_USER_INFO, gson.toJson(userInfos))
+ }
+
+ /**
+ * 清除所有数据
+ */
+ fun clearAll() {
+ kv.clearAll()
+ }
+
+ //真实的服务器时间
+ fun serverTimeMillis(): Long {
+ return System.currentTimeMillis() + timeDiff * 1000
+ }
+ fun setTimeDiff(timeDiff: Long) {
+ this.timeDiff = timeDiff
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/provider/utils/CustomX509TrustManager.kt b/app/src/main/java/com/img/rabbit/provider/utils/CustomX509TrustManager.kt
new file mode 100644
index 0000000..e9b6496
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/provider/utils/CustomX509TrustManager.kt
@@ -0,0 +1,53 @@
+package com.img.rabbit.provider.utils
+
+import android.annotation.SuppressLint
+import okhttp3.OkHttpClient
+import java.security.SecureRandom
+import java.security.cert.X509Certificate
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLContext
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
+
+@SuppressLint("CustomX509TrustManager")
+fun getUnsafeOkHttpClient(): OkHttpClient {
+ try {
+ // 创建一个信任所有证书的 TrustManager
+ val trustAllCerts = arrayOf(
+
+ object : X509TrustManager {
+ @SuppressLint("TrustAllX509TrustManager")
+ override fun checkClientTrusted(
+ chain: Array?,
+ authType: String?
+ ) {
+ }
+
+ @SuppressLint("TrustAllX509TrustManager")
+ override fun checkServerTrusted(
+ chain: Array?,
+ authType: String?
+ ) {
+ }
+
+ override fun getAcceptedIssuers(): Array =
+ arrayOf()
+ }
+ )
+
+ // 初始化 SSLContext
+ val sslContext = SSLContext.getInstance("TLS")
+ sslContext.init(null, trustAllCerts, SecureRandom())
+
+ // 创建一个允许所有主机名验证的 HostnameVerifier
+ val allHostsValid = HostnameVerifier { _, _ -> true }
+
+ // 创建 OkHttpClient 并配置 SSL 和主机名验证
+ return OkHttpClient.Builder()
+ .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
+ .hostnameVerifier(allHostsValid)
+ .build()
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/provider/utils/HeaderInterceptor.kt b/app/src/main/java/com/img/rabbit/provider/utils/HeaderInterceptor.kt
new file mode 100644
index 0000000..953283e
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/provider/utils/HeaderInterceptor.kt
@@ -0,0 +1,46 @@
+package com.img.rabbit.provider.utils
+
+import com.img.rabbit.provider.storage.PreferenceUtil
+import com.img.rabbit.BuildConfig
+import okhttp3.Interceptor
+import okhttp3.Response
+import java.io.IOException
+
+class HeaderInterceptor : Interceptor {
+
+ @Throws(IOException::class)
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val original = chain.request()
+ try {
+ val mBuilder = original.newBuilder()
+ val accessToken = PreferenceUtil.getXToken()?:""
+ mBuilder.header("x-token", accessToken)
+ mBuilder.header("x-platform", "android")
+ mBuilder.header("x-mobile-brand", android.os.Build.BRAND)
+ mBuilder.header("x-mobile-model", android.os.Build.MODEL)
+ mBuilder.header("x-package", BuildConfig.APPLICATION_ID)
+
+ val request = mBuilder
+ .method(original.method, original.body)
+ .build()
+
+ val response = chain.proceed(request)
+ /*
+ // 获取响应头
+ val headers = response.headers
+ // 处理响应头
+ headers.forEach { header ->
+ //Log.d(LOG_REQUEST, "ResponseHeader:${header.first} ${header.second}")
+ if (header.first == "Access-Token") {
+ authorization = header.second
+ }
+ }
+ */
+
+ return response
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return chain.proceed(original)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/provider/utils/RequestInterceptor.kt b/app/src/main/java/com/img/rabbit/provider/utils/RequestInterceptor.kt
new file mode 100644
index 0000000..980a03e
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/provider/utils/RequestInterceptor.kt
@@ -0,0 +1,177 @@
+package com.img.rabbit.provider.utils
+
+import android.text.TextUtils
+import android.util.Log
+import com.img.rabbit.BuildConfig
+import com.img.rabbit.config.Constants
+import com.img.rabbit.config.Constants.LOG_REQUEST
+import com.img.rabbit.provider.storage.PreferenceUtil
+import com.img.rabbit.utils.StringUtils
+import okhttp3.Interceptor
+import okhttp3.Response
+import okio.Buffer
+import java.io.IOException
+import java.nio.charset.StandardCharsets
+import java.text.SimpleDateFormat
+import java.util.Arrays
+import java.util.Date
+import java.util.Locale
+
+/**
+ * 加密数据
+ */
+class RequestInterceptor : Interceptor {
+ private val TAG = "Encryption"
+ private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val startTime = System.currentTimeMillis()
+ val request = chain.request()
+
+ // 记录请求信息
+ logRequest(request)
+
+ var modifiedRequest = request
+ val method = request.method.lowercase(Locale.getDefault()).trim { it <= ' ' }
+ val url = request.url
+ val apiPath = String.format("%s", url)
+
+ val baseUrl = if (BuildConfig.DEBUG) {
+ Constants.DEBUG_BASE_URL
+ } else {
+ Constants.RELEASE_BASE_URL
+ }
+
+ // 如果请求的不是服务端的接口则不用加密
+ if (!apiPath.startsWith(baseUrl)) {
+ val response = chain.proceed(modifiedRequest)
+ return response
+ }
+
+ /*
+ L.d("TAG-->>url=$apiPath")
+ //如果请求的不是服务端的接口则不用加密
+ if (!apiPath.startsWith(Constants.BaseUrl)) {
+ L.d("TAG-->>content-type=${request.headers["Content-Type"]}")
+ val requestBody = request.body
+ L.d("TAG-->>${requestBody.toString()}")
+ return chain.proceed(request)
+ }
+ */
+
+ var queryString = url.encodedQuery
+ queryString = if (!TextUtils.isEmpty(queryString)) {
+ (queryString + "&nonce=" + StringUtils.createUUID()) + "×tamp=" + PreferenceUtil.serverTimeMillis() / 1000
+ } else {
+ ("nonce=" + StringUtils.createUUID()) + "×tamp=" + PreferenceUtil.serverTimeMillis() / 1000
+ }
+ val sortQueryString = Arrays.stream(queryString.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray())
+ .sorted { obj: String, anotherString: String? -> obj.compareTo(anotherString!!) }
+ .reduce { x: String, y: String -> "$x&$y" }
+ .get()
+ //如果请求方式是get或者delete 则没有body
+ var paramsStr = ""
+ var bytes = ByteArray(0)
+ var signature: String? = ""
+ if (method == "put" || method == "post") {
+ val requestBody = request.body
+ Log.w(TAG, "TAG-->>${requestBody.toString()}")
+ val buffer = Buffer()
+ requestBody!!.writeTo(buffer)
+ bytes = StringUtils.addByte(bytes, buffer.readByteArray())
+ Log.w(TAG, "签名后bodyByte的长度为" + bytes.size)
+ paramsStr = String(bytes, StandardCharsets.UTF_8)
+ // paramsStr = buffer.readUtf8();
+ Log.w(TAG, "签名后body的长度为" + paramsStr.length)
+ }
+ signature = if (bytes.isNotEmpty()) {
+ Log.w(TAG, "当前的数组长度为----" + bytes.size)
+ StringUtils.getMD5Byte(
+ StringUtils.addByte(
+ StringUtils.addByte("$sortQueryString&".toByteArray(), bytes),
+ ("&" + StringUtils.getMD5String(Constants.Signature)).toByteArray()
+ )
+ )
+ } else {
+ StringUtils.getMD5String(sortQueryString + "&" + StringUtils.getMD5String(Constants.Signature))
+ }
+ val newUrl = apiPath.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + "?" + queryString + "&signature=" + signature
+ Log.w(TAG, "签名后的路径--->$newUrl")
+ /*
+ request = request.newBuilder().url(newUrl).build()
+ return chain.proceed(request)
+ */
+
+ modifiedRequest = request.newBuilder().url(newUrl).build()
+ val response = chain.proceed(modifiedRequest)
+ logResponse(response, startTime)
+ return response
+ }
+
+ //---------------------------------> 以下为格式化请求日志打印 <---------------------------------
+ private fun logRequest(request: okhttp3.Request) {
+ val timestamp = dateFormat.format(Date())
+ val method = request.method
+ val url = request.url.toString()
+
+ Log.i(LOG_REQUEST,"┌─────────────────────────────────────────────────────────────────────────────")
+ Log.i(LOG_REQUEST,"│ 📤 请求部分 [$timestamp]")
+ Log.i(LOG_REQUEST,"├─────────────────────────────────────────────────────────────────────────────")
+ Log.i(LOG_REQUEST,"│ -->方法: $method")
+ Log.i(LOG_REQUEST,"│ -->URL: $url")
+
+ // 记录请求体(如果有)
+ val requestBody = request.body
+ if (requestBody != null) {
+ try {
+ val buffer = Buffer()
+ requestBody.writeTo(buffer)
+ val bodyString = buffer.readUtf8()
+ if (bodyString.isNotEmpty()) {
+ Log.i(LOG_REQUEST,"│ -->请求参数:")
+ Log.i(LOG_REQUEST,"│ --> $bodyString")
+ }
+ } catch (e: IOException) {
+ Log.e(LOG_REQUEST,"│ -->读取请求体失败: ${e.message}")
+ }
+ }else{
+ Log.i(LOG_REQUEST,"│ -->无请求参数")
+ }
+ Log.i(LOG_REQUEST,"└─────────────────────────────────────────────────────────────────────────────")
+ }
+
+ private fun logResponse(response: Response, startTime: Long) {
+ val timestamp = dateFormat.format(Date())
+ val duration = System.currentTimeMillis() - startTime
+ val code = response.code
+ val message = response.message
+ val url = response.request.url.toString()
+ val apiPath = url.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0]
+
+ Log.i(LOG_REQUEST,"┌─────────────────────────────────────────────────────────────────────────────")
+ Log.i(LOG_REQUEST,"│ 📥 响应部分 [$timestamp]")
+ Log.i(LOG_REQUEST,"├─────────────────────────────────────────────────────────────────────────────")
+ Log.i(LOG_REQUEST,"│ URL: $url")
+ Log.i(LOG_REQUEST,"│ API: $apiPath")
+ Log.i(LOG_REQUEST,"│ 状态码: $code $message")
+ Log.i(LOG_REQUEST,"│ 耗时: ${duration}ms")
+
+ // 记录响应体
+ try {
+ val responseBody = response.peekBody(1024 * 1024L) // 最多读取1MB
+ val bodyString = responseBody.string()
+ if (bodyString.isNotEmpty()) {
+ Log.i(LOG_REQUEST,"│ 响应内容: (前${minOf(bodyString.length, 2000)}字符):")
+ val preview = if (bodyString.length > 2000) {
+ bodyString.take(2000) + "..."
+ } else {
+ bodyString
+ }
+ Log.i(LOG_REQUEST,"│ $preview")
+ }
+ } catch (e: IOException) {
+ Log.e(LOG_REQUEST,"│ 读取响应体失败: ${e.message}")
+ }
+ Log.i(LOG_REQUEST,"└─────────────────────────────────────────────────────────────────────────────")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/provider/utils/ResponseInterceptor.kt b/app/src/main/java/com/img/rabbit/provider/utils/ResponseInterceptor.kt
new file mode 100644
index 0000000..5f656c5
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/provider/utils/ResponseInterceptor.kt
@@ -0,0 +1,86 @@
+package com.img.rabbit.provider.utils
+
+import android.content.Intent
+import android.text.TextUtils
+import android.util.Log
+import com.img.rabbit.BuildConfig
+import com.img.rabbit.config.Constants
+import com.img.rabbit.utils.AESpkcs7paddingUtil
+import okhttp3.Interceptor
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
+import org.json.JSONObject
+import java.nio.charset.Charset
+import java.nio.charset.UnsupportedCharsetException
+
+
+/**
+ * 解密数据
+ */
+class ResponseInterceptor : Interceptor {
+ private val TAG = "Decode"
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+ val url = request.url
+ val apiPath = String.format("%s", url)
+ val baseUrl = if (BuildConfig.DEBUG) {
+ Constants.DEBUG_BASE_URL
+ } else {
+ Constants.RELEASE_BASE_URL
+ }
+ //如果请求的不是服务端的接口则不用加密
+ if (!apiPath.startsWith(baseUrl)) {
+ return chain.proceed(request)
+ }
+ val response = chain.proceed(request)
+ var charset = Charset.forName("UTF-8")
+ val contentType = response.header("Content-Type")
+ if (contentType != null && contentType.contains("application/json")) {
+ val responseBody = response.body
+ val source = responseBody.source()
+ source.request(Long.MAX_VALUE)
+ val buffer = source.buffer
+ val mediaType = responseBody.contentType()
+ if (mediaType != null) {
+ try {
+ charset = mediaType.charset(Charset.forName("UTF-8"))
+ } catch (e: UnsupportedCharsetException) {
+ e.printStackTrace()
+ }
+ }
+ val respBody = buffer.clone().readString(charset!!)
+ Log.w(TAG, "response=${respBody}")
+ if (TextUtils.isEmpty(respBody)) {
+ val responseCode = if (response.code == 400) 200 else response.code //兼容某些接口400的情况
+ return response.newBuilder().code(responseCode).body(responseBody).build()
+ }
+
+ val isEncrypt = JSONObject(respBody).optBoolean("encrypt")
+ Log.w(TAG, "是否需要解密:$isEncrypt")
+ if (!isEncrypt) { //如果不需要加密 直接返回
+ Log.w(TAG, "response=${response.body}")
+ val responseCode = if (response.code == 400) 200 else response.code //兼容某些接口400的情况
+ return response.newBuilder().code(responseCode).body(responseBody).build()
+ }
+ val decrybody = JSONObject(respBody).optString("data")
+ Log.w(TAG, "Json解析后的字符串为---->$decrybody")
+ var decryString: String?
+ try {
+ decryString = AESpkcs7paddingUtil.decryptNormal(decrybody, Constants.AESDecrypt)
+ Log.e("ResponseInterceptor", "解密后返回的字符串为---->$decryString")
+
+ //这里可以通过code处理token过去过期的情况
+ //val decCode = JSONObject(decryString).optInt("code")
+
+ //返回新创建的response
+ val responseCode = if (response.code == 400 && decrybody != null) 200 else response.code //兼容某些接口400的情况
+ return response.newBuilder().code(responseCode).body(decryString.toResponseBody("text/plain".toMediaType())).build()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ return response
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/utils/AESpkcs7paddingUtil.kt b/app/src/main/java/com/img/rabbit/utils/AESpkcs7paddingUtil.kt
new file mode 100644
index 0000000..e4578fe
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/utils/AESpkcs7paddingUtil.kt
@@ -0,0 +1,30 @@
+package com.img.rabbit.utils
+
+
+import android.util.Base64
+import io.github.fastaes.FastAES
+
+object AESpkcs7paddingUtil {
+
+ /**
+ * 编码格式
+ */
+ const val ENCODING = "utf-8"
+
+ /**
+ * AES解密
+ *
+ * @param encryptStr 加密后的密文
+ * @param key 密钥
+ * @return 源字符串
+ * @throws Exception
+ */
+ @Throws(Exception::class)
+ fun decryptNormal(encryptStr: String?, key: String): String {
+ val sourceBytes = Base64.decode(encryptStr, Base64.NO_WRAP)
+ val keyBytes = key.toByteArray(charset(ENCODING))
+ val plain: ByteArray = FastAES.decrypt(sourceBytes, keyBytes, key.substring(0, 16).toByteArray(charset(ENCODING)))
+ return String(plain)
+ }
+}
+
diff --git a/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java b/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java
index c339e10..08c19cd 100644
--- a/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java
+++ b/app/src/main/java/com/img/rabbit/utils/Bitmap2SVG.java
@@ -39,8 +39,8 @@ public class Bitmap2SVG
return true;
}
- private boolean trc_del;
- private PrintWriter pw;
+ private final boolean trc_del;
+ private final PrintWriter pw;
private int w, h;
private Bitmap2SVG( PrintWriter pw, boolean trc_delete )
@@ -116,11 +116,13 @@ public class Bitmap2SVG
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
+ assert os != null;
os.write(buffer, 0, len);
}
// 此时文件已带上正确的 MIME 类型存入相册
Toast.makeText(context, "SVG已保存", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
+ //noinspection CallToPrintStackTrace
e.printStackTrace();
}
}
diff --git a/app/src/main/java/com/img/rabbit/utils/ChannelUtils.kt b/app/src/main/java/com/img/rabbit/utils/ChannelUtils.kt
new file mode 100644
index 0000000..547fd5c
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/utils/ChannelUtils.kt
@@ -0,0 +1,56 @@
+package com.img.rabbit.utils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.text.TextUtils
+import com.bytedance.hume.readapk.HumeSDK
+import com.img.rabbit.BuildConfig
+import com.kwai.monitor.payload.TurboHelper
+import com.tencent.vasdolly.helper.ChannelReaderUtil
+
+
+object ChannelUtils {
+
+ fun getChannel(context: Context): String {
+ if (BuildConfig.DEBUG) {
+ MMKVUtils.put("app_channel", "test")
+ } else {
+ if (TextUtils.isEmpty(MMKVUtils.getString("app_channel"))) {
+ MMKVUtils.put("app_channel", getChannelBy(context))
+ }
+ }
+ return MMKVUtils.getString("app_channel") ?: ""
+ }
+
+ private fun getChannelBy(context: Context): String? {
+ val kuaishou = TurboHelper.getChannel(context)
+ if (!TextUtils.isEmpty(kuaishou)) {
+ return kuaishou
+ }
+ val tengxun = ChannelReaderUtil.getChannel(context)
+ if (!TextUtils.isEmpty(tengxun)) {
+ return tengxun
+ }
+ val juliang = HumeSDK.getChannel(context)
+ if (!TextUtils.isEmpty(juliang)) {
+ return juliang
+ }
+ return channel(context)
+ }
+
+ private fun channel(context: Context): String {
+ try {
+ val pm = context.packageManager
+ val appInfo = pm.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
+ val channel = appInfo.metaData.getString("UMENG_CHANNEL")
+ if (!TextUtils.isEmpty(channel)) {
+ return channel!!
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return ""
+ }
+
+}
+
diff --git a/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt b/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt
index 6b9c403..c9dfb6c 100644
--- a/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt
+++ b/app/src/main/java/com/img/rabbit/utils/ImageUtils.kt
@@ -20,10 +20,9 @@ import kotlin.apply
import android.graphics.*
import androidx.core.graphics.createBitmap
-import com.img.rabbit.bean.LongImageBean
+import com.img.rabbit.bean.local.LongImageBean
import java.io.ByteArrayOutputStream
import java.io.OutputStream
-import androidx.core.graphics.withClip
object ImageUtils {
fun decodeSampledBitmapFromResource(
diff --git a/app/src/main/java/com/img/rabbit/utils/MMKVUtils.kt b/app/src/main/java/com/img/rabbit/utils/MMKVUtils.kt
new file mode 100644
index 0000000..29214b1
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/utils/MMKVUtils.kt
@@ -0,0 +1,99 @@
+package com.img.rabbit.utils
+
+import android.os.Parcelable
+import com.tencent.mmkv.MMKV
+import java.util.Collections
+
+object MMKVUtils {
+
+ var mmkv: MMKV? = null
+
+ init {
+ mmkv = MMKV.defaultMMKV()
+ }
+
+ fun put(key: String, value: Any?): Boolean {
+ return when (value) {
+ is String -> mmkv?.encode(key, value)!!
+ is Float -> mmkv?.encode(key, value)!!
+ is Boolean -> mmkv?.encode(key, value)!!
+ is Int -> mmkv?.encode(key, value)!!
+ is Long -> mmkv?.encode(key, value)!!
+ is Double -> mmkv?.encode(key, value)!!
+ is ByteArray -> mmkv?.encode(key, value)!!
+ else -> false
+ }
+ }
+
+ /**
+ * 这里使用安卓自带的Parcelable序列化,它比java支持的Serializer序列化性能好些
+ */
+ fun put(key: String, t: T?): Boolean {
+ if (t == null) {
+ return false
+ }
+ return mmkv?.encode(key, t)!!
+ }
+
+ fun put(key: String, sets: Set?): Boolean {
+ if (sets == null) {
+ return false
+ }
+ return mmkv?.encode(key, sets)!!
+ }
+
+ fun getInt(key: String): Int? {
+ return mmkv?.decodeInt(key, 0)
+ }
+
+ fun getDouble(key: String): Double? {
+ return mmkv?.decodeDouble(key, 0.00)
+ }
+
+ fun getLong(key: String): Long? {
+ return mmkv?.decodeLong(key, 0L)
+ }
+
+ fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
+ return mmkv?.decodeBool(key, defaultValue) ?: defaultValue
+ }
+
+ fun getFloat(key: String): Float? {
+ return mmkv?.decodeFloat(key, 0F)
+ }
+
+ fun getByteArray(key: String): ByteArray? {
+ return mmkv?.decodeBytes(key)
+ }
+
+ fun getString(key: String): String? {
+ return mmkv?.decodeString(key, "")
+ }
+
+ /**
+ * SpUtils.getParcelable("")
+ */
+ inline fun getParcelable(key: String): T? {
+ return mmkv?.decodeParcelable(key, T::class.java)
+ }
+
+ fun getStringSet(key: String): Set? {
+ return mmkv?.decodeStringSet(key, Collections.emptySet())
+ }
+
+ /**
+ * 是否已经存在
+ */
+ fun contains(key: String): Boolean? {
+ return mmkv?.containsKey(key)
+ }
+
+ fun removeKey(key: String) {
+ mmkv?.removeValueForKey(key)
+ }
+
+ fun clearAll() {
+ mmkv?.clearAll()
+ }
+}
+
diff --git a/app/src/main/java/com/img/rabbit/utils/StringUtils.kt b/app/src/main/java/com/img/rabbit/utils/StringUtils.kt
new file mode 100644
index 0000000..f70907a
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/utils/StringUtils.kt
@@ -0,0 +1,494 @@
+package com.img.rabbit.utils
+
+import android.text.TextUtils
+import java.net.URLDecoder
+import java.security.MessageDigest
+import java.security.NoSuchAlgorithmException
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.GregorianCalendar
+import java.util.Hashtable
+import java.util.Locale
+import java.util.UUID
+import java.util.concurrent.ThreadLocalRandom
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+
+object StringUtils {
+ /**
+ * 功能:身份证的有效验证
+ *
+ * @param IDStr 身份证号
+ * @return 有效:返回"" 无效:返回String信息
+ * @throws ParseException
+ */
+ @Throws(ParseException::class)
+ fun IDCardValidate(IDStr: String): Boolean {
+ var errorInfo = "" // 记录错误信息
+ val ValCodeArr = arrayOf(
+ "1", "0", "x", "9", "8", "7", "6", "5", "4",
+ "3", "2"
+ )
+ val Wi = arrayOf(
+ "7", "9", "10", "5", "8", "4", "2", "1", "6", "3", "7",
+ "9", "10", "5", "8", "4", "2"
+ )
+ var Ai = ""
+ // ================号码的长度 15位或18位 ================
+ if (IDStr.length != 15 && IDStr.length != 18) {
+ errorInfo = "身份证号码长度应该为15位或18位。"
+ return false
+ }
+ // =======================(end)========================
+
+ // ================ 数字 除最后以为都为数字================
+ if (IDStr.length == 18) {
+ Ai = IDStr.substring(0, 17)
+ } else if (IDStr.length == 15) {
+ Ai = IDStr.substring(0, 6) + "19" + IDStr.substring(6, 15)
+ }
+ if (isNumeric(Ai) == false) {
+ errorInfo = "身份证15位号码都应为数字 ; 18位号码除最后一位外,都应为数字。"
+ return false
+ }
+ // =======================(end)========================
+
+ // ================ 出生年月是否有效 ================
+ val strYear = Ai.substring(6, 10) // 年份
+ val strMonth = Ai.substring(10, 12) // 月份
+ val strDay = Ai.substring(12, 14) // 月份
+ if (isDataFormat("$strYear-$strMonth-$strDay") == false) {
+ errorInfo = "身份证生日无效。"
+ return false
+ }
+ val gc = GregorianCalendar()
+ val s = SimpleDateFormat("yyyy-MM-dd")
+ try {
+ if (gc[Calendar.YEAR] - strYear.toInt() > 150
+ || gc.time.time - s.parse(
+ "$strYear-$strMonth-$strDay"
+ ).time < 0
+ ) {
+ errorInfo = "身份证生日不在有效范围。"
+ return false
+ }
+ } catch (e: NumberFormatException) {
+ // TODO Auto-generated catch block
+ e.printStackTrace()
+ } catch (e: ParseException) {
+ // TODO Auto-generated catch block
+ e.printStackTrace()
+ }
+ if (strMonth.toInt() > 12 || strMonth.toInt() == 0) {
+ errorInfo = "身份证月份无效"
+ return false
+ }
+ if (strDay.toInt() > 31 || strDay.toInt() == 0) {
+ errorInfo = "身份证日期无效"
+ return false
+ }
+ // =====================(end)=====================
+
+ // ================ 地区码时候有效================
+ val h = GetAreaCode()
+ if (h[Ai.substring(0, 2)] == null) {
+ errorInfo = "身份证地区编码错误。"
+ return false
+ }
+ // ==============================================
+
+ // ================ 判断最后一位的值================
+ var TotalmulAiWi = 0
+ for (i in 0..16) {
+ TotalmulAiWi = (TotalmulAiWi
+ + Ai[i].toString().toInt() * Wi[i].toInt())
+ }
+ val modValue = TotalmulAiWi % 11
+ val strVerifyCode = ValCodeArr[modValue]
+ Ai = Ai + strVerifyCode
+ if (IDStr.length == 18) {
+ if (Ai == IDStr == false) {
+ errorInfo = "身份证无效,不是合法的身份证号码"
+ return false
+ }
+ } else {
+ return true
+ }
+ // =====================(end)=====================
+ return true
+ }
+
+ /**
+ * 功能:判断字符串是否为数字
+ *
+ * @param str
+ * @return
+ */
+ private fun isNumeric(str: String): Boolean {
+ val pattern = Pattern.compile("[0-9]*")
+ val isNum = pattern.matcher(str)
+ return if (isNum.matches()) {
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * 功能:设置地区编码
+ *
+ * @return Hashtable 对象
+ */
+ private fun GetAreaCode(): Hashtable {
+ val hashtable = Hashtable()
+ hashtable["11"] = "北京"
+ hashtable["12"] = "天津"
+ hashtable["13"] = "河北"
+ hashtable["14"] = "山西"
+ hashtable["15"] = "内蒙古"
+ hashtable["21"] = "辽宁"
+ hashtable["22"] = "吉林"
+ hashtable["23"] = "黑龙江"
+ hashtable["31"] = "上海"
+ hashtable["32"] = "江苏"
+ hashtable["33"] = "浙江"
+ hashtable["34"] = "安徽"
+ hashtable["35"] = "福建"
+ hashtable["36"] = "江西"
+ hashtable["37"] = "山东"
+ hashtable["41"] = "河南"
+ hashtable["42"] = "湖北"
+ hashtable["43"] = "湖南"
+ hashtable["44"] = "广东"
+ hashtable["45"] = "广西"
+ hashtable["46"] = "海南"
+ hashtable["50"] = "重庆"
+ hashtable["51"] = "四川"
+ hashtable["52"] = "贵州"
+ hashtable["53"] = "云南"
+ hashtable["54"] = "西藏"
+ hashtable["61"] = "陕西"
+ hashtable["62"] = "甘肃"
+ hashtable["63"] = "青海"
+ hashtable["64"] = "宁夏"
+ hashtable["65"] = "新疆"
+ hashtable["71"] = "台湾"
+ hashtable["81"] = "香港"
+ hashtable["82"] = "澳门"
+ hashtable["91"] = "国外"
+ return hashtable
+ }
+
+ /**
+ * 验证日期字符串是否是YYYY-MM-DD格式
+ *
+ * @param str
+ * @return
+ */
+ private fun isDataFormat(str: String): Boolean {
+ var flag = false
+ // String
+ // regxStr="[1-9][0-9]{3}-[0-1][0-2]-((0[1-9])|([12][0-9])|(3[01]))";
+ val regxStr =
+ "^((\\d{2}(([02468][048])|([13579][26]))[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])))))|(\\d{2}(([02468][1235679])|([13579][01345789]))[\\-\\/\\s]?((((0?[13578])|(1[02]))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(3[01])))|(((0?[469])|(11))[\\-\\/\\s]?((0?[1-9])|([1-2][0-9])|(30)))|(0?2[\\-\\/\\s]?((0?[1-9])|(1[0-9])|(2[0-8]))))))(\\s(((0?[0-9])|([1-2][0-3]))\\:([0-5]?[0-9])((\\s)|(\\:([0-5]?[0-9])))))?$"
+ val pattern1 = Pattern.compile(regxStr)
+ val isNo = pattern1.matcher(str)
+ if (isNo.matches()) {
+ flag = true
+ }
+ return flag
+ }
+ //2.判断字符串是否是邮箱:
+ /**
+ * 描述:是否是邮箱.
+ *
+ * @param str 指定的字符串
+ * @return 是否是邮箱:是为true,否则false
+ */
+ fun isEmail(str: String): Boolean {
+ var isEmail = false
+ val expr = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$"
+ if (str.matches(expr.toRegex())) {
+ isEmail = true
+ }
+ return isEmail
+ }
+ //3.判断字符串是否是银行卡
+ /**
+ * 判断是否是银行卡号
+ *
+ * @param cardId
+ * @return
+ */
+ fun checkBankCard(cardId: String): Boolean {
+ val bit = getBankCardCheckCode(
+ cardId
+ .substring(0, cardId.length - 1)
+ )
+ return if (bit == 'N') {
+ false
+ } else cardId[cardId.length - 1] == bit
+ }
+
+ private fun getBankCardCheckCode(nonCheckCodeCardId: String?): Char {
+ if (nonCheckCodeCardId == null || nonCheckCodeCardId.trim { it <= ' ' }.length == 0 || !nonCheckCodeCardId.matches("\\d+".toRegex())) {
+ // 如果传的不是数据返回N
+ return 'N'
+ }
+ val chs = nonCheckCodeCardId.trim { it <= ' ' }.toCharArray()
+ var luhmSum = 0
+ var i = chs.size - 1
+ var j = 0
+ while (i >= 0) {
+ var k = chs[i].code - '0'.code
+ if (j % 2 == 0) {
+ k *= 2
+ k = k / 10 + k % 10
+ }
+ luhmSum += k
+ i--
+ j++
+ }
+ return if (luhmSum % 10 == 0) '0' else (10 - luhmSum % 10 + '0'.code).toChar()
+ }
+ //4、判断字符串是否是手机号
+ /**
+ * 判断是否是手机号
+ *
+ * @param phone
+ * @return
+ */
+ fun checkPhone(phone: String?): Boolean {
+ val pattern = Pattern
+ .compile("^(13[0-9]|15[0-3]|15[5-9]|18[0-9]|14[57]|17[0678])\\d{8}$")
+ val matcher = pattern.matcher(phone)
+ return if (matcher.matches()) {
+ true
+ } else false
+ }
+ //5.判断字符串是否是中文或者包含中文
+ /**
+ * 描述:判断一个字符串是否为null或空值.
+ *
+ * @param str 指定的字符串
+ * @return true or false
+ */
+ fun isEmpty(str: String?): Boolean {
+ return str == null || str.trim { it <= ' ' }.length == 0
+ }
+
+ /**
+ * 描述:是否是中文.
+ *
+ * @param str 指定的字符串
+ * @return 是否是中文:是为true,否则false
+ */
+ fun isChinese(str: String): Boolean {
+ var isChinese = true
+ val chinese = "[\u0391-\uFFE5]"
+ if (!isEmpty(str)) {
+ //获取字段值的长度,如果含中文字符,则每个中文字符长度为2,否则为1
+ for (i in 0 until str.length) {
+ //获取一个字符
+ val temp = str.substring(i, i + 1)
+ //判断是否为中文字符
+ if (temp.matches(chinese.toRegex())) {
+ } else {
+ isChinese = false
+ }
+ }
+ }
+ return isChinese
+ }
+
+ /**
+ * 描述:是否包含中文.
+ *
+ * @param str 指定的字符串
+ * @return 是否包含中文:是为true,否则false
+ */
+ fun isContainChinese(str: String): Boolean {
+ var isChinese = false
+ val chinese = "[\u0391-\uFFE5]"
+ if (!isEmpty(str)) {
+ //获取字段值的长度,如果含中文字符,则每个中文字符长度为2,否则为1
+ for (i in 0 until str.length) {
+ //获取一个字符
+ val temp = str.substring(i, i + 1)
+ //判断是否为中文字符
+ if (temp.matches(chinese.toRegex())) {
+ isChinese = true
+ } else {
+ }
+ }
+ }
+ return isChinese
+ }
+
+ /**
+ * 比较两个String的list 是否改变过
+ *
+ * @param listNew
+ * @param listOld
+ * @return
+ */
+ fun compareList(listNew: List?, listOld: List?): Boolean {
+ return if (listOld == null && listNew == null) {
+ false
+ } else if (listOld != null && listNew != null) {
+ if (listNew.size != listOld.size) {
+ true
+ } else !(listOld.containsAll(listNew) && listNew.containsAll(listOld))
+ } else {
+ true
+ }
+ }
+
+ /**
+ * 比较两个String 是否改变过
+ *
+ * @param newStr
+ * @param oldStr
+ * @return
+ */
+ fun compareString(newStr: String?, oldStr: String?): Boolean {
+ return if (newStr == null && oldStr == null) {
+ false
+ } else if (newStr != null && oldStr != null) {
+ newStr != oldStr
+ } else {
+ true
+ }
+ }
+
+ /**
+ * 比较签名 是否改变过
+ *
+ * @param newStr
+ * @param oldStr
+ * @return
+ */
+ fun compareSign(newStr: String, oldStr: String): Boolean {
+ return if (TextUtils.isEmpty(oldStr)) { //当原始值没有的时候 无需验证 因为上报时需验证是否已签过字 而且当原始值有的时候 依据现有业务新值不可能清空 故无需再判断其他情况
+ true
+ } else newStr != oldStr
+ }
+
+ /**
+ * 随机生成一个UUID
+ */
+ fun createUUID(): String {
+ return UUID.randomUUID().toString()
+ }
+
+ fun createUUIDFromLong(): String {
+ return UUID(ThreadLocalRandom.current().nextLong(), ThreadLocalRandom.current().nextLong()).toString()
+ }
+
+ /**
+ * MD5加密
+ */
+ fun getMD5String(data: String): String? {
+ try {
+ val md = MessageDigest.getInstance("MD5")
+ md.update(data.toByteArray())
+ val result = md.digest()
+ val stringBuffer = StringBuffer()
+ for (i in result.indices) {
+ val hex = Integer.toHexString(0xff and result[i].toInt())
+ if (hex.length == 1) stringBuffer.append('0')
+ stringBuffer.append(hex)
+ }
+ return stringBuffer.toString()
+ } catch (e: NoSuchAlgorithmException) {
+ e.printStackTrace()
+ }
+ return null
+ }
+
+ /**
+ * 对数组进行MD5加密
+ *
+ * @param bytes
+ * @return
+ */
+ fun getMD5Byte(bytes: ByteArray?): String? {
+ try {
+ val md = MessageDigest.getInstance("MD5")
+ md.update(bytes)
+ val result = md.digest()
+ val stringBuffer = StringBuffer()
+ for (i in result.indices) {
+ val hex = Integer.toHexString(0xff and result[i].toInt())
+ if (hex.length == 1) stringBuffer.append('0')
+ stringBuffer.append(hex)
+ }
+ return stringBuffer.toString()
+ } catch (e: NoSuchAlgorithmException) {
+ e.printStackTrace()
+ }
+ return null
+ }
+
+ //两个数组进行相加
+ fun addByte(array1: ByteArray, array2: ByteArray): ByteArray {
+ val combined = ByteArray(array1.size + array2.size)
+ System.arraycopy(array1, 0, combined, 0, array1.size)
+ System.arraycopy(array2, 0, combined, array1.size, array2.size)
+ return combined
+ }
+
+ //格式化时间yyyy-mm-dd
+ fun getSimpleYYYYMMDD(str: String): String {
+ if (TextUtils.isEmpty(str)) return ""
+
+ val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA)
+ val date = simpleDateFormat.parse(str)
+ return simpleDateFormat.format(date!!)
+
+ }
+
+
+ /**
+ * 将字符串中的unicode字符转换为中文字符
+ */
+ fun convertUnicodeToCh(str: String): String {
+ var newStr = str
+ val pattern: Pattern = Pattern.compile("(\\\\u(\\w{4}))")
+ val matcher: Matcher = pattern.matcher(newStr)
+
+ // 迭代,将str中的所有unicode转换为正常字符
+ while (matcher.find()) {
+ val unicodeFull = matcher.group(1) // 匹配出的每个字的unicode,比如\u83b7
+ val unicodeNum = matcher.group(2) // 匹配出每个字的数字,比如\u83b7,会匹配出u83b7
+
+ // 将匹配出的数字按照16进制转换为10进制,转换为char类型,就是对应的正常字符了
+ @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
+ val singleChar = unicodeNum.toInt(16).toChar()
+
+ // 替换原始字符串中的unicode码
+ @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
+ newStr = newStr.replace(unicodeFull, singleChar.toString() + "")
+ }
+ return newStr
+ }
+
+ // 解析函数
+ fun parseAlipayResult(rawResult: String): Map {
+ val resultMap = mutableMapOf()
+ val pairs = rawResult.split("&")
+
+ for (pair in pairs) {
+ val keyValue = pair.split("=")
+ if (keyValue.size == 2) {
+ val key = URLDecoder.decode(keyValue[0], "UTF-8")
+ val value = URLDecoder.decode(keyValue[1], "UTF-8")
+ resultMap[key] = value
+ }
+ }
+ return resultMap
+ }
+}
+
diff --git a/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt b/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt
index ce376fa..ed09ad8 100644
--- a/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt
+++ b/app/src/main/java/com/img/rabbit/utils/UrlLinkUtils.kt
@@ -3,13 +3,22 @@ package com.img.rabbit.utils
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
+import com.img.rabbit.WebViewActivity
object UrlLinkUtils {
- fun openAgreement(context: Context, url: String) {
- // 打开服务协议
- Intent(Intent.ACTION_VIEW, url.toUri()).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }.let { intent ->
+ fun openAgreement(context: Context, title: String, url: String, isExternal:Boolean = false) {
+ if(isExternal){
+ // 打开服务协议
+ Intent(Intent.ACTION_VIEW, url.toUri()).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }.let { intent ->
+ context.startActivity(intent)
+ }
+ }else{
+ val intent = Intent(context, WebViewActivity::class.java).apply {
+ putExtra("url", url)
+ putExtra("title", title)
+ }
context.startActivity(intent)
}
}
diff --git a/app/src/main/java/com/img/rabbit/viewmodel/BaseViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/BaseViewModel.kt
new file mode 100644
index 0000000..10c68e5
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/viewmodel/BaseViewModel.kt
@@ -0,0 +1,26 @@
+package com.img.rabbit.viewmodel
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+open class BaseViewModel : ViewModel() {
+ var isShowMsg = mutableStateOf(false)
+ var msgContent = mutableStateOf("")
+ val isLoading = mutableStateOf(false)
+
+ fun mLaunch(block: suspend () -> Unit) {
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ block()
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main){
+ isShowMsg.value = true
+ msgContent.value = "接口请求失败"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt
index 2bd5445..f462ce6 100644
--- a/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt
+++ b/app/src/main/java/com/img/rabbit/viewmodel/GeneralViewModel.kt
@@ -11,7 +11,12 @@ import android.os.Build
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import com.img.rabbit.provider.api.ApiManager
+import com.img.rabbit.provider.storage.PreferenceUtil
import com.tencent.mmkv.MMKV
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
@SuppressLint("ObsoleteSdkInt")
class GeneralViewModel(application: Application) : AndroidViewModel(application) {
@@ -38,6 +43,12 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application)
}
}
+ private val _agreementStatus = MutableLiveData()
+ val agreementStatus: LiveData = _agreementStatus
+ private fun getIsAgreement(): Boolean{
+ return kv.getBoolean("isAgreement", false)
+ }
+
init {
// 注册网络监听
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -51,6 +62,8 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application)
// 初始化状态
_networkStatus.value = isNetworkAvailable()
+ // 初始化隐私政策状态
+ _agreementStatus.value = getIsAgreement()
}
private fun isNetworkAvailable(): Boolean {
@@ -69,8 +82,23 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application)
_isNavigationBarVisible.value = visible
}
+ fun setIsAgreement(agreement: Boolean){
+ kv.putBoolean("isAgreement", agreement)
+ _agreementStatus.value = agreement
+ }
+
override fun onCleared() {
super.onCleared()
connectivityManager.unregisterNetworkCallback(networkCallback)
}
+
+ @OptIn(DelicateCoroutinesApi::class)
+ fun getServerTime() {
+ GlobalScope.launch {
+ val response = ApiManager.serviceVo.getServerTime()
+ if (response.status) {
+ PreferenceUtil.setTimeDiff(response.data - System.currentTimeMillis() / 1000)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt b/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt
index c42d8e9..2c3f7bf 100644
--- a/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt
+++ b/app/src/main/java/com/img/rabbit/viewmodel/LoginViewModel.kt
@@ -1,29 +1,69 @@
package com.img.rabbit.viewmodel
import android.app.Activity
+import android.content.Context
+import android.os.Build
import android.util.Log
+import android.widget.Toast
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
-import androidx.lifecycle.ViewModel
import androidx.compose.runtime.State
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.alipay.sdk.app.AuthTask
import com.img.rabbit.pages.LoginScreenType
import com.g.gysdk.GYManager
import com.g.gysdk.GYResponse
import com.g.gysdk.GyCallBack
import com.g.gysdk.GyConfig
-import com.img.rabbit.bean.OnekeyPreLogin
+import com.github.gzuliyujiang.oaid.DeviceIdentifier
+import com.google.gson.Gson
+import com.google.gson.JsonObject
+import com.img.rabbit.bean.local.ErrorBean
+import com.img.rabbit.bean.local.OnekeyPreLogin
+import com.img.rabbit.bean.local.WxBean
+import com.img.rabbit.bean.response.UserEntity
+import com.img.rabbit.bean.response.UserConfigEntity
+import com.img.rabbit.config.Constants
+import com.img.rabbit.provider.api.ApiManager
+import com.img.rabbit.provider.api.ResultVo
+import com.img.rabbit.provider.storage.PreferenceUtil
+import com.img.rabbit.utils.MMKVUtils
+import com.tencent.mm.opensdk.modelmsg.SendAuth
+import com.tencent.mm.opensdk.openapi.IWXAPI
+import com.tencent.mm.opensdk.openapi.WXAPIFactory
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.internal.platform.PlatformRegistry.applicationContext
-class LoginViewModel : ViewModel() {
+class LoginViewModel : BaseViewModel() {
private val TAG = "LoginViewModel"
private val ONEKEY_TAG = "OneKeyLoginViewModel"
+
+ private lateinit var api: IWXAPI
+
+ private val _wxState = MutableLiveData()
+ val wxState: LiveData = _wxState
+
+ fun updateWxState(newState: WxBean) {
+ _wxState.value = newState
+ }
+
+// private val _authInfo = MutableLiveData()
+ val authInfoForAlipay: MutableState = mutableStateOf("")
+
val loginScreenType = mutableStateOf(LoginScreenType.LOGIN_NORMAL)
// 登录用户名
val userName = mutableStateOf("")
// 登录验证码
val captcha = mutableStateOf("")
+ // 登录验证码发送时间戳
+ val captchaTimestamp = mutableStateOf("")
// 是否同意政策协议
- private val _policyAgreement = mutableStateOf(false)
+ private val _policyAgreement = mutableStateOf(true)
val isPolicyAgreement: State = _policyAgreement
@@ -44,7 +84,10 @@ class LoginViewModel : ViewModel() {
_policyAgreement.value = isAgreement
}
+ //用户配置
+ val _userConfig = MutableLiveData()
+ val userConfig: UserConfigEntity? get() = _userConfig.value
private val _isLogin = mutableStateOf(false)
val isLogin: State = _isLogin
@@ -53,6 +96,31 @@ class LoginViewModel : ViewModel() {
_isLogin.value = isLogin
}
+
+
+ val loginState = mutableStateOf?>(null)
+ val errorState = mutableStateOf(null)
+
+ fun requestUserConfig(){
+ mLaunch {
+ val oaid = MMKVUtils.getString("oaid") ?: ""
+ val response = ApiManager.serviceVo.getUserConfig(oaid, Build.VERSION.SDK_INT, "", DeviceIdentifier.getAndroidID(applicationContext), MMKVUtils.getString("gt_cid") ?: "")
+ if (response.status) {
+ PreferenceUtil.saveXToken(response.data.token)
+ PreferenceUtil.setTimeDiff(response.data.nowtime.toLong() - System.currentTimeMillis() / 1000)
+ _userConfig.postValue(response.data)
+
+ val resultJson = Gson().toJson(response.data)
+ MMKVUtils.put("userConfig", resultJson)
+
+ Log.w("LoginViewModel", "获取配置成功: $resultJson")
+ }else{
+ Log.w("LoginViewModel", "获取配置失败: code=${response.code}, message=${response.message}")
+ }
+ isLoading.value = false // 加载完成
+ }
+ }
+
fun oneKeyLoginForGeTuiSdk(activity: Activity, onShowOneKeyScreen:(Boolean)->Unit) {
// 初始化 SDK
GYManager.getInstance().init(GyConfig.with(activity.applicationContext).callBack(object : GyCallBack {
@@ -141,11 +209,203 @@ class LoginViewModel : ViewModel() {
}
}
+ fun requestOneKeyLogin(gyuid: String, token: String) {
+ isLoading.value = true // 开始加载
+
+ // 调用 API 获取数据
+ val jsonOneKey = JsonObject()
+// jsonOneKey.addProperty("gyuid", gyuid)
+// jsonOneKey.addProperty("token", token)
+// val jsonObject = JsonObject()
+// jsonObject.addProperty("login_type", "onekey")
+// jsonObject.add("onekey", jsonOneKey)
+// jsonObject.addProperty("bind", false)
+
+ jsonOneKey.addProperty("gyuid", gyuid)
+ jsonOneKey.addProperty("token", token)
+ val jsonObject = JsonObject()
+ jsonObject.addProperty("type", "onekey")
+ jsonObject.addProperty("bind", "")
+ jsonObject.add("data", jsonOneKey)
+
+ requestLogin(jsonObject)
+ }
+
/**
* 请求验证码
*/
- fun requestCaptcha() {
- // TODO: 发送请求获取验证码
+ fun requestCaptcha(phone: String) {
+ // 发送请求获取验证码
+ isLoading.value = true // 开始加载
+
+ mLaunch {
+ // 调用 API 获取数据
+ val jsonObject = JsonObject()
+ jsonObject.addProperty("phone", phone)
+ val response = ApiManager.serviceVo.sendCode(jsonObject.toString().toRequestBody())
+ if (response.status) {
+ Log.w("LoginViewModel", "请求验证码成功: ${response.data.timestamp}")
+ captchaTimestamp.value = response.data.timestamp
+ }else{
+ errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "获取验证码失败" })
+ }
+ isLoading.value = false // 加载完成
+ }
+ }
+
+ /**
+ * 请求登录(验证码)
+ */
+ fun requestLoginForCaptcha(phone: String, code: String) {
+ isLoading.value = true // 开始加载
+
+ // 调用 API 获取数据
+ val jsonPhone = JsonObject()
+ jsonPhone.addProperty("timestamp", captchaTimestamp.value)
+ jsonPhone.addProperty("phone", phone)
+ jsonPhone.addProperty("code", code)
+
+ val jsonObject = JsonObject()
+ jsonObject.addProperty("login_type", "phone")
+ jsonObject.add("phone", jsonPhone)
+
+ requestLogin(jsonObject)
+ }
+
+
+ fun initWXApi(context: Context) {
+ api = WXAPIFactory.createWXAPI(context, Constants.WechatAppId)
+ }
+
+ fun loginWithWechat(context: Context) {
+ if (isPolicyAgreement.value) {
+ doWxAuth(context)
+ }else{
+ Toast.makeText(context, "请先同意用户协议和隐私政策", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ /**
+ * 拿着微信授权码完成登录(在WXEntryActivity中调用)
+ * @param wechatCode 微信授权码
+ */
+ fun requestWxLogin(wechatCode: String) {
+ if(wechatCode.isEmpty()){
+ return
+ }
+ isLoading.value = true // 开始加载
+
+ // 调用 API 获取数据
+ val jsonWx = JsonObject()
+ jsonWx.addProperty("code", wechatCode)
+ jsonWx.addProperty("code_type", "")
+
+ val jsonObject = JsonObject()
+ jsonObject.addProperty("login_type", "weixin")
+ jsonObject.add("weixin", jsonWx)
+
+ requestLogin(jsonObject)
+ }
+
+ //获取微信授权
+ private fun doWxAuth(context: Context) {
+ if (!api.isWXAppInstalled) {
+ Toast.makeText(context, "您没有安装微信客户端,请先下载安装", Toast.LENGTH_SHORT).show()
+ return
+ }
+ val req = SendAuth.Req()
+ req.scope = "snsapi_userinfo" // 只能填 snsapi_userinfo
+ req.state = context.packageName + Math.random() * 1000 + "_phone"
+ api.sendReq(req)
+ }
+
+
+ /**
+ * 支付宝登录
+ * authInfo: 该参数需由后端生成并加签,包含 app_id、pid、target_id 等信息
+ */
+ fun loginWithAliPay(context: Context,onAuthResult: (Map) -> Unit) {
+ if(authInfoForAlipay.value.isEmpty()){
+ Toast.makeText(context, "请先获取支付宝登录授权码", Toast.LENGTH_SHORT).show()
+ return
+ }
+ // 发送请求获取支付宝登录授权码
+ mLaunch {
+ onAuthResult(doAlipayLogin(context as Activity, authInfoForAlipay.value))
+ }
+ }
+
+ /**
+ * 请求支付宝登录参数
+ */
+ fun requestAliPayAuthParam() {
+ isLoading.value = true // 开始加载
+
+ mLaunch {
+ val response = ApiManager.serviceVo.getAlipayAuthParam()
+ val data = response.data
+ val param = data.param
+
+ authInfoForAlipay.value = param
+ isLoading.value = false // 加载完成
+ }
+ }
+
+ /**
+ * 封装支付宝登录逻辑
+ * @param activity 当前 Activity 上下文
+ * @param authInfo 后端生成的授权字符串
+ * @return 支付宝返回的原始 Map 结果
+ */
+ private suspend fun doAlipayLogin(activity: Activity, authInfo: String): Map {
+ return withContext(Dispatchers.IO) {
+ // 初始化 AuthTask
+ val authTask = AuthTask(activity)
+
+ // 调用 authV2。第二个参数为 true 表示如果未安装支付宝则展示 Loading 界面, 该方法会阻塞当前线程直到用户操作结束
+ val result = authTask.authV2(authInfo, true)
+ Log.i(TAG, "doAlipayLogin: $result")
+
+ result ?: emptyMap()
+ }
+ }
+
+ /**
+ * 拿着微信授权码完成登录(在WXEntryActivity中调用)
+ * @param authCode 微信授权码
+ */
+ fun requestAlipayLogin(authCode: String) {
+ if(authCode.isEmpty()){
+ return
+ }
+ isLoading.value = true // 开始加载
+
+ // 调用 API 获取数据
+ val jsonWx = JsonObject()
+ jsonWx.addProperty("auth_code", authCode)//code
+
+ val jsonObject = JsonObject()
+ jsonObject.addProperty("type", "alipay")
+ jsonObject.addProperty("bind", "")
+ jsonObject.add("data", jsonWx)
+
+ requestLogin(jsonObject)
+ }
+
+ private fun requestLogin(jsonObject: JsonObject){
+ mLaunch {
+ val response = ApiManager.serviceVo.login(jsonObject.toString().toRequestBody())
+ if (response.status) {
+ loginState.value = response
+ val userEntity = response.data
+ userEntity.isLogin = true
+ //记录登录数据
+ PreferenceUtil.saveUserInfo(userEntity)
+ }else{
+ errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "登录失败" })
+ }
+ isLoading.value = false // 加载完成
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/viewmodel/interface/ServiceVo.kt b/app/src/main/java/com/img/rabbit/viewmodel/interface/ServiceVo.kt
new file mode 100644
index 0000000..62341b6
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/viewmodel/interface/ServiceVo.kt
@@ -0,0 +1,63 @@
+package com.img.rabbit.viewmodel.`interface`
+
+import com.img.rabbit.bean.response.AlipayParamEntity
+import com.img.rabbit.bean.response.CaptchaCodeEntity
+import com.img.rabbit.bean.response.UserEntity
+import com.img.rabbit.bean.response.UserConfigEntity
+import com.img.rabbit.provider.api.ResultVo
+import okhttp3.RequestBody
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Query
+
+interface ServiceVo {
+ /*
+ @POST("/dictionary/getTsSyspara")
+ suspend fun requestHospitalName(): Response>>
+ @POST("/dictionary/getLogo")
+ suspend fun requestHospitalLogo(): Response>
+ @POST("/nurse/login")
+ suspend fun requestLogin(@Body request: LoginRequest): Response>
+ */
+
+ /**
+ * 获取服务器时间
+ */
+ @GET("/api/time")
+ suspend fun getServerTime(): ResultVo
+
+ /**
+ * 获取客户端配置
+ */
+ @GET("/api/user/config")
+ suspend fun getUserConfig(
+ @Query("oaid") oaid: String,
+ @Query("os_version") osVersion: Int,
+ @Query("ua") ua: String,
+ @Query("imei") imei: String,
+ @Query("cid") cid: String,
+ ): ResultVo
+
+ @GET("api/alipay/app_param")
+ suspend fun getAlipayAuthParam(): ResultVo
+
+ /**
+ * 发送验证码
+ */
+ @POST("api/user/code")
+ suspend fun sendCode(@Body requestBody: RequestBody): ResultVo
+
+ /**
+ * 登录
+ */
+ @POST("/api/user/login")
+ suspend fun login(@Body requestBody: RequestBody): ResultVo
+
+
+ /**
+ * 退出登录
+ */
+ @POST("/mapi/user/logout")
+ suspend fun logout(): ResultVo
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/img/rabbit/wxapi/WXEntryActivity.kt b/app/src/main/java/com/img/rabbit/wxapi/WXEntryActivity.kt
new file mode 100644
index 0000000..2e19ac4
--- /dev/null
+++ b/app/src/main/java/com/img/rabbit/wxapi/WXEntryActivity.kt
@@ -0,0 +1,34 @@
+@file:OptIn(DelicateCoroutinesApi::class)
+
+package com.img.rabbit.wxapi
+
+import com.img.rabbit.provider.storage.GlobalStateManager
+import com.tencent.mm.opensdk.constants.ConstantsAPI
+import com.tencent.mm.opensdk.modelbase.BaseResp
+import com.tencent.mm.opensdk.modelmsg.SendAuth
+import com.umeng.socialize.weixin.view.WXCallbackActivity
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+
+class WXEntryActivity : WXCallbackActivity() {
+ override fun onResp(resp: BaseResp?) {
+ if (resp?.type == ConstantsAPI.COMMAND_SENDAUTH) {
+ when (resp.errCode) {
+ BaseResp.ErrCode.ERR_OK -> {
+ val authResp = resp as SendAuth.Resp
+ //val wxState = WxBean(code = authResp.code,state = authResp.state)
+
+ GlobalScope.launch {
+ GlobalStateManager(this@WXEntryActivity).storeGlobalWxAuthorization(authResp.code)
+
+ finish()
+ }
+ }
+
+ else -> {}
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_alipay_icon.xml b/app/src/main/res/drawable/ic_alipay_icon.xml
new file mode 100644
index 0000000..5e96051
--- /dev/null
+++ b/app/src/main/res/drawable/ic_alipay_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_wx_icon.xml b/app/src/main/res/drawable/ic_wx_icon.xml
new file mode 100644
index 0000000..3c32362
--- /dev/null
+++ b/app/src/main/res/drawable/ic_wx_icon.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/layout/layout_web.xml b/app/src/main/res/layout/layout_web.xml
new file mode 100644
index 0000000..82b3f17
--- /dev/null
+++ b/app/src/main/res/layout/layout_web.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_copy.webp b/app/src/main/res/mipmap-xxhdpi/ic_copy.webp
new file mode 100644
index 0000000..6b804b1
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_copy.webp differ
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 445f5ec..5a14f64 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -25,6 +25,10 @@ androidx-test-ext-junit = "1.1.5"
androidx-test-espresso-core = "3.5.1"
navigationRuntimeKtx = "2.9.7"
+# Network Version
+retrofit = "3.0.0"
+okhttp = "5.1.0"
+
# Third-party version
gson = "2.13.2"
gysdk = "3.2.3.0"
@@ -37,6 +41,20 @@ faceDetection = "16.1.5"
foundation = "1.10.2"
androidGifDrawableEncoder = "1.2.30"
gifeEncoder = "0.10.1"
+cropify = "0.5.2"
+matisse = "2.3.0"
+pictureselector = "v3.11.2"
+compress = "v3.11.2"
+wechatSdkAndroidWithoutMta = "6.8.0"
+# Umeng version
+umengUmsdkCommon = "9.8.9"
+umengUmsdkAsms = "1.8.7.2"
+umengUmsdkApm = "2.0.6"
+umengUmsdkShareCore = "7.3.7"
+tencentHelper = "3.0.6"
+#oaid
+android_cn_oaid = "4.2.12"
+fastaes = "1.1.5"
[libraries]
@@ -70,17 +88,41 @@ androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "a
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso-core" }
androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" }
+# Network dependencies
+retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
+retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
+retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version = "1.0.0" }
+okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
+okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
+
+
# Third-party dependencies
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
gysdk = { module = "com.getui:gysdk", version.ref = "gysdk" }
+matisse = { module = "io.github.leavesczy:matisse", version.ref = "matisse" }
mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" }
+pictureselector = { module = "io.github.lucksiege:pictureselector", version.ref = "pictureselector" }
+compress = { module = "io.github.lucksiege:compress", version.ref = "compress" }
segmentation-selfie = { module = "com.google.mlkit:segmentation-selfie", version.ref = "segmentationSelfie" }
face-detection = { module = "com.google.mlkit:face-detection", version.ref = "faceDetection" }
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" }
android-gif-drawable = { module = "pl.droidsonroids.gif:android-gif-drawable", version.ref = "androidGifDrawableEncoder" }
gif-encoder = { module = "com.squareup:gifencoder", version.ref = "gifeEncoder" }
+cropify = { module = "com.github.moyuruaizawa:cropify", version.ref = "cropify" }
+wechat-sdk = { module = "com.tencent.mm.opensdk:wechat-sdk-android-without-mta", version.ref = "wechatSdkAndroidWithoutMta" }
+#Umeng Sdk
+umeng-umsdk-common = { module = "com.umeng.umsdk:common", version.ref = "umengUmsdkCommon" }
+umeng-umsdk-asms = { module = "com.umeng.umsdk:asms", version.ref = "umengUmsdkAsms" }
+#noinspection Aligned16KB
+umeng-umsdk-apm = { module = "com.umeng.umsdk:apm", version.ref = "umengUmsdkApm" }
+umeng-umsdk-share-core = { module = "com.umeng.umsdk:share-core", version.ref = "umengUmsdkShareCore" }
+umeng-umsdk-share-wx = { module = "com.umeng.umsdk:share-wx", version.ref = "umengUmsdkShareCore" }
+tencent-helper = { module = "com.tencent.vasdolly:helper", version.ref = "tencentHelper" }
+android_cn_oaid = { module = "com.github.gzu-liyujiang:Android_CN_OAID", version.ref = "android_cn_oaid" }
+#Decrypt
+fastaes = { module = "io.github.billywei01:fastaes", version.ref = "fastaes" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 7da8671..f2788e5 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -7,6 +7,10 @@ pluginManagement {
includeGroupByRegex("androidx.*")
}
}
+ maven { url = uri("https://maven.aliyun.com/repository/public") }
+ maven { url = uri("https://maven.aliyun.com/repository/google") }
+ maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
+ maven { url = uri("https://developer.huawei.com/repo/") }
mavenCentral()
gradlePluginPortal()
}
@@ -17,9 +21,19 @@ dependencyResolutionManagement {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
+ maven { url = uri("https://company/com/maven2") }
maven { url = uri("https://repo1.maven.org/maven2/") }
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
- maven { url = uri("https://mvn.getui.com/nexus/content/repositories/releases") }
+ maven { url = uri("https://mvn.getui.com/nexus/content/repositories/releases") } //个推
+ maven { url = uri("https://maven.aliyun.com/repository/public") }
+ maven { url = uri("https://maven.aliyun.com/repository/google") }
+ maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
+ maven { url = uri("https://developer.huawei.com/repo/") }//添加华为仓库 获取oaid
+ maven { url = uri("https://developer.hihonor.com/repo/") }
+ maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") } //巨量融合
+ maven { url = uri("https://repo.eclipse.org/content/repositories/paho-snapshots/") } //mqtt
+ mavenCentral()
+ google()
}
}