1、接口接入
2、联调
This commit is contained in:
shenzuqiang 2026-02-27 18:44:54 +08:00
parent 19dc61e9af
commit f78f459616
43 changed files with 3235 additions and 450 deletions

View File

@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-02-26T05:37:22.871237900Z">
<DropdownSelection timestamp="2026-02-27T07:54:46.218482200Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=JRBI89BIE6AI5TG6" />

View File

@ -173,5 +173,6 @@ dependencies {
implementation(libs.android.cn.oaid) //获取手机设备id
implementation(libs.fastaes) //解密
implementation("com.google.accompanist:accompanist-permissions:0.32.0")
}

View File

@ -91,7 +91,7 @@
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data

View File

@ -1,5 +1,6 @@
package com.img.rabbit
import android.annotation.SuppressLint
import android.app.Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
@ -9,14 +10,18 @@ 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.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
@ -25,6 +30,7 @@ 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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -52,7 +58,9 @@ 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.GlobalStateManager
import com.img.rabbit.provider.storage.PreferenceUtil
import com.img.rabbit.provider.storage.PreferenceUtil.saveBDVID
import com.img.rabbit.utils.ChannelUtils
import com.img.rabbit.utils.UrlLinkUtils.openAgreement
import com.img.rabbit.viewmodel.GeneralViewModel
@ -60,38 +68,57 @@ 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.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.system.exitProcess
class MainActivity : ComponentActivity() {
@OptIn(DelicateCoroutinesApi::class)
@SuppressLint("UnrememberedMutableState", "CoroutineCreationDuringComposition")
override fun onCreate(savedInstanceState: Bundle?) {
// 必须在 super.onCreate 之前调用
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
initUM()
// 启用Edge-to-Edge模式沉浸模式
enableEdgeToEdge()
setContent {
var showUpdateDialog by remember { mutableStateOf(false) }
val splashViewModel: SplashViewModel = viewModel()
val generalViewModel: GeneralViewModel = viewModel()
val loginViewModel: LoginViewModel = viewModel()
var loginViewModel: LoginViewModel = viewModel()
val context = LocalContext.current
var showSplash by remember { mutableStateOf(false) }
var globalLogout by mutableStateOf(GlobalStateManager(context).globalLogoutFlow().collectAsState(initial = false))
LaunchedEffect(generalViewModel.agreementStatus.value) {
if (generalViewModel.agreementStatus.value == true){
saveBDVID()
//获取服务器时间
generalViewModel.getServerTime()
}
}
LaunchedEffect(generalViewModel.serverTime.value) {
if (generalViewModel.serverTime.value != null){
// 获取用户配置
loginViewModel.requestUserConfig()
//初始化微信登录
loginViewModel.initWXApi(this)
initUM()
}
}
//初始化微信登录
//loginViewModel.initWXApi(this)
// 设置启动页显示条件
splashScreen.setKeepOnScreenCondition {
splashViewModel.isLoading.value // 当为 true 时,启动页不消失
}
AppTheme {
SplashScreenContent{
//未同意提示政策弹窗
@ -114,7 +141,18 @@ class MainActivity : ComponentActivity() {
showSplash = true
}
if(showSplash){
if(showSplash || globalLogout.value == true){
// 全局注销,重新登录
if(globalLogout.value == true){
loginViewModel = viewModel()
loginViewModel.requestUserConfig()
//loginViewModel.initWXApi(this)
}
// 初始化全局注销状态为 false
GlobalScope.launch {
GlobalStateManager(context).storeGlobalLogout(false)
}
val token = PreferenceUtil.getAccessToken()
// 未登录,显示登录页
if (token.isNullOrEmpty() && !loginViewModel.isLogin.value) {
@ -135,6 +173,18 @@ class MainActivity : ComponentActivity() {
MainScreen(generalViewModel = generalViewModel, loginViewModel = loginViewModel)
}
}
if(showUpdateDialog){
UpdateDialog(
title = "新版本,更新提示",
newVersion = "1.0.0",
desc = "修复了一些问题,新增了一些功能",
url = "https://www.baidu.com",
){ state ->
showUpdateDialog = state
}
}
}
}
@ -418,6 +468,186 @@ private fun PrivacyPolicyScreen(viewModel: LoginViewModel, onAgreementChange: (B
}
@Composable
private fun UpdateDialog(
title: String = "新版本,更新提示",
newVersion: String = "1.0.0",
desc: String = "修复了一些问题,新增了一些功能",
url: String,
onStatusChange: (state: Boolean) -> Unit
){
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0x80000000))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onStatusChange(false)
},
verticalArrangement = Arrangement.Center
){
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 36.dp)
.background(Color.White, shape = RoundedCornerShape(26.dp))
.align(Alignment.CenterHorizontally)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
//什么都不用做,只是解决点击穿透问题
}
) {
Image(
painter = painterResource(id = R.mipmap.ic_dialog_top_mask1),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
)
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 46.dp)
) {
Text(
text = title,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF1A1A1A),
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterHorizontally)
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(14.dp)
)
Text(
text = newVersion,
fontSize = 12.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF767676),
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterHorizontally)
)
Text(
text = desc,
fontSize = 12.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF767676),
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterHorizontally)
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(16.dp)
)
//切换/退出账号
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 18.dp, end = 18.dp, bottom = 20.dp)
) {
//取消解绑手机号
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.weight(1f)
.background(
Color(0x00000000),
shape = RoundedCornerShape(359.dp),
)
.border(
width = 1.dp,
color = Color(0xFF000000),
shape = RoundedCornerShape(359.dp)
)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
// 取消解绑手机号
onStatusChange(false)
}
) {
Text(
"取消",
color = Color(0xFF1A1A1A),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight()
.padding(vertical = 12.dp)
.align(Alignment.Center)
)
}
Box(
modifier = Modifier
.width(11.dp)
)
//退出登录
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.weight(1f)
.background(
Color(0xFF252525),
shape = RoundedCornerShape(359.dp),
)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
//TODO 确定解绑手机号
onStatusChange(false)
}
) {
Text(
"确定",
color = Color(0xFFC2FF43),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight()
.padding(vertical = 12.dp)
.align(Alignment.Center)
)
}
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun MainScreenPreview() {

View File

@ -0,0 +1,9 @@
package com.img.rabbit.bean.local
import android.net.Uri
import com.img.rabbit.bean.response.UploadFileEntity
data class FileManagerBean(
var fileEntity: UploadFileEntity,
var uri: Uri? = null
)

View File

@ -3,7 +3,7 @@ package com.img.rabbit.bean.response
import kotlinx.serialization.Serializable
@Serializable
class UserEntity {
class LoginInfoEntity {
val user_id: String = ""
val name: String = ""
val avater: String = ""

View File

@ -0,0 +1,8 @@
package com.img.rabbit.bean.response
@kotlinx.serialization.Serializable
class UploadFileEntity(
var id: String = "",
var url: String = ""
)

View File

@ -0,0 +1,32 @@
package com.img.rabbit.bean.response
import kotlinx.serialization.Serializable
@Serializable
class UserInfoEntity(
val alipayid: String = "",
val app_id: String = "",
val appleid: String = "",
val avater: String = "",
val balance: String = "",
val client_cid: String = "",
val coupon_count: Int = 0,
val ip_area: String = "",
val ip_area_name: String = "",
val name: String = "",
val phone: String = "",
val role: String = "",
val temp: Boolean = false,
val unionid: String = "",
val user_id: String = "",
val vip: Int = 0,
val vip_expire: String = "",
val vip_expire_time: String = "",
val vip_name: String = "",
val weixinAppId: String = "",
val weixinAppIdType: String = "",
val weixinAppOpenId: String = "",
val weixinOpenId: String = ""
)

View File

@ -1,9 +1,9 @@
package com.img.rabbit.components
import android.Manifest
import android.annotation.SuppressLint
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -52,38 +52,30 @@ import androidx.core.content.FileProvider
import java.io.File
// 首先添加ImageUtils的导入
import android.graphics.Bitmap.CompressFormat
import androidx.compose.foundation.Image
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.ui.res.painterResource
import coil3.compose.AsyncImage
import com.img.rabbit.R
import com.img.rabbit.utils.ImageUtils
import kotlin.apply
import kotlin.collections.all
import kotlin.collections.minus
import kotlin.collections.plus
import kotlin.collections.toMutableList
import kotlin.let
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ImagePicker(
modifier: Modifier = Modifier,
imageHeight: Dp = 100.dp,
aspectRatio: Float = 100f / 100f,
maxCount: Int = 3,
imageHeight: Dp = 146.dp,
aspectRatio: Float = 747f / 395f,
maxCount: Int = 10,
addButtonName: String = "添加图片",
currentImageUris: List<Uri> = emptyList(),
currentImagePaths: List<String> = emptyList(), // 新增参数
onImagesUpdated: (uris: List<Uri>, paths: List<String>) -> Unit
onImagesUpdated: (currentUri: Uri, uris: List<Uri>) -> Unit,
onDeleteUpdated: (currentUri: Uri, uris: List<Uri>) -> Unit
) {
val context = LocalContext.current
// 添加临时文件管理
val tempImageUri = remember { mutableStateOf<Uri?>(null) }
// 记录当前操作场景
var currentScene by remember { mutableStateOf<PickerScene?>(null) }
// 新增选择对话框
var showChoiceDialog by remember { mutableStateOf(false) }
@ -103,86 +95,32 @@ fun ImagePicker(
}
}
/*
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
result.data?.data?.let { uri ->
if (currentImages.size < maxCount) {
onImagesUpdated(currentImages + uri)
}
}
}
*/
// 相册选择逻辑
val galleryLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
contract = ActivityResultContracts.PickVisualMedia()
) { uri ->
uri?.let {
uri?.let { uri ->
if (currentImageUris.size < maxCount) {
// 保存图片到存储并获取路径
val savedPath = ImageUtils.saveUriToStorage(
context = context,
uri = it,
format = CompressFormat.JPEG,
quality = 90
)
// 更新图片列表和路径列表
onImagesUpdated(
currentImageUris + it,
if (savedPath != null) currentImagePaths + savedPath else currentImagePaths
)
onImagesUpdated( uri, currentImageUris + uri )
}
}
}
// 拍照逻辑
val cameraLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.TakePicture()
contract = ActivityResultContracts.TakePicture()
) { success ->
if (success) {
tempImageUri.value?.let { uri ->
if (currentImageUris.size < maxCount) {
// 保存图片到存储并获取路径
val savedPath = ImageUtils.saveUriToStorage(
context = context,
uri = uri,
format = CompressFormat.JPEG,
quality = 90
)
// 更新图片列表和路径列表
onImagesUpdated(
currentImageUris + uri,
if (savedPath != null) currentImagePaths + savedPath else currentImagePaths
)
onImagesUpdated( uri, currentImageUris + uri )
}
}
}
}
// 权限检查
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
// 修改权限回调逻辑
if (permissions.all { it.value }) {
when (currentScene) {
PickerScene.GALLERY -> galleryLauncher.launch("image/*")
PickerScene.CAMERA -> {
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
File.createTempFile("IMG_", ".jpg", context.externalCacheDir)
)
tempImageUri.value = uri
cameraLauncher.launch(uri)
}
null -> { /* 无操作 */ }
}
}
}
if (showChoiceDialog) {
AlertDialog(
@ -193,14 +131,9 @@ fun ImagePicker(
TextButton(
onClick = {
showChoiceDialog = false
currentScene = PickerScene.CAMERA
permissionLauncher.launch(
arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
)
val uri = FileProvider.getUriForFile(context,"${context.packageName}.fileProvider",File.createTempFile("IMG_", ".jpg", context.externalCacheDir))
tempImageUri.value = uri
cameraLauncher.launch(uri)
}
) { Text(text = "拍照", fontSize = 14.sp) }
},
@ -208,13 +141,7 @@ fun ImagePicker(
TextButton(
onClick = {
showChoiceDialog = false
currentScene = PickerScene.GALLERY
permissionLauncher.launch(
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE
)
)
galleryLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
) { Text(text = "相册", fontSize = 14.sp) }
}
@ -237,7 +164,7 @@ fun ImagePicker(
LazyRow(modifier = modifier.defaultMinSize(minHeight = (imageHeight + 8.dp))) {
items(getDisplayImages()) { uri ->
Box(
Modifier.padding(end = 6.dp, top = 6.dp).background(color = Color(0x80E5E5E7), shape = RoundedCornerShape(8.dp))
Modifier.padding(4.dp).background(color = Color(0x80E5E5E7), shape = RoundedCornerShape(8.dp))
) {
AsyncImage(
model = uri,
@ -246,10 +173,7 @@ fun ImagePicker(
.height(imageHeight)
.aspectRatio(aspectRatio)
.clip(RoundedCornerShape(8.dp))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
.clickable {
previewImageUri = uri
}
)
@ -259,58 +183,17 @@ fun ImagePicker(
32.dp
}
Image(
painter = painterResource(id = R.mipmap.ic_picture_del),
painter = painterResource(id = R.drawable.ic_close),
contentDescription = null,
modifier = Modifier
.size(size)
.aspectRatio(1f) // 设置宽高比为1:1
.align(Alignment.TopEnd)
.padding(top = 4.dp, end = 4.dp)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
){
// 找到当前uri在列表中的索引同时从paths列表中移除对应的路径
val uriIndex = currentImageUris.indexOf(uri)
val newPaths = if (uriIndex >= 0 && uriIndex < currentImagePaths.size) {
currentImagePaths.toMutableList().apply { removeAt(uriIndex) }
} else {
// 如果uri不在currentImageUris中可能是从currentImagePaths转换过来的
// 尝试通过路径匹配找到对应的索引
val pathIndex = currentImagePaths.indexOf(uri.path)
if (pathIndex >= 0) {
currentImagePaths.toMutableList().apply { removeAt(pathIndex) }
} else {
currentImagePaths
}
}
onImagesUpdated(currentImageUris - uri, newPaths)
.clickable{
onDeleteUpdated(uri, currentImageUris - uri)
}
)
/*
IconButton(
onClick = {
// 找到当前uri在列表中的索引同时从paths列表中移除对应的路径
val uriIndex = currentImageUris.indexOf(uri)
val newPaths = if (uriIndex >= 0 && uriIndex < currentImagePaths.size) {
currentImagePaths.toMutableList().apply { removeAt(uriIndex) }
} else {
// 如果uri不在currentImageUris中可能是从currentImagePaths转换过来的
// 尝试通过路径匹配找到对应的索引
val pathIndex = currentImagePaths.indexOf(uri.path)
if (pathIndex >= 0) {
currentImagePaths.toMutableList().apply { removeAt(pathIndex) }
} else {
currentImagePaths
}
}
onImagesUpdated(currentImageUris - uri, newPaths)
},
modifier = Modifier.align(Alignment.TopEnd)
) {
Icon(Icons.Default.Close, "删除")
}
*/
}
}
@ -341,17 +224,12 @@ fun ImagePicker(
}
@Composable
private fun AddButton(
onClick: () -> Unit,
label: String = "添加图片",
imageHeight: Dp = 100.dp,
aspectRatio: Float = 100f / 100f
) {
private fun AddButton(onClick: () -> Unit, label: String = "添加图片") {
TextButton(
onClick = onClick,
modifier = Modifier
.height(imageHeight)
.aspectRatio(aspectRatio)
.size(100.dp)
.padding(4.dp)
.border(1.dp, Color(0xFFA5A5A5), RoundedCornerShape(8.dp))
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@ -363,8 +241,8 @@ private fun AddButton(
@Composable
private fun AddSingleButton(
imageHeight: Dp = 100.dp,
aspectRatio: Float = 100f / 100f,
imageHeight: Dp = 146.dp,
aspectRatio: Float = 747f / 395f,
onClick: () -> Unit,
label: String = "添加图片"
) {
@ -438,6 +316,3 @@ private fun ZoomableImage(uri: Uri, modifier: Modifier = Modifier) {
)
}
}
// 添加场景枚举
enum class PickerScene { GALLERY, CAMERA }

View File

@ -12,6 +12,8 @@ object Constants {
const val WechatAppSecret = ""//微信secret
const val UmengAppkey = ""//TODO 友盟appKey
const val AppId = ""//appid
//解密
const val AESDecrypt = "e4rOtnF8tJjtHO7ecZeJHN1rapED5ImB"
//加密字符

View File

@ -42,7 +42,6 @@ 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
@ -107,9 +106,14 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
if (loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token != null) {
//登录成功
PreferenceUtil.saveAccessToken(loginViewModel.loginState.value?.data?.token)
loginViewModel.setLogin(true)
// 获取用户信息
loginViewModel.requestUserInfo()
loginViewModel.setLogin(true)
Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show()
//清理loginState
loginViewModel.loginState.value = null
}else if(loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token == null){
//登录失败
loginViewModel.setLogin(false)
@ -120,7 +124,11 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
var globalWxAuthorization by mutableStateOf(GlobalStateManager(context).globalWxAuthorizationFlow().collectAsState(initial = ""))
LaunchedEffect(globalWxAuthorization.value) {
globalWxAuthorization.value?.let { loginViewModel.requestWxLogin(it) }
if(globalWxAuthorization.value?.isNotEmpty() == true){
loginViewModel.requestWxLogin(globalWxAuthorization.value!!)
//清理globalWxAuthorization
GlobalStateManager(context).storeGlobalWxAuthorization("")
}
}
LaunchedEffect(loginViewModel.authInfoForAlipay.value) {
@ -144,6 +152,8 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
alipayResult.memo ?: "登录失败"
}
}
//清理authInfoForAlipay
loginViewModel.authInfoForAlipay.value = ""
}
}
@ -160,9 +170,6 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
.wrapContentHeight()
)
// 顶部栏
TitleBar(navController = navController, paddingValues = it, title = "", showSave = false, showBreak = isVisibilityBreak)
Column(
modifier = Modifier
.fillMaxSize()
@ -176,7 +183,7 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
LoginScreenType.LOGIN_WX -> {
//微信登录
Box(modifier = Modifier.align(Alignment.Center).padding(bottom = 100.dp)){
WxLoginScreen(context, loginViewModel)
WxLoginScreen(context, loginViewModel, generalViewModel)
}
}
@ -200,7 +207,7 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
.padding(top = 27.dp),
verticalArrangement = Arrangement.Bottom
){
OtherLoginBar(context = context, viewModel = loginViewModel)
OtherLoginBar(viewModel = loginViewModel)
}
}
}
@ -221,6 +228,9 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
})
}
}
// 顶部栏
TitleBar(navController = navController, paddingValues = it, title = "", showSave = false, showBreak = isVisibilityBreak)
}
}
@ -700,8 +710,7 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel, gener
loginBtn = loginButton,
checkBox = checkbox,
privacyTv = agreementTextView,
viewModel = viewModel,
generalViewModel = generalViewModel
viewModel = viewModel
)
}
}
@ -713,6 +722,7 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel, gener
private fun WxLoginScreen(
context: Context,
viewModel: LoginViewModel,
generalViewModel: GeneralViewModel
) {
Column {
Image(
@ -761,8 +771,8 @@ private fun WxLoginScreen(
) {
// 启动微信验证,请求登录
if (viewModel.isPolicyAgreement.value) {
//TODO 打开微信登录
viewModel.loginWithWechat(context)
//打开微信登录
viewModel.loginWithWechat(context, generalViewModel.api)
} else {
Toast.makeText(
context,
@ -1244,8 +1254,7 @@ private fun OneKeyLoginScreen(context: Context, viewModel: LoginViewModel) {
*/
@Composable
private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) {
val scope = rememberCoroutineScope()
private fun OtherLoginBar(viewModel: LoginViewModel) {
Column(
modifier = Modifier
.fillMaxWidth()
@ -1309,18 +1318,6 @@ private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) {
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
/*
if (viewModel.isPolicyAgreement.value) {
//TODO 打开微信登录
viewModel.loginWithWechat(context)
} else {
Toast.makeText(
context,
"请先同意用户协议和隐私政策",
Toast.LENGTH_SHORT
).show()
}
*/
// 微信登录
viewModel.loginScreenType.value = LoginScreenType.LOGIN_WX
}
@ -1336,18 +1333,6 @@ private fun OtherLoginBar(context: Context, viewModel: LoginViewModel) {
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
/*
if (viewModel.isPolicyAgreement.value) {
// 打开支付宝登录
viewModel.requestAliPayAuthParam()
} else {
Toast.makeText(
context,
"请先同意用户协议和隐私政策",
Toast.LENGTH_SHORT
).show()
}
*/
// 支付宝登录
viewModel.loginScreenType.value = LoginScreenType.LOGIN_ALIPAY
}
@ -1405,7 +1390,6 @@ private fun oneKeyLogin(
checkBox: CheckBox,
privacyTv: TextView,
viewModel: LoginViewModel,
generalViewModel: GeneralViewModel
) {
val eloginActivityParam = EloginActivityParam()
.setActivity(context as Activity)
@ -1426,7 +1410,7 @@ private fun oneKeyLogin(
}
GYManager.getInstance().eAccountLogin(eloginActivityParam, 5000, object : GyCallBack {
override fun onSuccess(response: GYResponse?) {
//TODO 登录成功,需要与后端交互
// 登录成功,需要与后端交互
Log.i("OneKeyLogin", "onSuccess:$response")
try {
val jsonObject = JSONObject(response?.msg?:"{}")
@ -1439,7 +1423,7 @@ private fun oneKeyLogin(
}
override fun onFailed(p0: GYResponse?) {
//TODO 登录失败
// 登录失败
Log.e("OneKeyLogin", "onFailed:$p0")
}
})
@ -1464,7 +1448,7 @@ private fun PreviewOneKeyLoginScreen() {
@Preview(showBackground = true)
@Composable
private fun PreviewWxLoginScreen() {
WxLoginScreen(LocalContext.current, viewModel())
WxLoginScreen(LocalContext.current, viewModel(), viewModel())
}

View File

@ -2,11 +2,24 @@ package com.img.rabbit.pages
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.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.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@ -21,15 +34,22 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.img.rabbit.R
import com.img.rabbit.pages.screen.HomeScreen
import com.img.rabbit.pages.screen.MineScreen
@ -40,8 +60,11 @@ import com.img.rabbit.pages.screen.make.ResizeScreen
import com.img.rabbit.pages.screen.mine.setting.AboutScreen
import com.img.rabbit.pages.screen.mine.setting.AccountBindScreen
import com.img.rabbit.pages.screen.mine.setting.AccountManagerScreen
import com.img.rabbit.pages.screen.mine.setting.BindScreen
import com.img.rabbit.pages.screen.other.CameraGuideScreen
import com.img.rabbit.provider.storage.PreferenceUtil
import com.img.rabbit.route.ScreenRoute
import com.img.rabbit.viewmodel.BindViewModel
import com.img.rabbit.viewmodel.GeneralViewModel
import com.img.rabbit.viewmodel.LoginViewModel
@ -54,6 +77,7 @@ sealed class TabItem(val title: String, val normalIconRes: Int, val selectedIcon
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewModel) {
val navController = rememberNavController()
val networkStatus by generalViewModel.networkStatus.observeAsState(initial = true)
val isNavigationBarVisible by generalViewModel.isNavigationBarVisible.observeAsState(initial = true)
@ -134,6 +158,14 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode
navController = navController,
startDestination = ScreenRoute.Home.route
) {
// Main页面
composable(ScreenRoute.Main.route) {
MainScreen(
generalViewModel = generalViewModel,
loginViewModel = loginViewModel
)
}
// Tab页面
composable(ScreenRoute.Home.route) {
HomeScreen(
@ -177,7 +209,7 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode
OnlineServiceScreen(navController = navController)
}
composable(ScreenRoute.Setting.route) {
SettingScreen(navController = navController)
SettingScreen(navController = navController, loginViewModel = loginViewModel)
}
// 设置页面Setting
@ -200,6 +232,59 @@ fun MainScreen(generalViewModel: GeneralViewModel, loginViewModel: LoginViewMode
isVisibilityBreak = true
)
}
composable(
route = "login?type={type}",
arguments = listOf(
navArgument("type") {
type = NavType.IntType
defaultValue = LoginViewModel.JumpLoginType.NORMAL.type
}
)
) { backStackEntry ->
val isVisibilityBreak = when(backStackEntry.arguments?.getInt("type")){
LoginViewModel.JumpLoginType.FROM_ADD.type -> {
// 添加账号
true
}
LoginViewModel.JumpLoginType.FROM_LOGOUT.type -> {
// 退出登录
false
}
else -> false
}
LoginScreen(
navController = navController,
generalViewModel = generalViewModel,
loginViewModel = loginViewModel,
isVisibilityBreak = isVisibilityBreak
)
}
// 绑定页面Bind
/*
composable(ScreenRoute.Bind.route) {
BindScreen(
navController = navController,
generalViewModel = generalViewModel,
bindType = BindViewModel.BindType.FROM_PHONE.type
)
}
*/
composable(
route = "bind?type={type}",
arguments = listOf(
navArgument("type") {
type = NavType.IntType
defaultValue = BindViewModel.BindType.FROM_PHONE.type
}
)
) { backStackEntry ->
BindScreen(
navController = navController,
generalViewModel = generalViewModel,
bindType = backStackEntry.arguments?.getInt("type")?:BindViewModel.BindType.FROM_PHONE.type
)
}
}
// 根据选中的Tab切换导航路由

View File

@ -1,5 +1,6 @@
package com.img.rabbit.pages.screen
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -14,7 +15,6 @@ 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
@ -38,23 +38,26 @@ 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.BuildConfig
import com.img.rabbit.R
import com.img.rabbit.provider.storage.PreferenceUtil
import com.img.rabbit.utils.AppUpdate
import com.img.rabbit.viewmodel.GeneralViewModel
import com.img.rabbit.viewmodel.LoginViewModel
@Composable
fun MineScreen(
navController: NavHostController,
generalViewModel: GeneralViewModel,
) {
val TAG = "Rabbit_Mine"
val context = LocalContext.current
val vipMember by remember { mutableStateOf(false) }
val userInfo by remember { mutableStateOf(PreferenceUtil.loginUserInfo()) }
val userInfo by remember { mutableStateOf(PreferenceUtil.getUserInfo()) }
// 监听返回事件
val currentBackStackEntry = navController.currentBackStackEntry
@ -69,7 +72,9 @@ fun MineScreen(
Box(
modifier = Modifier.fillMaxSize().background(Color(0xFFF9F9F9))
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF9F9F9))
){
Image(
painter = painterResource(id = R.mipmap.ic_mine_top_mask),
@ -129,11 +134,11 @@ fun MineScreen(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
if(userInfo == null){
if (userInfo == null) {
// 隐藏TabBar
generalViewModel.setNavigationBarVisible(false)
// 跳转登录页面
navController.navigate("login")
navController.navigate("login?type=${LoginViewModel.JumpLoginType.NORMAL.type}")
} else {
//TODO 已登录,跳转个人信息页面
//navController.navigate("userInfo")
@ -153,14 +158,19 @@ fun MineScreen(
.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)
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()
Toast.makeText(context, "已复制到剪贴板", Toast.LENGTH_SHORT)
.show()
}
}
){
@ -334,7 +344,11 @@ fun MineScreen(
)
}
Box(
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
modifier = Modifier
.fillMaxWidth()
.height(0.5.dp)
.padding(horizontal = 12.dp)
.background(
Color(0x4DBBBBBB)
)
)
@ -385,7 +399,11 @@ fun MineScreen(
)
}
Box(
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
modifier = Modifier
.fillMaxWidth()
.height(0.5.dp)
.padding(horizontal = 12.dp)
.background(
Color(0x4DBBBBBB)
)
)
@ -397,8 +415,19 @@ fun MineScreen(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
// 处理点击事件
Toast.makeText(context, "版本更新", Toast.LENGTH_SHORT).show()
//更新检测
val versionConfig =
PreferenceUtil.getUserConfig()?.config?.versionEntity
AppUpdate.checkUpdate(versionConfig) { isUpdate, tips ->
if (isUpdate) {
//提示执行更新
//startUpdate(result, fragment)
Log.i(TAG, "checkUpdate: 有新版本, tips = $tips")
} else {
Toast.makeText(context, tips, Toast.LENGTH_SHORT).show()
}
}
},
verticalAlignment = Alignment.CenterVertically
) {
@ -424,17 +453,34 @@ fun MineScreen(
)
}
Row {
Text(
text = "V${BuildConfig.VERSION_NAME}",
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFFAAAAAA),
modifier = Modifier
.wrapContentSize()
.padding(start = 8.dp)
.align(Alignment.CenterVertically)
)
Image(
painter = painterResource(id = R.mipmap.ic_arrow_right),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.wrapContentSize()
.padding(end = 12.dp) // 改为end padding
.padding(end = 12.dp)
.align(Alignment.CenterVertically)
)
}
}
Box(
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
modifier = Modifier
.fillMaxWidth()
.height(0.5.dp)
.padding(horizontal = 12.dp)
.background(
Color(0x4DBBBBBB)
)
)

View File

@ -1,5 +1,7 @@
package com.img.rabbit.pages.screen.mine
import android.text.TextUtils
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@ -18,15 +20,15 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -34,12 +36,24 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.img.rabbit.components.ImagePicker
import com.img.rabbit.pages.toolbar.TitleBar
import com.img.rabbit.viewmodel.FeedbackViewModel
import okhttp3.RequestBody.Companion.toRequestBody
@Composable
fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewModel = viewModel()) {
val context = LocalContext.current
LaunchedEffect(viewModel.errorState.value) {
if(viewModel.errorState.value != null){
Toast.makeText(context, viewModel.errorState.value?.message, Toast.LENGTH_SHORT).show()
}
viewModel.errorState.value = null
}
Scaffold{
Column(
modifier = Modifier.fillMaxSize()
@ -57,7 +71,9 @@ fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewMode
) {
// 意见反馈类型
Row(
modifier = Modifier.fillMaxWidth().wrapContentHeight()
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Text(
text = "*",
@ -74,7 +90,10 @@ fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewMode
)
}
Row(
modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 12.dp)
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 12.dp)
) {
Box(
modifier = Modifier
@ -172,10 +191,15 @@ fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewMode
}
//补充反馈内容
Column(
modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 18.dp)
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 18.dp)
) {
Row(
modifier = Modifier.fillMaxWidth().wrapContentHeight()
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Text(
text = "*",
@ -255,10 +279,15 @@ fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewMode
}
//提供相关图片
Column(
modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 18.dp)
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 18.dp)
) {
Row(
modifier = Modifier.fillMaxWidth().wrapContentHeight()
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Text(
text = "请提供相关问题的图片",
@ -271,35 +300,48 @@ fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewMode
color = Color(0xFFAAAAAA),
fontSize = 11.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier.wrapContentSize()
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterVertically)
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight().padding(top = 12.dp)
.wrapContentHeight()
.padding(top = 12.dp)
) {
ImagePicker(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
imageHeight = 100.dp,
aspectRatio = 100f / 100f,
maxCount = 3,
addButtonName = "添加图片",
currentImageUris = viewModel.currentImageUris,
currentImagePaths = emptyList(), // 新增参数
onImagesUpdated = { uris, _ ->
onImagesUpdated = { uri, uris ->
viewModel.setCurrentImageUris(uris)
viewModel.uploadImage(context, uri)
},
onDeleteUpdated = { uri, uris ->
viewModel.setCurrentImageUris(uris)
viewModel.deleteImage(uri)
}
)
}
}
//联系方式
Column(
modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 18.dp)
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 18.dp)
) {
Row(
modifier = Modifier.fillMaxWidth().wrapContentHeight()
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Text(
text = "联系方式",
@ -312,7 +354,8 @@ fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewMode
color = Color(0xFFAAAAAA),
fontSize = 11.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier.wrapContentSize()
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterVertically)
)
}
@ -342,7 +385,12 @@ fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewMode
.fillMaxWidth()
.wrapContentHeight()
.background(Color.Transparent)
.padding(start = 18.dp, top = 10.dp, end = 18.dp, bottom = 10.dp),
.padding(
start = 18.dp,
top = 10.dp,
end = 18.dp,
bottom = 10.dp
),
textStyle = androidx.compose.ui.text.TextStyle(
color = Color.Black,
fontSize = 14.sp
@ -390,8 +438,21 @@ fun FeedbackScreen(navController: NavHostController, viewModel: FeedbackViewMode
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
//TODO 提交反馈
viewModel.submitFeedback()
if (viewModel.feedbackMore.isEmpty()) {
Toast.makeText(context, "请填写详细问题或意见...", Toast.LENGTH_SHORT).show()
return@clickable
}
// 提交反馈
val jsonObject = JsonObject()
jsonObject.addProperty("type", viewModel.feedbackType.value)
jsonObject.addProperty("content", viewModel.feedbackMore)
jsonObject.addProperty("contact", viewModel.feedbackContact)
val imageArray = JsonArray()
viewModel.uploadFiles.forEach { fileEntity ->
imageArray.add(fileEntity.fileEntity.url)
}
jsonObject.add("images", imageArray)
viewModel.submitFeedback(jsonObject.toString().toRequestBody())
}
.align(Alignment.BottomCenter)
) {

View File

@ -19,25 +19,61 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
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.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.img.rabbit.R
import com.img.rabbit.pages.toolbar.TitleBar
import com.img.rabbit.utils.AppDataStoreUtils
import com.img.rabbit.viewmodel.LoginViewModel
@Composable
fun SettingScreen(navController: NavHostController) {
fun SettingScreen(navController: NavHostController, loginViewModel: LoginViewModel) {
val context = LocalContext.current
var cacheDataSize by remember { mutableStateOf("正在计算...") }
/*
LaunchedEffect(loginViewModel.logoutState.value) {
if(loginViewModel.logoutState.value?.status == true){
// 执行用户数据清除
PreferenceUtil.clearLogin()
loginViewModel.setLogin(false)
// 跳转登录页面
loginViewModel.restViewModel.intValue = 1
loginViewModel.logoutState.value = null
loginViewModel.loginScreenType.value = LoginScreenType.LOGIN_NORMAL
navController.navigate("login?type=${LoginViewModel.JumpLoginType.FROM_LOGOUT.type}") {
// 清除所有页面,包括登录页
popUpTo(navController.graph.startDestinationId) { inclusive = true }
}
}
}
*/
LaunchedEffect(Unit) {
AppDataStoreUtils.getAppStorageStats(context){ _, _, _, totalDataCacheSize ->
cacheDataSize = totalDataCacheSize
}
}
Scaffold{
Column(
modifier = Modifier.fillMaxSize()
@ -47,7 +83,7 @@ fun SettingScreen(navController: NavHostController) {
Box(
modifier = Modifier.fillMaxSize().padding(start = 17.dp, end = 17.dp)
){
//TODO 设置内容
// 设置内容
Column(
modifier = Modifier
.wrapContentSize()
@ -72,7 +108,8 @@ fun SettingScreen(navController: NavHostController) {
interactionSource = remember { MutableInteractionSource() }
) {
// 跳转页面
//navController.navigate("feedback")
//navController.navigate("")
AppDataStoreUtils.openAppSettings(context)
},
verticalAlignment = Alignment.CenterVertically
) {
@ -94,7 +131,7 @@ fun SettingScreen(navController: NavHostController) {
modifier = Modifier.wrapContentSize()
) {
Text(
"12MB",
cacheDataSize,
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier
@ -292,7 +329,8 @@ fun SettingScreen(navController: NavHostController) {
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
//TODO 退出登录
// 退出登录
loginViewModel.requestLogout(context)
}
) {
Text(
@ -316,5 +354,5 @@ fun SettingScreen(navController: NavHostController) {
@Preview(showBackground = true)
@Composable
private fun PreviewSettingScreen(){
SettingScreen(navController = rememberNavController())
SettingScreen(navController = rememberNavController(), loginViewModel = viewModel())
}

View File

@ -1,6 +1,7 @@
package com.img.rabbit.pages.screen.mine.setting
import android.annotation.SuppressLint
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -19,11 +20,13 @@ 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.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -31,21 +34,46 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
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.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.img.rabbit.R
import com.img.rabbit.pages.toolbar.TitleBar
import com.img.rabbit.provider.storage.PreferenceUtil
import com.img.rabbit.viewmodel.AccountBindViewModel
import com.img.rabbit.viewmodel.BindViewModel
@SuppressLint("UnrememberedMutableState")
@Composable
fun AccountBindScreen(navController: NavHostController) {
val showDialogStatus = mutableStateOf(false)
fun AccountBindScreen(navController: NavHostController, viewModel: AccountBindViewModel = viewModel()) {
val context = LocalContext.current
/**
* 0:m默认未绑定1:已绑定手机号去解绑2:已绑定微信去解绑
*/
val showDialogStatus = mutableIntStateOf(0)
val userInfo by remember { mutableStateOf(PreferenceUtil.getUserInfo()) }
LaunchedEffect(viewModel.unBindState.value) {
if(viewModel.unBindState.value != null){
Toast.makeText(context, "解绑成功!", Toast.LENGTH_SHORT).show()
viewModel.unBindState.value = null
}
}
LaunchedEffect(viewModel.errorState.value) {
if(viewModel.errorState.value != null){
Toast.makeText(context, "解绑失败...", Toast.LENGTH_SHORT).show()
viewModel.errorState.value = null
}
}
Scaffold{
Box(
modifier = Modifier.fillMaxSize()
@ -56,9 +84,11 @@ fun AccountBindScreen(navController: NavHostController) {
TitleBar(navController = navController, paddingValues = it, title = "账号绑定")
Box(
modifier = Modifier.fillMaxSize().padding(start = 17.dp, end = 17.dp)
modifier = Modifier
.fillMaxSize()
.padding(start = 17.dp, end = 17.dp)
){
//TODO 设置内容
// 设置内容
Column(
modifier = Modifier
.wrapContentSize()
@ -82,8 +112,12 @@ fun AccountBindScreen(navController: NavHostController) {
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
//TODO 跳转绑定与解绑手机号
showDialogStatus.value = !showDialogStatus.value
// 跳转绑定与解绑手机号
if (userInfo?.phone.isNullOrEmpty()) {
navController.navigate("bind?type=${BindViewModel.BindType.FROM_PHONE.type}")
} else {
showDialogStatus.intValue = 1
}
},
verticalAlignment = Alignment.CenterVertically
) {
@ -104,8 +138,14 @@ fun AccountBindScreen(navController: NavHostController) {
Row(
modifier = Modifier.wrapContentSize()
) {
val phone = if(userInfo?.phone.isNullOrEmpty()){
"去绑定"
}else{
"+86 ${userInfo?.phone?.replaceRange(3, 7, "****") ?: ""}"
}
Text(
"+86 123****123",
phone,
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier
@ -126,7 +166,11 @@ fun AccountBindScreen(navController: NavHostController) {
}
}
Box(
modifier = Modifier.fillMaxWidth().height(0.5.dp).padding(horizontal = 12.dp).background(
modifier = Modifier
.fillMaxWidth()
.height(0.5.dp)
.padding(horizontal = 12.dp)
.background(
Color(0x4DBBBBBB)
)
)
@ -139,6 +183,11 @@ fun AccountBindScreen(navController: NavHostController) {
interactionSource = remember { MutableInteractionSource() }
) {
//TODO 跳转绑定或解绑微信
if (userInfo?.weixinAppId.isNullOrEmpty()) {
navController.navigate("bind?type=${BindViewModel.BindType.FROM_WX.type}")
} else {
showDialogStatus.intValue = 2
}
},
verticalAlignment = Alignment.CenterVertically
) {
@ -156,13 +205,16 @@ fun AccountBindScreen(navController: NavHostController) {
)
}
Row(
modifier = Modifier.wrapContentSize()
) {
val wxBind = if(userInfo?.weixinAppId.isNullOrEmpty()){
"去绑定"
}else{
"已绑定,去解绑"
}
Text(
"去绑定",
wxBind,
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier
@ -186,11 +238,21 @@ fun AccountBindScreen(navController: NavHostController) {
}
}
if(showDialogStatus.value){
if(showDialogStatus.intValue > 0){
UnBindPhoneDialog(
showDialogStatus = showDialogStatus,
onStatusChange = {
showDialogStatus.value = it
onStatusChange = { stateVal, typeVal, isCancel ->
if(!isCancel){
if(typeVal == 1){
// 解绑手机号
PreferenceUtil.getUserInfo()?.phone?.let { phone -> viewModel.requestUnBindByPhone(phone) }
}else{
// 解绑微信
PreferenceUtil.getWxCode()?.let { wechatCode -> viewModel.requestUnBindByWechat(wechatCode) }
}
}
showDialogStatus.intValue = stateVal
}
)
}
@ -200,9 +262,25 @@ fun AccountBindScreen(navController: NavHostController) {
@Composable
private fun UnBindPhoneDialog(
showDialogStatus: MutableState<Boolean>,
onStatusChange: (Boolean) -> Unit
showDialogStatus: MutableState<Int>,
onStatusChange: (state:Int, type:Int, isCancel:Boolean) -> Unit
){
val title = if(showDialogStatus.value == 1){
"解开绑定的手机号?"
}else{
"确定解除绑定的微信?"
}
val content = if(showDialogStatus.value == 1){
"当前绑定的手机号码为"
}else{
"解除后将无法使用该微信登录此账号,请谨慎操作!"
}
val content1 = if(showDialogStatus.value == 1){
"+86 ${PreferenceUtil.getUserInfo()?.phone?.replaceRange(3, 7, "****") ?: ""}"
}else{
""
}
Column(
modifier = Modifier
.fillMaxSize()
@ -210,8 +288,8 @@ private fun UnBindPhoneDialog(
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
){
showDialogStatus.value = false
) {
onStatusChange(0, showDialogStatus.value, true)
},
verticalArrangement = Arrangement.Center
){
@ -225,7 +303,7 @@ private fun UnBindPhoneDialog(
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
){
) {
//什么都不用做,只是解决点击穿透问题
}
) {
@ -244,7 +322,7 @@ private fun UnBindPhoneDialog(
.padding(top = 46.dp)
) {
Text(
text = "解开绑定的手机号?",
text = title,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF1A1A1A),
@ -260,7 +338,7 @@ private fun UnBindPhoneDialog(
)
Text(
text = "当前绑定的手机号码为",
text = content,
fontSize = 12.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF767676),
@ -270,7 +348,7 @@ private fun UnBindPhoneDialog(
)
Text(
text = "+86 123****456",
text = content1,
fontSize = 12.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFF767676),
@ -288,7 +366,9 @@ private fun UnBindPhoneDialog(
//切换/退出账号
Row(
modifier = Modifier.fillMaxWidth().padding(start = 18.dp, end = 18.dp, bottom = 20.dp)
modifier = Modifier
.fillMaxWidth()
.padding(start = 18.dp, end = 18.dp, bottom = 20.dp)
) {
//取消解绑手机号
Box(
@ -300,13 +380,17 @@ private fun UnBindPhoneDialog(
Color(0x00000000),
shape = RoundedCornerShape(359.dp),
)
.border(width = 1.dp, color = Color(0xFF000000), shape = RoundedCornerShape(359.dp))
.border(
width = 1.dp,
color = Color(0xFF000000),
shape = RoundedCornerShape(359.dp)
)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
//TODO 取消解绑手机号
showDialogStatus.value = false
// 取消解绑手机号
onStatusChange(0, showDialogStatus.value, true)
}
) {
Text(
@ -344,7 +428,7 @@ private fun UnBindPhoneDialog(
interactionSource = remember { MutableInteractionSource() }
) {
//TODO 确定解绑手机号
showDialogStatus.value = false
onStatusChange(0, showDialogStatus.value, false)
}
) {
Text(
@ -376,5 +460,5 @@ private fun PreviewAccountBindScreen(){
@Preview(showBackground = true)
@Composable
private fun PreviewUnBindPhoneDialog(){
UnBindPhoneDialog(showDialogStatus = mutableStateOf(true), onStatusChange = {})
UnBindPhoneDialog(showDialogStatus = mutableIntStateOf(0), onStatusChange = { _, _, _->})
}

View File

@ -20,6 +20,7 @@ import androidx.compose.material3.Checkbox
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -35,15 +36,18 @@ 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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.img.rabbit.R
import com.img.rabbit.bean.local.UserInfo
import com.img.rabbit.pages.toolbar.TitleBar
import com.img.rabbit.viewmodel.AccountManagerViewModel
import com.img.rabbit.viewmodel.LoginViewModel
@Composable
fun AccountManagerScreen(navController: NavHostController) {
fun AccountManagerScreen(navController: NavHostController, viewModel: AccountManagerViewModel = viewModel()) {
val userList by remember {
mutableStateOf(
listOf(
@ -53,6 +57,10 @@ fun AccountManagerScreen(navController: NavHostController) {
)
)
}
LaunchedEffect(Unit) {
viewModel.requestAccount()
}
Scaffold{
Box(
modifier = Modifier.fillMaxSize()
@ -225,7 +233,7 @@ private fun AddItem(navController: NavController){
interactionSource = remember { MutableInteractionSource() }
) {
//TODO 添加账号
navController.navigate("login")
navController.navigate("login?type=${LoginViewModel.JumpLoginType.FROM_ADD.type}")
}
) {
Row(

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,9 @@ fun TitleBar(navController: NavController?, paddingValues: PaddingValues, title:
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { navController?.popBackStack() }
) {
navController?.popBackStack()
}
.padding(end = 26.dp)
)
}

View File

@ -5,6 +5,7 @@ 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.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
@ -18,6 +19,7 @@ class GlobalStateManager(
companion object {
private val GLOBAL_LOADING = booleanPreferencesKey("global_loading")
private val GLOBAL_WX_AUTHORIZATION = stringPreferencesKey("global_wx_authorization")
private val GLOBAL_LOGOUT = booleanPreferencesKey("global_logout")
}
suspend fun storeGlobalLoading(value: Boolean) {
@ -46,4 +48,17 @@ class GlobalStateManager(
}
}
suspend fun storeGlobalLogout(value: Boolean) {
context.storeData.edit { preferences ->
preferences[GLOBAL_LOGOUT] = value
}
}
fun globalLogoutFlow(): Flow<Boolean?> {
return context.storeData.data.map {
preferences ->
preferences[GLOBAL_LOGOUT]
}
}
}

View File

@ -1,9 +1,16 @@
package com.img.rabbit.provider.storage
import android.text.TextUtils
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.img.rabbit.bean.response.UserEntity
import com.tencent.mmkv.MMKV
import com.img.rabbit.bean.response.UserConfigEntity
import com.img.rabbit.bean.response.LoginInfoEntity
import com.img.rabbit.bean.response.UserInfoEntity
import com.img.rabbit.provider.utils.HeadParamUtils.applicationContext
import com.img.rabbit.utils.MMKVUtils
import com.img.rabbit.utils.MMKVUtils.mmkv
import com.img.rabbit.utils.appwalle.ChannelReader
import org.json.JSONObject
/**
* SharedPreferences工具类用于简化数据持久化操作
@ -11,76 +18,98 @@ import com.tencent.mmkv.MMKV
object PreferenceUtil {
private const val KEY_ACCESS_TOKEN = "access_token"
private const val KEY_X_TOKEN = "x_token"
private const val KEY_LOGIN_INFO = "login_info"
private const val KEY_DB_VID = "bd_vid"
private const val KEY_USER_CONFIG = "user_config"
private const val KEY_WX_CODE = "wx_code"
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
// Gson实例
private val gson: Gson = GsonBuilder().create()
/**
* 保存AccessToken
*/
fun saveAccessToken(token: String?) {
kv.encode(KEY_ACCESS_TOKEN, token)
mmkv.encode(KEY_ACCESS_TOKEN, token)
mmkv.encode(KEY_X_TOKEN, token)
}
/**
* 获取保存的AccessToken
*/
fun getAccessToken(): String? {
return kv.decodeString(KEY_ACCESS_TOKEN, null)
return mmkv.decodeString(KEY_ACCESS_TOKEN, null)
}
fun saveXToken(token: String?) {
kv.encode(KEY_X_TOKEN, token)
mmkv.encode(KEY_X_TOKEN, token)
}
fun getXToken(): String? {
return kv.decodeString(KEY_X_TOKEN, null)
return mmkv.decodeString(KEY_X_TOKEN, null)
}
fun saveUserConfig(config: UserConfigEntity) {
val resultJson = Gson().toJson(config)
mmkv.encode(KEY_USER_CONFIG, resultJson)
}
fun getUserConfig(): UserConfigEntity? {
return gson.fromJson(mmkv.decodeString(KEY_USER_CONFIG, "{}"), UserConfigEntity::class.java)
}
fun getUserInfos(): MutableList<UserEntity>? {
fun loginInfo(): LoginInfoEntity?{
return getLoginInfos()?.find { it.isLogin }
}
fun saveLoginInfo(loginInfoEntity: LoginInfoEntity) {
val loginInfos = getLoginInfos()?: mutableListOf()
val isContain = loginInfos.find { it.user_id == loginInfoEntity.user_id } != null
if(!isContain){
loginInfos.add(loginInfoEntity)
}
mmkv.encode(KEY_LOGIN_INFO, gson.toJson(loginInfos))
}
fun removeLoginInfo(loginInfoEntity: LoginInfoEntity) {
val loginInfos = getLoginInfos()?: mutableListOf()
loginInfos.removeIf { it.user_id == loginInfoEntity.user_id }
mmkv.encode(KEY_LOGIN_INFO, gson.toJson(loginInfos))
}
fun getLoginInfos(): MutableList<LoginInfoEntity>? {
/**
*[{"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<UserEntity>::class.java)?.toMutableList()
return gson.fromJson(mmkv.decodeString(KEY_LOGIN_INFO, "[]"), Array<LoginInfoEntity>::class.java)?.toMutableList()
}
fun loginUserInfo(): UserEntity?{
return getUserInfos()?.find { it.isLogin }
fun saveUserInfo(userInfoEntity: UserInfoEntity) {
mmkv.encode(KEY_USER_INFO, gson.toJson(userInfoEntity))
}
fun saveUserInfo(userEntity: UserEntity) {
val userInfos = getUserInfos()?: mutableListOf()
val isContain = userInfos.find { it.user_id == userEntity.user_id } != null
if(!isContain){
userInfos.add(userEntity)
fun getUserInfo(): UserInfoEntity? {
return gson.fromJson(mmkv.decodeString(KEY_USER_INFO, "{}"), UserInfoEntity::class.java)
}
kv.encode(KEY_USER_INFO, gson.toJson(userInfos))
fun saveWxCode(code: String) {
mmkv.encode(KEY_WX_CODE, code)
}
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 getWxCode(): String? {
return mmkv.decodeString(KEY_WX_CODE, null)
}
//真实的服务器时间
@ -90,4 +119,50 @@ object PreferenceUtil {
fun setTimeDiff(timeDiff: Long) {
this.timeDiff = timeDiff
}
/**
* 保存百度归因bd_vid
*/
fun saveBDVID() {
try {
val str = applicationContext?.let { ChannelReader.get(it) }
if (!TextUtils.isEmpty(str)) {
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
val jsonObj = JSONObject(str)
jsonObj.getString("bd_vid").let {
MMKVUtils.put("bd_vid", it)
}
} else {
MMKVUtils.put("bd_vid", "")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 获取百度归因bd_vid
*/
fun getBDVID(): String {
return MMKVUtils.getString(KEY_DB_VID) ?: ""
}
/**
* 退出登录时清除用户信息
*/
fun clearLogin(){
mmkv.remove(KEY_ACCESS_TOKEN)
mmkv.remove(KEY_X_TOKEN)
mmkv.remove(KEY_LOGIN_INFO)
mmkv.remove(KEY_USER_INFO)
mmkv.remove(KEY_WX_CODE)
}
/**
* 清除所有数据
*/
fun clearAll() {
mmkv.clearAll()
}
}

View File

@ -0,0 +1,40 @@
package com.img.rabbit.provider.utils
import android.content.Context
import android.content.pm.PackageManager
import com.img.rabbit.R
import okhttp3.internal.platform.ContextAwarePlatform
import okhttp3.internal.platform.Platform
object HeadParamUtils {
var applicationContext: Context?
get() = (Platform.get() as? ContextAwarePlatform)?.applicationContext
set(value) {
(Platform.get() as? ContextAwarePlatform)?.applicationContext = value
}
fun getAppVersionName(): String? {
return try {
val pm = applicationContext?.packageManager
val pi = applicationContext?.packageName?.let { pm?.getPackageInfo(it, 0) }
pi?.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
null
}
}
fun getAppName(): String? {
return applicationContext?.resources?.getString(R.string.app_name)
}
fun buildVersionNo(): Int? {
return try {
val pm = applicationContext?.packageManager
val pi = applicationContext?.packageName?.let { pm?.getPackageInfo(it, 0) }
pi?.versionCode
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
null
}
}
}

View File

@ -1,7 +1,15 @@
package com.img.rabbit.provider.utils
import android.util.Log
import com.github.gzuliyujiang.oaid.DeviceIdentifier
import com.img.rabbit.provider.storage.PreferenceUtil
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.getBDVID
import com.img.rabbit.provider.utils.HeadParamUtils.applicationContext
import com.img.rabbit.provider.utils.HeadParamUtils.getAppVersionName
import com.img.rabbit.utils.ChannelUtils
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
@ -15,10 +23,32 @@ class HeaderInterceptor : Interceptor {
val mBuilder = original.newBuilder()
val accessToken = PreferenceUtil.getXToken()?:""
mBuilder.header("x-token", accessToken)
mBuilder.header("x-version", getAppVersionName()?:"")
mBuilder.header("x-platform", "android")
mBuilder.header("x-device-id", DeviceIdentifier.getAndroidID(applicationContext))
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)
mBuilder.header("x-base-version", getAppVersionName()?:"")
mBuilder.header("x-channel", "rabbit_${ChannelUtils.getChannel(applicationContext)}")
mBuilder.header("x-click-id", getBDVID())
mBuilder.header("x-app-id", Constants.AppId)
val stringBuilder = StringBuilder()
stringBuilder.append("-------------> 📤 header start<-------------\n")
stringBuilder.append("│ x-token = $accessToken\n")
stringBuilder.append("│ x-version = ${getAppVersionName()}\n")
stringBuilder.append("│ x-device-id = ${DeviceIdentifier.getAndroidID(applicationContext)}\n")
stringBuilder.append("│ x-mobile-brand = ${android.os.Build.BRAND}\n")
stringBuilder.append("│ x-mobile-model = ${android.os.Build.MODEL}\n")
stringBuilder.append("│ x-base-version = ${getAppVersionName()}\n")
stringBuilder.append("│ x-channel = rabbit_${ChannelUtils.getChannel(applicationContext)}\n")
stringBuilder.append("│ x-click-id = ${getBDVID()}\n")
stringBuilder.append("│ x-package = ${BuildConfig.APPLICATION_ID}\n")
stringBuilder.append("-------------> header end <-------------")
Log.i(LOG_REQUEST, stringBuilder.toString())
val request = mBuilder
.method(original.method, original.body)

View File

@ -2,6 +2,8 @@ package com.img.rabbit.route
// 定义导航路由
sealed class ScreenRoute(val route: String) {
//Main页面
object Main : ScreenRoute("main")
//Tab页面
object Home : ScreenRoute("home")
object Mine : ScreenRoute("mine")
@ -26,4 +28,5 @@ sealed class ScreenRoute(val route: String) {
object BindAccount : ScreenRoute("bindAccount")
object ManagerAccount : ScreenRoute("managerAccount")
object AboutMine : ScreenRoute("aboutMine")
object Bind : ScreenRoute("bind")
}

View File

@ -1,6 +1,14 @@
package com.img.rabbit.utils
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.usage.StorageStatsManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.storage.StorageManager
import android.provider.Settings
import android.text.format.Formatter
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
@ -8,6 +16,8 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.io.File
import java.util.UUID
// 创建DataStore实例
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "app_settings")
@ -46,3 +56,98 @@ class AppDataStore(private val context: Context) {
}
}
}
object AppDataStoreUtils {
fun getAppStorageStats(context: Context,onResult:(appSize:String,cacheSize:String,dataSize:String,totalDataCacheSize:String)->Unit) {
var cacheSize = "0MB"
var dataSize = "0MB"
var totalDataCacheSize = "0MB"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val storageStatsManager = context.getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
// 获取应用所在的存储卷 UUID (通常是内部存储)
val uuid: UUID = storageManager.getUuidForPath(context.filesDir)
// 查询当前应用的统计信息
val stats = storageStatsManager.queryStatsForUid(uuid, context.applicationInfo.uid)
// 格式化为易读字符串 (如 10.5 MB)
//val appSize = Formatter.formatFileSize(context, stats.appBytes) // 应用本身 (APK等)
dataSize = Formatter.formatFileSize(context, stats.dataBytes) // 用户数据 (包含缓存)
cacheSize = Formatter.formatFileSize(context, stats.cacheBytes) // 仅缓存数据
totalDataCacheSize = Formatter.formatFileSize(context, stats.dataBytes + stats.cacheBytes) // 数据缓存 (如数据库缓存)
}else{
// 1. 缓存大小 (Cache): 包括内部缓存和外部缓存
val internalCache = getFolderSize(context.cacheDir)
val externalCache = getFolderSize(context.externalCacheDir)
val totalCache = internalCache + externalCache
// 2. 数据大小 (Data): 通常指 files、databases、shared_prefs 等
val filesDirSize = getFolderSize(context.filesDir)
val dbDir = context.getDatabasePath("dummy").parentFile // 获取数据库根目录
val databasesSize = getFolderSize(dbDir)
val prefsSize = getFolderSize(File(context.filesDir.parent, "shared_prefs"))
val totalUserData = filesDirSize + databasesSize + prefsSize
// 格式化显示
cacheSize = Formatter.formatFileSize(context, totalCache)
dataSize = Formatter.formatFileSize(context, totalUserData)
totalDataCacheSize = Formatter.formatFileSize(context, totalCache + totalUserData)
}
onResult("",dataSize, cacheSize, totalDataCacheSize)
}
/**
* 递归计算文件夹或文件的大小
*/
fun getFolderSize(file: File?): Long {
if (file == null || !file.exists()) return 0L
var size: Long = 0
val fileList = file.listFiles() ?: return file.length() // 如果是文件直接返回长度
for (subFile in fileList) {
size += if (subFile.isDirectory) {
getFolderSize(subFile) // 递归遍历子目录
} else {
subFile.length()
}
}
return size
}
/**
* 引导用户打开应用设置页面手动清除
*/
fun openAppSettings(context: Context) {
try {
// 尝试直接进入存储详情页
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = android.net.Uri.fromParts("package", context.packageName, null)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
// 如果直达失败,降级跳转到应用详情主页
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = android.net.Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
}
}
/**
* 直接清除应用数据包括缓存和数据库
*/
fun clearAppData(context: Context) {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
// 这一步会直接清除所有私有数据并结束进程
val isSuccess = activityManager.clearApplicationUserData()
}
}

View File

@ -0,0 +1,51 @@
package com.img.rabbit.utils
import android.text.TextUtils
import com.img.rabbit.BuildConfig
import com.img.rabbit.bean.response.VersionEntity
object AppUpdate {
fun checkUpdate(versionInfo: VersionEntity?, onResult: (isUpdate: Boolean, tips: String) -> Unit) {
if(versionInfo == null){
onResult.invoke(false, "已是最新版本")
return
}
versionInfo.apply {
if (checkVersion(version, BuildConfig.VERSION_NAME)) {
if (last_version_force == BuildConfig.VERSION_NAME) {
force = true
} else if (checkVersion(last_version_force, BuildConfig.VERSION_NAME)) {
force = true
}
onResult.invoke(true, "检测到新版本")
//提示执行更新
//startUpdate(result, fragment)
} else {
onResult.invoke(false, "当前版本为最新版本")
}
}
}
private fun checkVersion(version: String, origin: String): Boolean {
if (TextUtils.isEmpty(version) || TextUtils.isEmpty(origin)) {
return false
}
val versions = version.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val origins = origin.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (versions.size != 3) {
return false
}
if (origins.size != 3) {
return false
}
for (i in 0..2) {
if (versions[i].toInt() > origins[i].toInt()) {
return true
} else if (versions[i].toInt() < origins[i].toInt()) {
return false
}
}
return false
}
}

View File

@ -11,12 +11,12 @@ import com.tencent.vasdolly.helper.ChannelReaderUtil
object ChannelUtils {
fun getChannel(context: Context): String {
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))
MMKVUtils.put("app_channel", getChannelBy(context?:return ""))
}
}
return MMKVUtils.getString("app_channel") ?: ""

View File

@ -25,6 +25,25 @@ import java.io.ByteArrayOutputStream
import java.io.OutputStream
object ImageUtils {
fun imageCompressToUri(context: Context, uri: Uri): ByteArray? {
// 获取输入流并解码为Bitmap
val inputStream = context.contentResolver.openInputStream(uri)
?: throw IOException("无法从Uri中读取文件(File)")
val bitmap = inputStream.use { inputStream ->
BitmapFactory.decodeStream(inputStream)
} ?: throw IOException("无法解码图片")
// 压缩图片 (JPEG格式80%质量)
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
val compressedByteArray = outputStream.toByteArray()
bitmap.recycle() // 释放Bitmap内存
// 使用压缩后的字节数组
return compressedByteArray
}
fun decodeSampledBitmapFromResource(
context: Context,
resId: Int,

View File

@ -6,21 +6,17 @@ import java.util.Collections
object MMKVUtils {
var mmkv: MMKV? = null
init {
mmkv = MMKV.defaultMMKV()
}
val mmkv by lazy { 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)!!
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
}
}
@ -32,68 +28,68 @@ object MMKVUtils {
if (t == null) {
return false
}
return mmkv?.encode(key, t)!!
return mmkv.encode(key, t)
}
fun put(key: String, sets: Set<String>?): Boolean {
if (sets == null) {
return false
}
return mmkv?.encode(key, sets)!!
return mmkv.encode(key, sets)
}
fun getInt(key: String): Int? {
return mmkv?.decodeInt(key, 0)
fun getInt(key: String): Int {
return mmkv.decodeInt(key, 0)
}
fun getDouble(key: String): Double? {
return mmkv?.decodeDouble(key, 0.00)
fun getDouble(key: String): Double {
return mmkv.decodeDouble(key, 0.00)
}
fun getLong(key: String): Long? {
return mmkv?.decodeLong(key, 0L)
fun getLong(key: String): Long {
return mmkv.decodeLong(key, 0L)
}
fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
return mmkv?.decodeBool(key, defaultValue) ?: defaultValue
return mmkv.decodeBool(key, defaultValue) ?: defaultValue
}
fun getFloat(key: String): Float? {
return mmkv?.decodeFloat(key, 0F)
fun getFloat(key: String): Float {
return mmkv.decodeFloat(key, 0F)
}
fun getByteArray(key: String): ByteArray? {
return mmkv?.decodeBytes(key)
return mmkv.decodeBytes(key)
}
fun getString(key: String): String? {
return mmkv?.decodeString(key, "")
return mmkv.decodeString(key, "")
}
/**
* SpUtils.getParcelable<Class>("")
*/
inline fun <reified T : Parcelable> getParcelable(key: String): T? {
return mmkv?.decodeParcelable(key, T::class.java)
return mmkv.decodeParcelable(key, T::class.java)
}
fun getStringSet(key: String): Set<String>? {
return mmkv?.decodeStringSet(key, Collections.emptySet())
return mmkv.decodeStringSet(key, Collections.emptySet())
}
/**
* 是否已经存在
*/
fun contains(key: String): Boolean? {
return mmkv?.containsKey(key)
fun contains(key: String): Boolean {
return mmkv.containsKey(key)
}
fun removeKey(key: String) {
mmkv?.removeValueForKey(key)
mmkv.removeValueForKey(key)
}
fun clearAll() {
mmkv?.clearAll()
mmkv.clearAll()
}
}

View File

@ -0,0 +1,187 @@
package com.img.rabbit.utils.appwalle;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.LinkedHashMap;
import java.util.Map;
final class ApkUtil {
private ApkUtil() {
}
public static long getCommentLength(FileChannel fileChannel) throws IOException {
long archiveSize = fileChannel.size();
if (archiveSize < 22L) {
throw new IOException("APK too small for ZIP End of Central Directory (EOCD) record");
} else {
long maxCommentLength = Math.min(archiveSize - 22L, 65535L);
long eocdWithEmptyCommentStartPosition = archiveSize - 22L;
for(int expectedCommentLength = 0; (long)expectedCommentLength <= maxCommentLength; ++expectedCommentLength) {
long eocdStartPos = eocdWithEmptyCommentStartPosition - (long)expectedCommentLength;
ByteBuffer byteBuffer = ByteBuffer.allocate(4);
fileChannel.position(eocdStartPos);
fileChannel.read(byteBuffer);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
if (byteBuffer.getInt(0) == 101010256) {
ByteBuffer commentLengthByteBuffer = ByteBuffer.allocate(2);
fileChannel.position(eocdStartPos + 20L);
fileChannel.read(commentLengthByteBuffer);
commentLengthByteBuffer.order(ByteOrder.LITTLE_ENDIAN);
int actualCommentLength = commentLengthByteBuffer.getShort(0);
if (actualCommentLength == expectedCommentLength) {
return actualCommentLength;
}
}
}
throw new IOException("ZIP End of Central Directory (EOCD) record not found");
}
}
public static long findCentralDirStartOffset(FileChannel fileChannel) throws IOException {
return findCentralDirStartOffset(fileChannel, getCommentLength(fileChannel));
}
public static long findCentralDirStartOffset(FileChannel fileChannel, long commentLength) throws IOException {
ByteBuffer zipCentralDirectoryStart = ByteBuffer.allocate(4);
zipCentralDirectoryStart.order(ByteOrder.LITTLE_ENDIAN);
fileChannel.position(fileChannel.size() - commentLength - 6L);
fileChannel.read(zipCentralDirectoryStart);
return zipCentralDirectoryStart.getInt(0);
}
public static Pair<ByteBuffer, Long> findApkSigningBlock(FileChannel fileChannel) throws IOException, SignatureNotFoundException {
long centralDirOffset = findCentralDirStartOffset(fileChannel);
return findApkSigningBlock(fileChannel, centralDirOffset);
}
public static Pair<ByteBuffer, Long> findApkSigningBlock(FileChannel fileChannel, long centralDirOffset) throws IOException, SignatureNotFoundException {
if (centralDirOffset < 32L) {
throw new SignatureNotFoundException("APK too small for APK Signing Block. ZIP Central Directory offset: " + centralDirOffset);
} else {
fileChannel.position(centralDirOffset - 24L);
ByteBuffer footer = ByteBuffer.allocate(24);
fileChannel.read(footer);
footer.order(ByteOrder.LITTLE_ENDIAN);
if (footer.getLong(8) == 2334950737559900225L && footer.getLong(16) == 3617552046287187010L) {
long apkSigBlockSizeInFooter = footer.getLong(0);
if (apkSigBlockSizeInFooter >= (long)footer.capacity() && apkSigBlockSizeInFooter <= 2147483639L) {
int totalSize = (int)(apkSigBlockSizeInFooter + 8L);
long apkSigBlockOffset = centralDirOffset - (long)totalSize;
if (apkSigBlockOffset < 0L) {
throw new SignatureNotFoundException("APK Signing Block offset out of range: " + apkSigBlockOffset);
} else {
fileChannel.position(apkSigBlockOffset);
ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize);
fileChannel.read(apkSigBlock);
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
throw new SignatureNotFoundException("APK Signing Block sizes in header and footer do not match: " + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
} else {
return Pair.of(apkSigBlock, apkSigBlockOffset);
}
}
} else {
throw new SignatureNotFoundException("APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
}
} else {
throw new SignatureNotFoundException("No APK Signing Block before ZIP Central Directory");
}
}
}
public static Map<Integer, ByteBuffer> findIdValues(ByteBuffer apkSigningBlock) throws SignatureNotFoundException {
checkByteOrderLittleEndian(apkSigningBlock);
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
Map<Integer, ByteBuffer> idValues = new LinkedHashMap();
int entryCount = 0;
while(pairs.hasRemaining()) {
++entryCount;
if (pairs.remaining() < 8) {
throw new SignatureNotFoundException("Insufficient data to read size of APK Signing Block entry #" + entryCount);
}
long lenLong = pairs.getLong();
if (lenLong < 4L || lenLong > 2147483647L) {
throw new SignatureNotFoundException("APK Signing Block entry #" + entryCount + " size out of range: " + lenLong);
}
int len = (int)lenLong;
int nextEntryPos = pairs.position() + len;
if (len > pairs.remaining()) {
throw new SignatureNotFoundException("APK Signing Block entry #" + entryCount + " size out of range: " + len + ", available: " + pairs.remaining());
}
int id = pairs.getInt();
idValues.put(id, getByteBuffer(pairs, len - 4));
pairs.position(nextEntryPos);
}
return idValues;
}
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
if (start < 0) {
throw new IllegalArgumentException("start: " + start);
} else if (end < start) {
throw new IllegalArgumentException("end < start: " + end + " < " + start);
} else {
int capacity = source.capacity();
if (end > source.capacity()) {
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
} else {
int originalLimit = source.limit();
int originalPosition = source.position();
ByteBuffer var7;
try {
source.position(0);
source.limit(end);
source.position(start);
ByteBuffer result = source.slice();
result.order(source.order());
var7 = result;
} finally {
source.position(0);
source.limit(originalLimit);
source.position(originalPosition);
}
return var7;
}
}
}
private static ByteBuffer getByteBuffer(ByteBuffer source, int size) throws BufferUnderflowException {
if (size < 0) {
throw new IllegalArgumentException("size: " + size);
} else {
int originalLimit = source.limit();
int position = source.position();
int limit = position + size;
if (limit >= position && limit <= originalLimit) {
source.limit(limit);
ByteBuffer var6;
try {
ByteBuffer result = source.slice();
result.order(source.order());
source.position(limit);
var6 = result;
} finally {
source.limit(originalLimit);
}
return var6;
} else {
throw new BufferUnderflowException();
}
}
}
private static void checkByteOrderLittleEndian(ByteBuffer buffer) {
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
}
}
}

View File

@ -0,0 +1,61 @@
package com.img.rabbit.utils.appwalle;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.File;
/**
* 封装读取逻辑
*/
public final class ChannelReader {
private ChannelReader() {
}
/**
* 读取注入的内容
* @param context
* @return
*/
public static String get(@NonNull Context context) {
String apkPath = getApkPath(context);
String raw = TextUtils.isEmpty(apkPath) ? null : getRaw(new File(apkPath));
if (!TextUtils.isEmpty(raw)) {
raw = raw.replaceAll("\"", "");
raw = raw.replaceAll("#", "");
}
try {
if (TextUtils.isEmpty(raw)) {
return "";
}
return new String(Base64.decode(raw.getBytes(), Base64.NO_WRAP));
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
public static String getApkPath(@NonNull Context context) {
String apkPath = null;
try {
ApplicationInfo applicationInfo = context.getApplicationInfo();
if (applicationInfo == null) {
return null;
}
apkPath = applicationInfo.sourceDir;
} catch (Exception var3) {
Log.d("异常", var3.getMessage());
}
return apkPath;
}
public static String getRaw(File apkFile) {
return PayloadReader.getString(apkFile, 1896449981);
}
}

View File

@ -0,0 +1,53 @@
package com.img.rabbit.utils.appwalle;
final class Pair<A, B> {
private final A mFirst;
private final B mSecond;
private Pair(A first, B second) {
this.mFirst = first;
this.mSecond = second;
}
public static <A, B> Pair<A, B> of(A first, B second) {
return new Pair<>(first, second);
}
public A getFirst() {
return this.mFirst;
}
public B getSecond() {
return this.mSecond;
}
public int hashCode() {
int result = 1;
result = 31 * result + (this.mFirst == null ? 0 : this.mFirst.hashCode());
result = 31 * result + (this.mSecond == null ? 0 : this.mSecond.hashCode());
return result;
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj == null) {
return false;
} else if (this.getClass() != obj.getClass()) {
return false;
} else {
@SuppressWarnings("rawtypes") Pair other = (Pair) obj;
if (this.mFirst == null) {
if (other.mFirst != null) {
return false;
}
} else if (!this.mFirst.equals(other.mFirst)) {
return false;
}
if (this.mSecond == null) {
return other.mSecond == null;
} else return this.mSecond.equals(other.mSecond);
}
}
}

View File

@ -0,0 +1,85 @@
package com.img.rabbit.utils.appwalle;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
public final class PayloadReader {
private PayloadReader() {
}
/**
* 读取指定ID的数据
*
* @param apkFile
* @param id
* @return
*/
public static String getString(File apkFile, int id) {
byte[] bytes = get(apkFile, id);
if (bytes == null) {
return null;
} else {
return new String(bytes, StandardCharsets.UTF_8);
}
}
public static byte[] get(File apkFile, int id) {
Map<Integer, ByteBuffer> idValues = getAll(apkFile);
if (idValues == null) {
return null;
} else {
ByteBuffer byteBuffer = (ByteBuffer) idValues.get(id);
return byteBuffer == null ? null : getBytes(byteBuffer);
}
}
private static byte[] getBytes(ByteBuffer byteBuffer) {
byte[] array = byteBuffer.array();
int arrayOffset = byteBuffer.arrayOffset();
return Arrays.copyOfRange(array, arrayOffset + byteBuffer.position(), arrayOffset + byteBuffer.limit());
}
private static Map<Integer, ByteBuffer> getAll(File apkFile) {
Map<Integer, ByteBuffer> idValues = null;
try {
RandomAccessFile randomAccessFile = null;
FileChannel fileChannel = null;
try {
randomAccessFile = new RandomAccessFile(apkFile, "r");
fileChannel = randomAccessFile.getChannel();
ByteBuffer apkSigningBlock2 = (ByteBuffer) ApkUtil.findApkSigningBlock(fileChannel).getFirst();
idValues = ApkUtil.findIdValues(apkSigningBlock2);
} catch (IOException var18) {
Log.d("异常", Objects.requireNonNull(var18.getMessage()));
} finally {
try {
if (fileChannel != null) {
fileChannel.close();
}
} catch (IOException var17) {
Log.d("异常", Objects.requireNonNull(var17.getMessage()));
}
try {
if (randomAccessFile != null) {
randomAccessFile.close();
}
} catch (IOException var16) {
Log.d("异常", Objects.requireNonNull(var16.getMessage()));
}
}
} catch (SignatureNotFoundException var20) {
Log.d("异常", Objects.requireNonNull(var20.getMessage()));
}
return idValues;
}
}

View File

@ -0,0 +1,9 @@
package com.img.rabbit.utils.appwalle;
public class SignatureNotFoundException extends Exception {
private static final long serialVersionUID = 1L;
public SignatureNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,71 @@
package com.img.rabbit.viewmodel
import androidx.compose.runtime.mutableStateOf
import com.google.gson.JsonObject
import com.img.rabbit.bean.local.ErrorBean
import com.img.rabbit.bean.response.LoginInfoEntity
import com.img.rabbit.provider.api.ApiManager
import com.img.rabbit.provider.api.ResultVo
import com.img.rabbit.provider.storage.PreferenceUtil
import okhttp3.RequestBody.Companion.toRequestBody
class AccountBindViewModel : BaseViewModel() {
// 登录状态
val unBindState = mutableStateOf<ResultVo<LoginInfoEntity>?>(null)
// 错误状态
val errorState = mutableStateOf<ErrorBean?>(null)
/**
* 请求登录(验证码)
*/
fun requestUnBindByPhone(phone: String) {
isLoading.value = true // 开始加载
// 调用 API 获取数据
val jsonPhone = JsonObject()
jsonPhone.addProperty("phone", phone)
val jsonObject = JsonObject()
jsonObject.addProperty("type", "phone")
jsonObject.addProperty("bind", "2")
jsonObject.add("data", jsonPhone)
unBindAccount(jsonObject)
}
/**
* 拿着微信授权码完成登录(在WXEntryActivity中调用)
* @param wechatCode 微信授权码
*/
fun requestUnBindByWechat(wechatCode: String) {
if(wechatCode.isEmpty()){
return
}
isLoading.value = true // 开始加载
PreferenceUtil.saveWxCode(wechatCode)
// 调用 API 获取数据
val jsonWx = JsonObject()
jsonWx.addProperty("code", wechatCode)
jsonWx.addProperty("code_type", "")
val jsonObject = JsonObject()
jsonObject.addProperty("type", "weixin")
jsonObject.addProperty("bind", "2")
jsonObject.add("data", jsonWx)
unBindAccount(jsonObject)
}
fun unBindAccount(jsonObject: JsonObject){
mLaunch {
val response = ApiManager.serviceVo.login(jsonObject.toString().toRequestBody())
if (response.status) {
unBindState.value = response
}else{
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "解绑失败" })
}
isLoading.value = false // 加载完成
}
}
}

View File

@ -0,0 +1,24 @@
package com.img.rabbit.viewmodel
import androidx.compose.runtime.mutableStateOf
import com.img.rabbit.bean.local.ErrorBean
import com.img.rabbit.provider.api.ApiManager
import okhttp3.RequestBody
class AccountManagerViewModel : BaseViewModel() {
// 错误状态
val errorState = mutableStateOf<ErrorBean?>(null)
fun requestAccount() {
isLoading.value = true // 开始加载
mLaunch {
val response = ApiManager.serviceVo.account()
if (response.status) {
// TODO 处理获取账号列表
}else{
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "提交失败..." })
}
isLoading.value = false // 开始加载
}
}
}

View File

@ -0,0 +1,269 @@
package com.img.rabbit.viewmodel
import android.app.Activity
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import com.g.gysdk.GYManager
import com.g.gysdk.GYResponse
import com.g.gysdk.GyCallBack
import com.g.gysdk.GyConfig
import com.google.gson.JsonObject
import com.img.rabbit.bean.local.ErrorBean
import com.img.rabbit.bean.local.OnekeyPreLogin
import com.img.rabbit.bean.response.LoginInfoEntity
import com.img.rabbit.pages.screen.mine.setting.BindScreenType
import com.img.rabbit.provider.api.ApiManager
import com.img.rabbit.provider.api.ResultVo
import com.img.rabbit.provider.storage.PreferenceUtil
import com.tencent.mm.opensdk.modelmsg.SendAuth
import com.tencent.mm.opensdk.openapi.IWXAPI
import kotlinx.serialization.json.Json
import okhttp3.RequestBody.Companion.toRequestBody
class BindViewModel : BaseViewModel() {
private val TAG = "BindViewModel"
private val ONEKEY_TAG = "BindViewModel_OneKey"
val bindScreenType = mutableStateOf(BindScreenType.BIND_NORMAL)
// 登录状态
val bindState = mutableStateOf<ResultVo<LoginInfoEntity>?>(null)
// 错误状态
val errorState = mutableStateOf<ErrorBean?>(null)
// 是否同意政策协议
private val _policyAgreement = mutableStateOf(true)
val isPolicyAgreement: State<Boolean> = _policyAgreement
fun setIsPolicyAgreement(isAgreement: Boolean) {
_policyAgreement.value = isAgreement
}
// 一键登录是否有效
private val isGYUIDValid = mutableStateOf(false)
var oneKeyPreLogin: OnekeyPreLogin? = null
// 登录用户名
val userName = mutableStateOf("")
fun setUserName(loginName: String) {
userName.value = loginName
}
// 登录验证码
val captcha = mutableStateOf("")
// 登录验证码发送时间戳
val captchaTimestamp = mutableStateOf("")
fun setCaptcha(loginCaptcha: String) {
captcha.value = loginCaptcha
}
fun oneKeyLoginForGeTuiSdk(activity: Activity, onShowOneKeyScreen:(Boolean)->Unit) {
// 初始化 SDK
GYManager.getInstance().init(GyConfig.with(activity.applicationContext).callBack(object : GyCallBack {
override fun onSuccess(response: GYResponse?) {
isGYUIDValid.value = true
Log.i(ONEKEY_TAG, "--->初始化: onSuccess")
//预登录(提高拉起速度,可选但推荐)
GYManager.getInstance().ePreLogin(/* timeout = */ 8000, /* gyCallBack = */ object : GyCallBack {
override fun onSuccess(response: GYResponse?) {
Log.i(ONEKEY_TAG, "--->预登录: onSuccess--->${response}")
val preLoginData = response?.msg
if (!preLoginData.isNullOrEmpty()) {
try {
val json = Json { ignoreUnknownKeys = true }
oneKeyPreLogin =
json.decodeFromString<OnekeyPreLogin>(preLoginData)
// 打印解析后的详细数据
Log.i(ONEKEY_TAG, "=== 预登录数据解析结果 ===")
Log.i(ONEKEY_TAG, "流程ID: ${oneKeyPreLogin?.process_id}")
Log.i(ONEKEY_TAG, "运营商类型: ${oneKeyPreLogin?.operatorType}")
Log.i(ONEKEY_TAG, "客户端类型: ${oneKeyPreLogin?.clienttype}")
Log.i(ONEKEY_TAG, "访问码: ${oneKeyPreLogin?.accessCode}")
Log.i(ONEKEY_TAG, "手机号: ${oneKeyPreLogin?.number}")
Log.i(ONEKEY_TAG, "过期时间: ${oneKeyPreLogin?.expiredTime}")
Log.i(ONEKEY_TAG, "错误码: ${oneKeyPreLogin?.errorCode}")
Log.i(ONEKEY_TAG, "错误描述: ${oneKeyPreLogin?.errorDesc}")
Log.i(ONEKEY_TAG, "耗时: ${oneKeyPreLogin?.costTime}ms")
Log.i(ONEKEY_TAG, "================================")
// 根据解析结果决定是否继续根据errorCode判断
if (oneKeyPreLogin?.errorCode == 0) {
oneKeyLoginValid(
onShowOneKeyScreen = onShowOneKeyScreen
)
} else {
onShowOneKeyScreen(false)
Log.e(ONEKEY_TAG, "预登录校验失败: ${oneKeyPreLogin?.errorDesc}")
}
} catch (e: Exception) {
Log.e(ONEKEY_TAG, "JSON解析失败: ${e.message}")
Log.e(ONEKEY_TAG, "原始数据: $preLoginData")
// 解析失败时继续执行原逻辑
onShowOneKeyScreen(false)
}
}
}
override fun onFailed(response: GYResponse?) {
onShowOneKeyScreen(false)
Log.i(ONEKEY_TAG, "--->预登录: onFailed--->${response}")
}
})
}
override fun onFailed(response: GYResponse?) {
onShowOneKeyScreen(false)
Log.i(ONEKEY_TAG, "--->初始化: onFailed--->${response}")
}
}).build())
}
private fun oneKeyLoginValid(onShowOneKeyScreen:(Boolean)->Unit) {
if (GYManager.getInstance().isPreLoginResultValid) {
//预登录有效,启动登录授权页
onShowOneKeyScreen(true)
} else {
//考虑到是用户在等待建议超时8s以上至少设置5s以上
GYManager.getInstance().ePreLogin(5000, object : GyCallBack {
override fun onSuccess(response: GYResponse?) {
//预登录成功,启动登录授权页
onShowOneKeyScreen(true)
}
override fun onFailed(response: GYResponse?) {
//预登录失败,提示用户稍后重试
onShowOneKeyScreen(false)
}
})
}
}
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("type", "onekey")
jsonObject.addProperty("bind", "1")
jsonObject.add("data", jsonOneKey)
requestBind(jsonObject)
}
/**
* 请求验证码
*/
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(TAG, "请求验证码成功: ${response.data.timestamp}")
captchaTimestamp.value = response.data.timestamp
}else{
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "获取验证码失败" })
}
isLoading.value = false // 加载完成
}
}
/**
* 请求登录(验证码)
*/
fun requestBindForCaptcha(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("type", "phone")
jsonObject.addProperty("bind", "1")
jsonObject.add("data", jsonPhone)
requestBind(jsonObject)
}
/**
* 请求微信授权
*/
fun requestWithWechatAuth(context: Context, wxApi: IWXAPI) {
if (isPolicyAgreement.value) {
doWechatAuth(context, wxApi)
}else{
Toast.makeText(context, "请先同意用户协议和隐私政策", Toast.LENGTH_SHORT).show()
}
}
//获取微信授权
private fun doWechatAuth(context: Context, wxApi: IWXAPI) {
if (!wxApi.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"
wxApi.sendReq(req)
}
/**
* 拿着微信授权码完成登录(在WXEntryActivity中调用)
* @param wechatCode 微信授权码
*/
fun requestWxBind(wechatCode: String) {
if(wechatCode.isEmpty()){
return
}
isLoading.value = true // 开始加载
PreferenceUtil.saveWxCode(wechatCode)
// 调用 API 获取数据
val jsonWx = JsonObject()
jsonWx.addProperty("code", wechatCode)
jsonWx.addProperty("code_type", "")
val jsonObject = JsonObject()
jsonObject.addProperty("type", "weixin")
jsonObject.addProperty("bind", "1")
jsonObject.add("data", jsonWx)
requestBind(jsonObject)
}
//请求绑定
private fun requestBind(jsonObject: JsonObject){
mLaunch {
val response = ApiManager.serviceVo.login(jsonObject.toString().toRequestBody())
if (response.status) {
bindState.value = response
}else{
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "登录失败" })
}
isLoading.value = false // 加载完成
}
}
enum class BindType(val type: Int,val desc: String){
FROM_PHONE(0,"手机号"),
FROM_WX(1,"微信")
}
}

View File

@ -1,15 +1,20 @@
package com.img.rabbit.viewmodel
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.img.rabbit.bean.local.ErrorBean
import com.img.rabbit.bean.local.FileManagerBean
import com.img.rabbit.provider.api.ApiManager
import com.img.rabbit.utils.ImageUtils
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.io.IOException
class FeedbackViewModel : ViewModel() {
val isLoading = mutableStateOf(true)
fun setLoading(loading: Boolean) {
isLoading.value = loading
}
class FeedbackViewModel : BaseViewModel() {
// 错误状态
val errorState = mutableStateOf<ErrorBean?>(null)
// 反馈类型
private val _feedbackType = mutableStateOf(FeedbackType.FUNCTION)
@ -38,6 +43,15 @@ class FeedbackViewModel : ViewModel() {
_currentImageUris.value = currentImageUris
}
// 上传文件
private val _uploadFiles = mutableStateOf(emptyList<FileManagerBean>())
val uploadFiles: List<FileManagerBean>
get() = _uploadFiles.value
fun setUploadFiles(uploadFiles: List<FileManagerBean>) {
_uploadFiles.value = uploadFiles
}
// 联系方式
private val _feedbackContact = mutableStateOf("")
val feedbackContact: String
@ -47,9 +61,66 @@ class FeedbackViewModel : ViewModel() {
_feedbackContact.value = feedbackContact
}
// 提交反馈
fun submitFeedback() {
setLoading(true)
fun uploadImage(context: Context, uri: Uri) {
isLoading.value = true // 开始加载
mLaunch {
try {
val compressedUri = ImageUtils.imageCompressToUri(context, uri) ?: throw IOException("无法压缩图片")
val requestFile = RequestBody.create("multipart/form-data".toMediaTypeOrNull(), compressedUri)
val filePart = MultipartBody.Part.createFormData("file", uri.lastPathSegment, requestFile)
val response = ApiManager.serviceVo.upload(filePart, "feedback")
if (response.status) {
val updateFile = FileManagerBean(response.data, uri)
setUploadFiles(uploadFiles + updateFile)
}else{
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "上传失败" })
}
isLoading.value = false // 开始加载
}catch (e: IOException){
e.printStackTrace()
isLoading.value = false // 开始加载
}
}
}
fun batchDeleteImage(uris: List<Uri>) {
isLoading.value = true // 开始加载
uris.forEach {
deleteImage(it)
}
}
fun deleteImage(uri: Uri) {
isLoading.value = true // 开始加载
val fileId = uploadFiles.firstOrNull { it.uri == uri }?.fileEntity?.id
if(fileId.isNullOrEmpty()){
isLoading.value = false // 开始加载
return
}
mLaunch {
val response = ApiManager.serviceVo.deleteFile(fileId)
if (response.status) {
setUploadFiles(uploadFiles.filter { it.uri != uri })
}else{
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "删除失败" })
}
isLoading.value = false // 开始加载
}
}
fun submitFeedback(requestBody: RequestBody) {
isLoading.value = true // 开始加载
mLaunch {
val response = ApiManager.serviceVo.feedback(requestBody)
if (response.status) {
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "谢谢您的反馈!" })
}else{
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "提交失败..." })
}
isLoading.value = false // 开始加载
}
}
enum class FeedbackType(val keyName: String, val value: String) {

View File

@ -8,25 +8,33 @@ import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.img.rabbit.config.Constants
import com.img.rabbit.provider.api.ApiManager
import com.img.rabbit.provider.storage.PreferenceUtil
import com.tencent.mmkv.MMKV
import com.img.rabbit.utils.MMKVUtils.mmkv
import com.tencent.mm.opensdk.openapi.IWXAPI
import com.tencent.mm.opensdk.openapi.WXAPIFactory
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@SuppressLint("ObsoleteSdkInt")
class GeneralViewModel(application: Application) : AndroidViewModel(application) {
val kv by lazy { MMKV.defaultMMKV() }
lateinit var api: IWXAPI
private val _networkStatus = MutableLiveData<Boolean>()
val networkStatus: LiveData<Boolean> = _networkStatus
fun setNetworkStatus(status: Boolean) {
_networkStatus.value = status
}
private val _serverTime = mutableStateOf<Long?>(null)
val serverTime: State<Long?> = _serverTime
private val _isNavigationBarVisible = MutableLiveData<Boolean>()
val isNavigationBarVisible: LiveData<Boolean> = _isNavigationBarVisible
@ -46,7 +54,7 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application)
private val _agreementStatus = MutableLiveData<Boolean>()
val agreementStatus: LiveData<Boolean> = _agreementStatus
private fun getIsAgreement(): Boolean{
return kv.getBoolean("isAgreement", false)
return mmkv.getBoolean("isAgreement", false)
}
init {
@ -64,6 +72,9 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application)
_networkStatus.value = isNetworkAvailable()
// 初始化隐私政策状态
_agreementStatus.value = getIsAgreement()
// 初始化微信API
initWXApi(application)
}
private fun isNetworkAvailable(): Boolean {
@ -83,10 +94,14 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application)
}
fun setIsAgreement(agreement: Boolean){
kv.putBoolean("isAgreement", agreement)
mmkv.putBoolean("isAgreement", agreement)
_agreementStatus.value = agreement
}
private fun initWXApi(context: Context) {
api = WXAPIFactory.createWXAPI(context, Constants.WechatAppId)
}
override fun onCleared() {
super.onCleared()
connectivityManager.unregisterNetworkCallback(networkCallback)
@ -97,6 +112,7 @@ class GeneralViewModel(application: Application) : AndroidViewModel(application)
GlobalScope.launch {
val response = ApiManager.serviceVo.getServerTime()
if (response.status) {
_serverTime.value = response.data
PreferenceUtil.setTimeDiff(response.data - System.currentTimeMillis() / 1000)
}
}

View File

@ -8,8 +8,7 @@ import android.widget.Toast
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.State
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.compose.runtime.mutableIntStateOf
import com.alipay.sdk.app.AuthTask
import com.img.rabbit.pages.LoginScreenType
import com.g.gysdk.GYManager
@ -17,22 +16,23 @@ import com.g.gysdk.GYResponse
import com.g.gysdk.GyCallBack
import com.g.gysdk.GyConfig
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.bean.response.LoginInfoEntity
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.GlobalStateManager
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.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.RequestBody.Companion.toRequestBody
@ -40,18 +40,10 @@ import okhttp3.internal.platform.PlatformRegistry.applicationContext
class LoginViewModel : BaseViewModel() {
private val TAG = "LoginViewModel"
private val ONEKEY_TAG = "OneKeyLoginViewModel"
//private lateinit var api: IWXAPI
private lateinit var api: IWXAPI
private val _wxState = MutableLiveData<WxBean?>()
val wxState: LiveData<WxBean?> = _wxState
fun updateWxState(newState: WxBean) {
_wxState.value = newState
}
// private val _authInfo = MutableLiveData<String>()
val authInfoForAlipay: MutableState<String> = mutableStateOf("")
val loginScreenType = mutableStateOf(LoginScreenType.LOGIN_NORMAL)
@ -61,34 +53,29 @@ class LoginViewModel : BaseViewModel() {
val captcha = mutableStateOf("")
// 登录验证码发送时间戳
val captchaTimestamp = mutableStateOf("")
// 是否同意政策协议
private val _policyAgreement = mutableStateOf(true)
val isPolicyAgreement: State<Boolean> = _policyAgreement
fun setCaptcha(loginCaptcha: String) {
captcha.value = loginCaptcha
}
fun setUserName(loginName: String) {
userName.value = loginName
}
private val isGYUIDValid = mutableStateOf(false)
var oneKeyPreLogin: OnekeyPreLogin? = null
fun setUserName(loginName: String) {
userName.value = loginName
}
fun setCaptcha(loginCaptcha: String) {
captcha.value = loginCaptcha
}
// 是否同意政策协议
private val _policyAgreement = mutableStateOf(true)
val isPolicyAgreement: State<Boolean> = _policyAgreement
fun setIsPolicyAgreement(isAgreement: Boolean) {
_policyAgreement.value = isAgreement
}
//用户配置
val _userConfig = MutableLiveData<UserConfigEntity?>()
val userConfig: UserConfigEntity? get() = _userConfig.value
private val _isLogin = mutableStateOf(false)
val isLogin: State<Boolean> = _isLogin
@ -98,7 +85,9 @@ class LoginViewModel : BaseViewModel() {
val loginState = mutableStateOf<ResultVo<UserEntity>?>(null)
// 登录状态
val loginState = mutableStateOf<ResultVo<LoginInfoEntity>?>(null)
// 错误状态
val errorState = mutableStateOf<ErrorBean?>(null)
fun requestUserConfig(){
@ -108,12 +97,7 @@ class LoginViewModel : BaseViewModel() {
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")
PreferenceUtil.saveUserConfig(response.data)
}else{
Log.w("LoginViewModel", "获取配置失败: code=${response.code}, message=${response.message}")
}
@ -155,7 +139,6 @@ class LoginViewModel : BaseViewModel() {
// 根据解析结果决定是否继续根据errorCode判断
if (oneKeyPreLogin?.errorCode == 0) {
oneKeyLoginValid(
activity = activity,
onShowOneKeyScreen = onShowOneKeyScreen
)
} else {
@ -186,7 +169,7 @@ class LoginViewModel : BaseViewModel() {
}).build())
}
private fun oneKeyLoginValid(activity: Activity, onShowOneKeyScreen:(Boolean)->Unit) {
private fun oneKeyLoginValid(onShowOneKeyScreen:(Boolean)->Unit) {
if (GYManager.getInstance().isPreLoginResultValid) {
//预登录有效,启动登录授权页
onShowOneKeyScreen(true)
@ -214,12 +197,6 @@ class LoginViewModel : BaseViewModel() {
// 调用 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)
@ -273,19 +250,26 @@ class LoginViewModel : BaseViewModel() {
requestLogin(jsonObject)
}
fun initWXApi(context: Context) {
api = WXAPIFactory.createWXAPI(context, Constants.WechatAppId)
}
fun loginWithWechat(context: Context) {
fun loginWithWechat(context: Context, wxApi:IWXAPI) {
if (isPolicyAgreement.value) {
doWxAuth(context)
doWxAuth(context, wxApi)
}else{
Toast.makeText(context, "请先同意用户协议和隐私政策", Toast.LENGTH_SHORT).show()
}
}
//获取微信授权
private fun doWxAuth(context: Context, wxApi:IWXAPI) {
if (!wxApi.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"
wxApi.sendReq(req)
}
/**
* 拿着微信授权码完成登录(在WXEntryActivity中调用)
* @param wechatCode 微信授权码
@ -295,6 +279,7 @@ class LoginViewModel : BaseViewModel() {
return
}
isLoading.value = true // 开始加载
PreferenceUtil.saveWxCode(wechatCode)
// 调用 API 获取数据
val jsonWx = JsonObject()
@ -308,18 +293,6 @@ class LoginViewModel : BaseViewModel() {
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)
}
/**
* 支付宝登录
@ -393,19 +366,60 @@ class LoginViewModel : BaseViewModel() {
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
val loginInfoEntity = response.data
loginInfoEntity.isLogin = true
//记录登录数据
PreferenceUtil.saveUserInfo(userEntity)
PreferenceUtil.saveLoginInfo(loginInfoEntity)
}else{
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "登录失败" })
}
isLoading.value = false // 加载完成
}
}
fun requestUserInfo(){
mLaunch {
val response = ApiManager.serviceVo.userInfo()
if(response.status){
PreferenceUtil.saveUserInfo(response.data)
}
}
}
/**
* 请求退出登录
*/
@OptIn(DelicateCoroutinesApi::class)
fun requestLogout(context: Context) {
isLoading.value = true // 开始加载
mLaunch {
val response = ApiManager.serviceVo.logout()
if (response.status) {
//logoutState.value = response
PreferenceUtil.clearLogin()
setLogin(false)
// 跳转登录页面
//restViewModel.intValue = 1
GlobalScope.launch {
GlobalStateManager(context).storeGlobalLogout(true)
}
} else {
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "退出登录失败" })
}
isLoading.value = false // 加载完成
}
}
enum class JumpLoginType(val type: Int,val desc: String){
NORMAL(0,"正常登录"),
FROM_ADD(1,"添加账号"),
FROM_LOGOUT(2,"退出登录")
}
}

View File

@ -2,13 +2,19 @@ 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.UploadFileEntity
import com.img.rabbit.bean.response.LoginInfoEntity
import com.img.rabbit.bean.response.UserConfigEntity
import com.img.rabbit.bean.response.UserInfoEntity
import com.img.rabbit.provider.api.ResultVo
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Query
interface ServiceVo {
@ -52,12 +58,49 @@ interface ServiceVo {
* 登录
*/
@POST("/api/user/login")
suspend fun login(@Body requestBody: RequestBody): ResultVo<UserEntity>
suspend fun login(@Body requestBody: RequestBody): ResultVo<LoginInfoEntity>
/**
* 退出登录
*/
@POST("/mapi/user/logout")
@POST("/api/user/logout")
suspend fun logout(): ResultVo<Any>
/**
* 注销
*/
@POST("/api/user/destroy")
suspend fun userDestroy(): ResultVo<Any>
/**
* 获取用户信息
*/
@GET("/api/user")
suspend fun userInfo(): ResultVo<UserInfoEntity>
/**
* 获取账号列表
*/
@POST("/api/user/account")
suspend fun account(): ResultVo<Any>//@Query("scene") scene: String="account"
/**
* 上传文件
*/
@Multipart
@POST("/api/user/upload")
suspend fun upload(@Part part: MultipartBody.Part, @Query("scene") scene: String): ResultVo<UploadFileEntity>
/**
*删除上传文件
*/
@DELETE("/api/user/upload")
suspend fun deleteFile(@Query("id") id: String): ResultVo<Any>
/**
* 意见反馈
*/
@POST("/api/user/feedback")
suspend fun feedback(@Body requestBody: RequestBody): ResultVo<Any>
}

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF000000"
android:pathData="M512,42.7a469.3,469.3 0,1 0,469.3 469.3A469.3,469.3 0,0 0,512 42.7zM512,906.7a394.7,394.7 0,1 1,394.7 -394.7,395.1 395.1,0 0,1 -394.7,394.7z"/>
<path
android:fillColor="#FF000000"
android:pathData="M670.4,300.8l-154.7,154.7a5.3,5.3 0,0 1,-7.6 0l-154.7,-154.7a5.3,5.3 0,0 0,-7.5 0l-45.2,45.3a5.3,5.3 0,0 0,0 7.5l154.7,154.7a5.3,5.3 0,0 1,0 7.6l-154.7,154.7a5.3,5.3 0,0 0,0 7.5l45.3,45.3a5.3,5.3 0,0 0,7.5 0l154.7,-154.7a5.3,5.3 0,0 1,7.6 0l154.7,154.7a5.3,5.3 0,0 0,7.5 0l45.3,-45.3a5.3,5.3 0,0 0,0 -7.5l-154.7,-154.7a5.3,5.3 0,0 1,0 -7.6l154.7,-154.7a5.3,5.3 0,0 0,0 -7.5l-45.3,-45.3a5.3,5.3 0,0 0,-7.6 0z"/>
</vector>

View File

@ -25,10 +25,26 @@
<external-media-path
name="external_media_path"
path="." />
<!--配置root-path。这样子可以读取到sd卡和一些应用分身的目录否则微信分身保存的图片就会导致 java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/999/tencent/MicroMsg/WeiXin/export1544062754693.jpg在小米6的手机上微信分身有这个crash华为没有
-->
<!--配置root-path。这样子可以读取到sd卡和一些应用分身的目录否则微信分身保存的图片就会导致 java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/999/tencent/MicroMsg/WeiXin/export1544062754693.jpg在小米6的手机上微信分身有这个crash华为没有-->
<root-path
name="root-path"
path="" />
<!-- 添加根目录访问权限 -->
<external-cache-path
name="external_cache"
path="." />
<!-- 兼容旧版本 -->
<cache-path
name="internal_cache"
path="." />
<external-files-path
name="external_files"
path="." />
<cache-path
name="cache"
path="." />
</paths>