改包名com.aiosman.ravenow

This commit is contained in:
2024-11-17 20:07:42 +08:00
parent 914cfca6be
commit 074244c0f8
168 changed files with 897 additions and 970 deletions

View File

@@ -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()
}
}

View File

@@ -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()
}
}
}
}
}

View File

@@ -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()
}
}
}
}
}
}
}

View 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()
)
}
}
}
}

View 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)
}
}
}
}
}