改包名com.aiosman.ravenow
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
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 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)
|
||||
suspend fun reloadProfile() {
|
||||
accountService.getMyAccountProfile().let {
|
||||
profile = it
|
||||
name = it.nickName
|
||||
bio = it.bio
|
||||
}
|
||||
}
|
||||
|
||||
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 newName = if (name == profile?.nickName) null else name
|
||||
accountService.updateProfile(
|
||||
avatar = newAvatar,
|
||||
banner = null,
|
||||
nickName = newName,
|
||||
bio = bio
|
||||
)
|
||||
// 刷新用户资料
|
||||
reloadProfile()
|
||||
// 刷新个人资料页面的用户资料
|
||||
MyProfileViewModel.loadUserProfile()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.aiosman.ravenow.ui.account
|
||||
|
||||
import android.widget.Toast
|
||||
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.toInt()
|
||||
}
|
||||
} 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()
|
||||
) {
|
||||
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,160 @@
|
||||
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.unit.dp
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
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 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("") }
|
||||
var errorMessage 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 {
|
||||
oldPasswordError =
|
||||
if (currentPassword.isEmpty()) "Please enter your current password" else null
|
||||
passwordError = when {
|
||||
newPassword.length < 8 -> "Password must be at least 8 characters long"
|
||||
!newPassword.any { it.isDigit() } -> "Password must contain at least one digit"
|
||||
!newPassword.any { it.isUpperCase() } -> "Password must contain at least one uppercase letter"
|
||||
!newPassword.any { it.isLowerCase() } -> "Password must contain at least one lowercase letter"
|
||||
else -> null
|
||||
}
|
||||
confirmPasswordError =
|
||||
if (newPassword != confirmPassword) "Passwords do not match" 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 = "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 = "Current password",
|
||||
hint = "Enter your current password",
|
||||
error = oldPasswordError
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
TextInputField(
|
||||
text = newPassword,
|
||||
onValueChange = { newPassword = it },
|
||||
password = true,
|
||||
label = "New password",
|
||||
hint = "Enter your new password",
|
||||
error = passwordError
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
TextInputField(
|
||||
text = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
password = true,
|
||||
label = "Confirm new password",
|
||||
hint = "Enter your new password again",
|
||||
error = confirmPasswordError
|
||||
)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
ActionButton(
|
||||
modifier = Modifier
|
||||
.width(345.dp),
|
||||
text = "Let's Ride",
|
||||
) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
240
app/src/main/java/com/aiosman/ravenow/ui/account/edit.kt
Normal file
240
app/src/main/java/com/aiosman/ravenow/ui/account/edit.kt
Normal file
@@ -0,0 +1,240 @@
|
||||
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 displayName = cur.getString(cur.getColumnIndex("_display_name"))
|
||||
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 displayName = cur.getString(cur.getColumnIndex("_display_name"))
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
190
app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt
Normal file
190
app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt
Normal file
@@ -0,0 +1,190 @@
|
||||
package com.aiosman.ravenow.ui.account
|
||||
|
||||
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.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.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.composables.form.FormTextInput
|
||||
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 编辑用户资料界面
|
||||
*/
|
||||
@Composable
|
||||
fun AccountEditScreen2() {
|
||||
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) }
|
||||
fun onNicknameChange(value: String) {
|
||||
model.name = value
|
||||
usernameError = when {
|
||||
value.isEmpty() -> "昵称不能为空"
|
||||
value.length < 3 -> "昵称长度不能小于3"
|
||||
value.length > 20 -> "昵称长度不能大于20"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val appColors = LocalAppTheme.current
|
||||
|
||||
fun onBioChange(value: String) {
|
||||
model.bio = value
|
||||
bioError = when {
|
||||
value.length > 100 -> "个人简介长度不能大于24"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun validate(): Boolean {
|
||||
return usernameError == null && bioError == null
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (model.profile == null) {
|
||||
model.reloadProfile()
|
||||
}
|
||||
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = appColors.background),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||
) {
|
||||
NoticeScreenHeader(
|
||||
title = stringResource(R.string.edit_profile),
|
||||
moreIcon = false
|
||||
) {
|
||||
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.noRippleClickable {
|
||||
|
||||
if (validate() && !model.isUpdating) {
|
||||
model.viewModelScope.launch {
|
||||
model.isUpdating = true
|
||||
model.updateUserProfile(context)
|
||||
model.viewModelScope.launch(Dispatchers.Main) {
|
||||
navController.navigateUp()
|
||||
model.isUpdating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "保存",
|
||||
tint = if (validate() && !model.isUpdating) Color.Black else Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(44.dp))
|
||||
model.profile?.let {
|
||||
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(appColors.main)
|
||||
.align(Alignment.BottomEnd)
|
||||
.noRippleClickable {
|
||||
navController.navigate(NavigationRoute.ImageCrop.route)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = "Add",
|
||||
tint = Color.White,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.height(58.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)
|
||||
}
|
||||
// Spacer(modifier = Modifier.height(16.dp))
|
||||
FormTextInput(
|
||||
value = model.bio,
|
||||
label = stringResource(R.string.bio),
|
||||
hint = "Input bio",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
error = bioError
|
||||
) { value ->
|
||||
onBioChange(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user