test
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
package com.aiosman.ravenow.ui.account
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.aiosman.ravenow.data.AccountService
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.UploadImage
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
|
||||
import com.aiosman.ravenow.utils.TrtcHelper
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
|
||||
object AccountEditViewModel : ViewModel() {
|
||||
var name by mutableStateOf("")
|
||||
var bio by mutableStateOf("")
|
||||
var imageUrl by mutableStateOf<Uri?>(null)
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
||||
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
||||
var isUpdating by mutableStateOf(false)
|
||||
var isLoading by mutableStateOf(false)
|
||||
suspend fun reloadProfile(updateTrtcProfile:Boolean = false) {
|
||||
Log.d("AccountEditViewModel", "reloadProfile: 开始加载用户资料")
|
||||
isLoading = true
|
||||
try {
|
||||
Log.d("AccountEditViewModel", "reloadProfile: 调用API获取用户资料")
|
||||
accountService.getMyAccountProfile().let {
|
||||
Log.d("AccountEditViewModel", "reloadProfile: 成功获取用户资料 - nickName: ${it.nickName}")
|
||||
profile = it
|
||||
name = it.nickName
|
||||
bio = it.bio
|
||||
// 清除之前裁剪的图片
|
||||
croppedBitmap = null
|
||||
if (updateTrtcProfile) {
|
||||
TrtcHelper.updateTrtcProfile(
|
||||
it.nickName,
|
||||
it.rawAvatar
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 处理异常,避免UI消失
|
||||
Log.e("AccountEditViewModel", "reloadProfile: 加载用户资料失败", e)
|
||||
e.printStackTrace()
|
||||
// 如果是首次加载失败,至少保持之前的profile不变
|
||||
// 这样UI不会突然消失
|
||||
} finally {
|
||||
Log.d("AccountEditViewModel", "reloadProfile: 加载完成,isLoading设为false")
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
fun resetToOriginalData() {
|
||||
profile?.let {
|
||||
name = it.nickName
|
||||
bio = it.bio
|
||||
// 清除之前裁剪的图片
|
||||
croppedBitmap = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun updateUserProfile(context: Context) {
|
||||
val newAvatar = croppedBitmap?.let {
|
||||
val file = File(context.cacheDir, "avatar.jpg")
|
||||
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
||||
UploadImage(file, "avatar.jpg", "", "jpg")
|
||||
}
|
||||
// 去除换行符,确保昵称和个人简介不包含换行
|
||||
val cleanName = name.trim().replace("\n", "").replace("\r", "")
|
||||
val cleanBio = bio.trim().replace("\n", "").replace("\r", "")
|
||||
|
||||
val newName = if (cleanName == profile?.nickName) null else cleanName
|
||||
accountService.updateProfile(
|
||||
avatar = newAvatar,
|
||||
banner = null,
|
||||
nickName = newName,
|
||||
bio = cleanBio
|
||||
)
|
||||
// 刷新用户资料
|
||||
reloadProfile()
|
||||
// 刷新个人资料页面的用户资料
|
||||
MyProfileViewModel.loadUserProfile()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置ViewModel状态
|
||||
* 用于用户登出或切换账号时清理数据
|
||||
*/
|
||||
fun ResetModel() {
|
||||
Log.d("AccountEditViewModel", "ResetModel: 重置ViewModel状态")
|
||||
profile = null
|
||||
name = ""
|
||||
bio = ""
|
||||
imageUrl = null
|
||||
croppedBitmap = null
|
||||
isUpdating = false
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.aiosman.ravenow.ui.account
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.Messaging
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.index.NavItem
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountSetting() {
|
||||
val appColors = LocalAppTheme.current
|
||||
val navController = LocalNavController.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(appColors.background),
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
) {
|
||||
NoticeScreenHeader(
|
||||
title = stringResource(R.string.account_and_security),
|
||||
moreIcon = false
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 24.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
NavItem(
|
||||
iconRes = R.mipmap.rider_pro_change_password,
|
||||
label = stringResource(R.string.change_password),
|
||||
modifier = Modifier.noRippleClickable {
|
||||
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 分割线
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(appColors.divider)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
NavItem(
|
||||
iconRes = R.drawable.rider_pro_moment_delete,
|
||||
label = stringResource(R.string.remove_account),
|
||||
modifier = Modifier.noRippleClickable {
|
||||
navController.navigate(NavigationRoute.RemoveAccountScreen.route)
|
||||
}
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(appColors.divider)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.aiosman.ravenow.ui.account
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.ConstVars
|
||||
import com.aiosman.ravenow.data.api.ErrorCode
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.data.AccountService
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.DictService
|
||||
import com.aiosman.ravenow.data.DictServiceImpl
|
||||
import com.aiosman.ravenow.data.ServiceException
|
||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.composables.TextInputField
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ResetPasswordScreen() {
|
||||
var username by remember { mutableStateOf("") }
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
val dictService: DictService = DictServiceImpl()
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
var isSendSuccess by remember { mutableStateOf<Boolean?>(null) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
val navController = LocalNavController.current
|
||||
var usernameError by remember { mutableStateOf<String?>(null) }
|
||||
var countDown by remember { mutableStateOf<Int?>(null) }
|
||||
var countDownMax by remember { mutableStateOf(60) }
|
||||
val appColors = LocalAppTheme.current
|
||||
fun validate(): Boolean {
|
||||
if (username.isEmpty()) {
|
||||
usernameError = context.getString(R.string.text_error_email_required)
|
||||
return false
|
||||
}
|
||||
usernameError = null
|
||||
return true
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
dictService.getDictByKey(ConstVars.DIC_KEY_RESET_EMAIL_INTERVAL).let {
|
||||
countDownMax = it.value as? Int ?: 60
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
countDownMax = 60
|
||||
}
|
||||
}
|
||||
|
||||
fun startCountDown() {
|
||||
scope.launch {
|
||||
countDown = countDownMax
|
||||
while (countDown!! > 0) {
|
||||
delay(1000)
|
||||
countDown = countDown!! - 1
|
||||
}
|
||||
countDown = null
|
||||
}
|
||||
}
|
||||
|
||||
fun resetPassword() {
|
||||
if (!validate()) return
|
||||
scope.launch {
|
||||
isLoading = true
|
||||
try {
|
||||
accountService.resetPassword(username)
|
||||
isSendSuccess = true
|
||||
startCountDown()
|
||||
} catch (e: ServiceException) {
|
||||
if (e.code == ErrorCode.USER_NOT_EXIST.code){
|
||||
usernameError = context.getString(R.string.error_40002_user_not_exist)
|
||||
} else {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
isSendSuccess = false
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().background(color = appColors.background)
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Box(
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 16.dp,
|
||||
bottom = 0.dp
|
||||
)
|
||||
) {
|
||||
NoticeScreenHeader(
|
||||
stringResource(R.string.recover_account_upper),
|
||||
moreIcon = false
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(72.dp))
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
TextInputField(
|
||||
text = username,
|
||||
onValueChange = { username = it },
|
||||
hint = stringResource(R.string.text_hint_email),
|
||||
enabled = !isLoading && countDown == null,
|
||||
error = usernameError,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Box(
|
||||
modifier = Modifier.height(72.dp)
|
||||
) {
|
||||
isSendSuccess?.let {
|
||||
if (it) {
|
||||
Text(
|
||||
text = stringResource(R.string.reset_mail_send_success),
|
||||
style = TextStyle(
|
||||
color = appColors.text,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.reset_mail_send_failed),
|
||||
style = TextStyle(
|
||||
color = appColors.text,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ActionButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
text = if (countDown != null) {
|
||||
stringResource(R.string.resend, "(${countDown})")
|
||||
} else {
|
||||
stringResource(R.string.recover)
|
||||
},
|
||||
backgroundColor = appColors.main,
|
||||
color = appColors.mainText,
|
||||
isLoading = isLoading,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
enabled = countDown == null,
|
||||
) {
|
||||
resetPassword()
|
||||
}
|
||||
isSendSuccess?.let {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ActionButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp),
|
||||
text = stringResource(R.string.back_upper),
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.data.AccountService
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.ServiceException
|
||||
import com.aiosman.ravenow.data.api.ErrorCode
|
||||
import com.aiosman.ravenow.data.api.showToast
|
||||
import com.aiosman.ravenow.data.api.toErrorMessage
|
||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.composables.TextInputField
|
||||
import com.aiosman.ravenow.utils.PasswordValidator
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 修改密码页面的 ViewModel
|
||||
*/
|
||||
class ChangePasswordViewModel {
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param currentPassword 当前密码
|
||||
* @param newPassword 新密码
|
||||
*/
|
||||
suspend fun changePassword(currentPassword: String, newPassword: String) {
|
||||
accountService.changeAccountPassword(currentPassword, newPassword)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码页面
|
||||
*/
|
||||
@Composable
|
||||
fun ChangePasswordScreen() {
|
||||
val context = LocalContext.current
|
||||
val viewModel = remember { ChangePasswordViewModel() }
|
||||
var currentPassword by remember { mutableStateOf("") }
|
||||
var newPassword by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val navController = LocalNavController.current
|
||||
var oldPasswordError by remember { mutableStateOf<String?>(null) }
|
||||
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
|
||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||
val AppColors = LocalAppTheme.current
|
||||
fun validate(): Boolean {
|
||||
// 使用通用密码校验器校验当前密码
|
||||
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
|
||||
oldPasswordError = if (!currentPasswordValidation.isValid) currentPasswordValidation.errorMessage else null
|
||||
|
||||
// 使用通用密码校验器校验新密码
|
||||
val newPasswordValidation = PasswordValidator.validatePassword(newPassword, context)
|
||||
passwordError = if (!newPasswordValidation.isValid) newPasswordValidation.errorMessage else null
|
||||
|
||||
// 使用通用密码确认校验器
|
||||
val confirmPasswordValidation = PasswordValidator.validatePasswordConfirmation(newPassword, confirmPassword, context)
|
||||
confirmPasswordError = if (!confirmPasswordValidation.isValid) confirmPasswordValidation.errorMessage else null
|
||||
|
||||
return passwordError == null && confirmPasswordError == null && oldPasswordError == null
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.background),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
) {
|
||||
NoticeScreenHeader(
|
||||
title = stringResource(R.string.change_password),
|
||||
moreIcon = false
|
||||
)
|
||||
|
||||
}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(80.dp))
|
||||
TextInputField(
|
||||
text = currentPassword,
|
||||
onValueChange = { currentPassword = it },
|
||||
password = true,
|
||||
label = stringResource(R.string.current_password),
|
||||
hint = stringResource(R.string.current_password_tip5),
|
||||
error = oldPasswordError
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
TextInputField(
|
||||
text = newPassword,
|
||||
onValueChange = { newPassword = it },
|
||||
password = true,
|
||||
label = stringResource(R.string.new_password),
|
||||
hint = stringResource(R.string.new_password),
|
||||
error = passwordError
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
TextInputField(
|
||||
text = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
password = true,
|
||||
label = stringResource(R.string.confirm_new_password_tip1),
|
||||
hint = stringResource(R.string.new_password_tip1),
|
||||
error = confirmPasswordError
|
||||
)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
ActionButton(
|
||||
modifier = Modifier
|
||||
.width(345.dp),
|
||||
text = stringResource(R.string.lets_ride_upper),
|
||||
) {
|
||||
if (validate()) {
|
||||
scope.launch {
|
||||
try {
|
||||
viewModel.changePassword(currentPassword, newPassword)
|
||||
|
||||
navController.navigateUp()
|
||||
} catch (e: ServiceException) {
|
||||
when (e.errorType) {
|
||||
ErrorCode.IncorrectOldPassword ->
|
||||
oldPasswordError = e.errorType.toErrorMessage(context)
|
||||
else ->
|
||||
e.errorType.showToast(context)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
242
app/src/main/java/com/aiosman/ravenow/ui/account/edit.kt
Normal file
242
app/src/main/java/com/aiosman/ravenow/ui/account/edit.kt
Normal file
@@ -0,0 +1,242 @@
|
||||
package com.aiosman.ravenow.ui.account
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TopAppBar
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.data.AccountService
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.UploadImage
|
||||
import com.aiosman.ravenow.entity.AccountProfileEntity
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.ravenow.ui.post.NewPostViewModel.uriToFile
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 编辑用户资料界面
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountEditScreen() {
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
var name by remember { mutableStateOf("") }
|
||||
var bio by remember { mutableStateOf("") }
|
||||
var imageUrl by remember { mutableStateOf<Uri?>(null) }
|
||||
var bannerImageUrl by remember { mutableStateOf<Uri?>(null) }
|
||||
var profile by remember {
|
||||
mutableStateOf<AccountProfileEntity?>(
|
||||
null
|
||||
)
|
||||
}
|
||||
val navController = LocalNavController.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
/**
|
||||
* 加载用户资料
|
||||
*/
|
||||
suspend fun reloadProfile() {
|
||||
accountService.getMyAccountProfile().let {
|
||||
profile = it
|
||||
name = it.nickName
|
||||
bio = it.bio
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUserProfile() {
|
||||
scope.launch {
|
||||
val newAvatar = imageUrl?.let {
|
||||
val cursor = context.contentResolver.query(it, null, null, null, null)
|
||||
var newAvatar: UploadImage? = null
|
||||
cursor?.use { cur ->
|
||||
if (cur.moveToFirst()) {
|
||||
val columnIndex = cur.getColumnIndex("_display_name")
|
||||
val displayName = if (columnIndex >= 0) cur.getString(columnIndex) else "unknown"
|
||||
val extension = displayName.substringAfterLast(".")
|
||||
Log.d("NewPost", "File name: $displayName, extension: $extension")
|
||||
// read as file
|
||||
val file = uriToFile(context, it)
|
||||
Log.d("NewPost", "File size: ${file.length()}")
|
||||
newAvatar = UploadImage(file, displayName, it.toString(), extension)
|
||||
}
|
||||
}
|
||||
newAvatar
|
||||
}
|
||||
var newBanner = bannerImageUrl?.let {
|
||||
val cursor = context.contentResolver.query(it, null, null, null, null)
|
||||
var newBanner: UploadImage? = null
|
||||
cursor?.use { cur ->
|
||||
if (cur.moveToFirst()) {
|
||||
val columnIndex = cur.getColumnIndex("_display_name")
|
||||
val displayName = if (columnIndex >= 0) cur.getString(columnIndex) else "unknown"
|
||||
val extension = displayName.substringAfterLast(".")
|
||||
Log.d("NewPost", "File name: $displayName, extension: $extension")
|
||||
// read as file
|
||||
val file = uriToFile(context, it)
|
||||
Log.d("NewPost", "File size: ${file.length()}")
|
||||
newBanner = UploadImage(file, displayName, it.toString(), extension)
|
||||
}
|
||||
}
|
||||
newBanner
|
||||
}
|
||||
val newName = if (name == profile?.nickName) null else name
|
||||
accountService.updateProfile(
|
||||
avatar = newAvatar,
|
||||
banner = newBanner,
|
||||
nickName = newName,
|
||||
bio = bio
|
||||
)
|
||||
reloadProfile()
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
val pickImageLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val uri = result.data?.data
|
||||
uri?.let {
|
||||
imageUrl = it
|
||||
}
|
||||
}
|
||||
}
|
||||
val pickBannerImageLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val uri = result.data?.data
|
||||
uri?.let {
|
||||
bannerImageUrl = uri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
reloadProfile()
|
||||
}
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Edit") },
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
updateUserProfile()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Save"
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
profile?.let {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
if (imageUrl != null) {
|
||||
imageUrl.toString()
|
||||
} else {
|
||||
it.avatar
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(100.dp)
|
||||
.noRippleClickable {
|
||||
Intent(Intent.ACTION_PICK).apply {
|
||||
type = "image/*"
|
||||
pickImageLauncher.launch(this)
|
||||
}
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
if (bannerImageUrl != null) {
|
||||
bannerImageUrl.toString()
|
||||
} else {
|
||||
it.banner!!
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
.noRippleClickable {
|
||||
Intent(Intent.ACTION_PICK).apply {
|
||||
type = "image/*"
|
||||
pickBannerImageLauncher.launch(this)
|
||||
}
|
||||
},
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
TextField(
|
||||
value = name,
|
||||
onValueChange = {
|
||||
name = it
|
||||
},
|
||||
label = {
|
||||
Text("Name")
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
TextField(
|
||||
value = bio,
|
||||
onValueChange = {
|
||||
bio = it
|
||||
},
|
||||
label = {
|
||||
Text("Bio")
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
350
app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt
Normal file
350
app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt
Normal file
@@ -0,0 +1,350 @@
|
||||
package com.aiosman.ravenow.ui.account
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Icon
|
||||
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.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.composables.form.FormTextInput
|
||||
import com.aiosman.ravenow.ui.composables.debouncedClickable
|
||||
import com.aiosman.ravenow.ui.composables.rememberDebouncedNavigation
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.ConstVars
|
||||
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 编辑用户资料界面
|
||||
*/
|
||||
@Composable
|
||||
fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,) {
|
||||
val model = AccountEditViewModel
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
var usernameError by remember { mutableStateOf<String?>(null) }
|
||||
var bioError by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// 防抖导航器
|
||||
val debouncedNavigation = rememberDebouncedNavigation()
|
||||
|
||||
// 添加图片选择启动器
|
||||
val scope = rememberCoroutineScope()
|
||||
val pickBannerImageLauncher = pickupAndCompressLauncher(
|
||||
context,
|
||||
scope,
|
||||
maxSize = ConstVars.BANNER_IMAGE_MAX_SIZE,
|
||||
quality = 100
|
||||
) { uri, file ->
|
||||
// 处理选中的图片
|
||||
onUpdateBanner?.invoke(uri, file, context)
|
||||
}
|
||||
|
||||
fun onNicknameChange(value: String) {
|
||||
// 去除换行符,确保昵称不包含换行
|
||||
val cleanValue = value.replace("\n", "").replace("\r", "")
|
||||
model.name = cleanValue
|
||||
usernameError = when {
|
||||
cleanValue.trim().isEmpty() -> "昵称不能为空"
|
||||
cleanValue.length < 3 -> "昵称长度不能小于3"
|
||||
cleanValue.length > 20 -> "昵称长度不能大于20"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val appColors = LocalAppTheme.current
|
||||
|
||||
fun onBioChange(value: String) {
|
||||
// 去除换行符,确保个人简介不包含换行
|
||||
val cleanValue = value.replace("\n", "").replace("\r", "")
|
||||
model.bio = cleanValue
|
||||
bioError = when {
|
||||
cleanValue.length > 100 -> "个人简介长度不能大于100"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun validate(): Boolean {
|
||||
return usernameError == null && bioError == null
|
||||
}
|
||||
|
||||
// 检查是否为游客模式
|
||||
if (AppStore.isGuest) {
|
||||
LaunchedEffect(Unit) {
|
||||
// 游客模式不允许编辑资料,返回上一页(防抖)
|
||||
debouncedNavigation {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
// 游客模式时不渲染任何内容
|
||||
return
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
// 每次进入编辑页面时都重新加载当前用户的资料
|
||||
// 确保显示的是当前登录用户的信息,而不是之前用户的缓存数据
|
||||
model.reloadProfile()
|
||||
}
|
||||
StatusBarMaskLayout(
|
||||
modifier = Modifier.background(color = appColors.background).padding(horizontal = 16.dp),
|
||||
darkIcons = !AppState.darkMode,
|
||||
maskBoxBackgroundColor = appColors.background
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = appColors.background),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
//StatusBarSpacer()
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 0.dp, vertical = 16.dp)
|
||||
) {
|
||||
NoticeScreenHeader(
|
||||
title = stringResource(R.string.edit_profile),
|
||||
moreIcon = false
|
||||
) {
|
||||
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.debouncedClickable(
|
||||
enabled = validate() && !model.isUpdating,
|
||||
debounceTime = 1000L
|
||||
) {
|
||||
if (validate() && !model.isUpdating) {
|
||||
model.viewModelScope.launch {
|
||||
model.isUpdating = true
|
||||
model.updateUserProfile(context)
|
||||
model.viewModelScope.launch(Dispatchers.Main) {
|
||||
debouncedNavigation {
|
||||
navController.navigateUp()
|
||||
}
|
||||
model.isUpdating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "保存",
|
||||
tint = if (validate() && !model.isUpdating) appColors.text else appColors.nonActiveText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 添加横幅图片区域
|
||||
val banner = model.profile?.banner
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
) {
|
||||
if (banner != null) {
|
||||
CustomAsyncImage(
|
||||
context = LocalContext.current,
|
||||
imageUrl = banner,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentDescription = "Banner",
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Gray.copy(alpha = 0.1f))
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(120.dp)
|
||||
.height(42.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 12.dp, bottom = 12.dp)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.4f),
|
||||
shape = RoundedCornerShape(9.dp)
|
||||
)
|
||||
.noRippleClickable {
|
||||
Intent(Intent.ACTION_PICK).apply {
|
||||
type = "image/*"
|
||||
pickBannerImageLauncher.launch(this)
|
||||
}
|
||||
}
|
||||
){
|
||||
Text(
|
||||
text = "change",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.W600,
|
||||
color = Color.White,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
// 显示内容或加载状态
|
||||
Log.d("AccountEditScreen2", "UI状态 - profile: ${model.profile?.nickName}, isLoading: ${model.isLoading}")
|
||||
when {
|
||||
model.profile != null -> {
|
||||
Log.d("AccountEditScreen2", "显示用户资料内容")
|
||||
// 有数据时显示内容
|
||||
val it = model.profile!!
|
||||
Box(
|
||||
modifier = Modifier.size(88.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
model.croppedBitmap ?: it.avatar,
|
||||
modifier = Modifier
|
||||
.size(88.dp)
|
||||
.clip(
|
||||
RoundedCornerShape(88.dp)
|
||||
),
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF7c45ed),
|
||||
Color(0x997c68ef),
|
||||
Color(0xFF7bd8f8)
|
||||
)
|
||||
),
|
||||
)
|
||||
.align(Alignment.BottomEnd)
|
||||
.debouncedClickable(
|
||||
debounceTime = 800L
|
||||
) {
|
||||
debouncedNavigation {
|
||||
navController.navigate(NavigationRoute.ImageCrop.route)
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = "Add",
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
FormTextInput(
|
||||
value = model.name,
|
||||
label = stringResource(R.string.nickname),
|
||||
hint = "Input nickname",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
error = usernameError
|
||||
) { value ->
|
||||
onNicknameChange(value)
|
||||
}
|
||||
FormTextInput(
|
||||
value = model.bio,
|
||||
label = stringResource(R.string.bio),
|
||||
hint = "Input bio",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
error = bioError
|
||||
) { value ->
|
||||
onBioChange(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
model.isLoading -> {
|
||||
Log.d("AccountEditScreen2", "显示加载指示器")
|
||||
// 加载中状态
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
androidx.compose.material3.CircularProgressIndicator(
|
||||
color = appColors.main
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.d("AccountEditScreen2", "显示错误信息 - 没有数据且不在加载中")
|
||||
// 没有数据且不在加载中,显示错误信息
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
androidx.compose.material3.Text(
|
||||
text = "加载用户资料失败,请重试",
|
||||
color = appColors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.aiosman.ravenow.ui.account
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.AppStore
|
||||
import com.aiosman.ravenow.Messaging
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.ServiceException
|
||||
import com.aiosman.ravenow.data.api.ErrorCode
|
||||
import com.aiosman.ravenow.data.api.showToast
|
||||
import com.aiosman.ravenow.data.api.toErrorMessage
|
||||
import com.aiosman.ravenow.ui.NavigationRoute
|
||||
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.ravenow.ui.composables.ActionButton
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.composables.TextInputField
|
||||
import com.aiosman.ravenow.utils.PasswordValidator
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun RemoveAccountScreen() {
|
||||
val appColors = LocalAppTheme.current
|
||||
val navController = LocalNavController.current
|
||||
var inputPassword by remember { mutableStateOf("") }
|
||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
fun removeAccount(password: String) {
|
||||
// 使用通用密码校验器
|
||||
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
|
||||
if (!passwordValidation.isValid) {
|
||||
passwordError = passwordValidation.errorMessage
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
val accountService = AccountServiceImpl()
|
||||
accountService.removeAccount(password)
|
||||
Messaging.unregisterDevice(context)
|
||||
AppStore.apply {
|
||||
token = null
|
||||
rememberMe = false
|
||||
saveData()
|
||||
}
|
||||
//返回到登录页面
|
||||
navController.navigate(NavigationRoute.Login.route) {
|
||||
popUpTo(NavigationRoute.Login.route) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
//重置AppState
|
||||
AppState.ReloadAppState(context)
|
||||
Toast.makeText(context, "Account has been deleted", Toast.LENGTH_SHORT).show()
|
||||
} catch (e: ServiceException) {
|
||||
passwordError = "Incorrect password"
|
||||
// e.errorType.showToast(context)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(context, "An error occurred. Please try again.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(appColors.background),
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
) {
|
||||
NoticeScreenHeader(
|
||||
title = stringResource(R.string.remove_account),
|
||||
moreIcon = false
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
.background(appColors.background),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 32.dp, top = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.remove_account_desc),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
color = appColors.text
|
||||
)
|
||||
}
|
||||
TextInputField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = inputPassword,
|
||||
onValueChange = {
|
||||
inputPassword = it
|
||||
if (passwordError != null) {
|
||||
passwordError = null
|
||||
}
|
||||
},
|
||||
password = true,
|
||||
hint = stringResource(R.string.remove_account_password_hint),
|
||||
error = passwordError
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
ActionButton(
|
||||
text = stringResource(R.string.remove_account),
|
||||
fullWidth = true,
|
||||
enabled = true,
|
||||
click = {
|
||||
removeAccount(inputPassword)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user