更新个人资料编辑功能

- 新增主页背景图编辑
- 优化个人资料编辑页面布局
This commit is contained in:
2024-08-28 16:40:09 +08:00
parent 07b89fe3cc
commit 65e348e704
11 changed files with 232 additions and 103 deletions

View File

@@ -90,7 +90,5 @@ dependencies {
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("androidx.credentials:credentials:1.2.2")
implementation("androidx.credentials:credentials-play-services-auth:1.2.2")
}

View File

@@ -36,7 +36,11 @@ data class AccountProfile(
// 粉丝数
val followerCount: Int,
// 是否关注
val isFollowing: Boolean
val isFollowing: Boolean,
// 个人简介
val bio: String,
// 主页背景图
val banner: String?,
) {
/**
* 转换为Entity
@@ -48,9 +52,10 @@ data class AccountProfile(
followingCount = followingCount,
nickName = nickname,
avatar = "${ApiClient.BASE_SERVER}$avatar",
bio = "",
bio = bio,
country = "Worldwide",
isFollowing = isFollowing
isFollowing = isFollowing,
banner = "${ApiClient.BASE_SERVER}$banner"
)
}
}
@@ -230,8 +235,14 @@ interface AccountService {
* @param avatar 头像
* @param nickName 昵称
* @param bio 简介
* @param banner 主页背景图
*/
suspend fun updateProfile(avatar: UploadImage?, nickName: String?, bio: String?)
suspend fun updateProfile(
avatar: UploadImage?,
banner: UploadImage?,
nickName: String?,
bio: String?
)
/**
* 注册用户
@@ -328,12 +339,21 @@ class AccountServiceImpl : AccountService {
return MultipartBody.Part.createFormData(name, filename, requestFile)
}
override suspend fun updateProfile(avatar: UploadImage?, nickName: String?, bio: String?) {
override suspend fun updateProfile(
avatar: UploadImage?,
banner: UploadImage?,
nickName: String?,
bio: String?
) {
val nicknameField: RequestBody? = nickName?.toRequestBody("text/plain".toMediaTypeOrNull())
val bioField: RequestBody? = bio?.toRequestBody("text/plain".toMediaTypeOrNull())
val avatarField: MultipartBody.Part? = avatar?.let {
createMultipartBody(it.file, it.filename, "avatar")
}
ApiClient.api.updateProfile(avatarField, nicknameField)
val bannerField: MultipartBody.Part? = banner?.let {
createMultipartBody(it.file, it.filename, "banner")
}
ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
}
override suspend fun registerUserWithPassword(loginName: String, password: String) {

View File

@@ -169,7 +169,9 @@ interface RiderProAPI {
@PATCH("account/my/profile")
suspend fun updateProfile(
@Part avatar: MultipartBody.Part?,
@Part banner: MultipartBody.Part?,
@Part("nickname") nickname: RequestBody?,
@Part("bio") bio: RequestBody?,
): Response<Unit>
@POST("account/my/password")

View File

@@ -52,7 +52,9 @@ data class AccountProfileEntity(
// 国家
val country: String,
// 是否关注,针对当前登录用户
val isFollowing: Boolean
val isFollowing: Boolean,
// 主页背景图
val banner: String?,
)
/**

View File

@@ -10,6 +10,7 @@ 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
@@ -33,12 +34,11 @@ 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.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.data.AccountService
import com.aiosman.riderpro.data.AccountServiceImpl
import com.aiosman.riderpro.data.UserServiceImpl
import com.aiosman.riderpro.data.UploadImage
import com.aiosman.riderpro.data.UserService
import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.NewPostViewModel.uriToFile
@@ -54,11 +54,13 @@ fun AccountEditScreen() {
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
@@ -71,8 +73,6 @@ fun AccountEditScreen() {
name = it.nickName
bio = it.bio
}
}
fun updateUserProfile() {
@@ -93,10 +93,31 @@ fun AccountEditScreen() {
}
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(newAvatar, newName, bio)
accountService.updateProfile(
avatar = newAvatar,
banner = newBanner,
nickName = newName,
bio = bio
)
reloadProfile()
navController.popBackStack()
}
}
@@ -110,11 +131,20 @@ fun AccountEditScreen() {
}
}
}
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(
@@ -161,6 +191,26 @@ fun AccountEditScreen() {
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 = {

View File

@@ -115,9 +115,10 @@ fun IndexScreen() {
}
}
) { innerPadding ->
innerPadding
HorizontalPager(
state = pagerState,
modifier = Modifier.padding(innerPadding),
modifier = Modifier.padding(0.dp),
beyondBoundsPageCount = 5,
userScrollEnabled = false
) { page ->

View File

@@ -7,11 +7,14 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
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.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -51,6 +54,8 @@ fun NotificationsScreen() {
val systemUiController = rememberSystemUiController()
var dataFlow = MessageListViewModel.commentItemsFlow
var comments = dataFlow.collectAsLazyPagingItems()
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(Color.Transparent)
MessageListViewModel.initData()
@@ -59,6 +64,7 @@ fun NotificationsScreen() {
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.padding(statusBarPaddingValues.calculateTopPadding()))
Box(
modifier = Modifier
.fillMaxWidth()

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
@@ -21,6 +22,7 @@ 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.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
@@ -100,44 +102,52 @@ fun MomentsList() {
refreshing = false
}
}
Box(Modifier.pullRefresh(state)) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(
moments.itemCount,
key = { idx -> moments[idx]?.id ?: idx }
) { idx ->
val momentItem = moments[idx] ?: return@items
MomentCard(momentEntity = momentItem,
onAddComment = {
scope.launch {
model.onAddComment(momentItem.id)
}
},
onLikeClick = {
scope.launch {
if (momentItem.liked) {
model.dislikeMoment(momentItem.id)
} else {
model.likeMoment(momentItem.id)
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
Column(
modifier = Modifier
.fillMaxSize()
.padding(
top = statusBarPaddingValues.calculateTopPadding(),
)
) {
Box(Modifier.pullRefresh(state)) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
items(
moments.itemCount,
key = { idx -> moments[idx]?.id ?: idx }
) { idx ->
val momentItem = moments[idx] ?: return@items
MomentCard(momentEntity = momentItem,
onAddComment = {
scope.launch {
model.onAddComment(momentItem.id)
}
},
onLikeClick = {
scope.launch {
if (momentItem.liked) {
model.dislikeMoment(momentItem.id)
} else {
model.likeMoment(momentItem.id)
}
}
},
onFavoriteClick = {
scope.launch {
if (momentItem.isFavorite) {
model.unfavoriteMoment(momentItem.id)
} else {
model.favoriteMoment(momentItem.id)
}
}
}
},
onFavoriteClick = {
scope.launch {
if (momentItem.isFavorite) {
model.unfavoriteMoment(momentItem.id)
} else {
model.favoriteMoment(momentItem.id)
}
}
}
)
)
}
}
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
}
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
}
}

View File

@@ -3,7 +3,9 @@ package com.aiosman.riderpro.ui.index.tabs.profile
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -12,12 +14,15 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
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.layout.systemBars
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
@@ -75,67 +80,92 @@ fun ProfilePage() {
val moments = model.momentsFlow.collectAsLazyPagingItems()
val navController: NavController = LocalNavController.current
val scope = rememberCoroutineScope()
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
LazyColumn(
modifier = Modifier
.fillMaxSize(),
.fillMaxSize()
) {
item {
item {
Box(
modifier = Modifier
.fillMaxWidth()
) {
val banner = model.profile?.banner
if (banner != null) {
CustomAsyncImage(
LocalContext.current,
banner,
modifier = Modifier
.fillMaxWidth()
.height(400.dp),
contentDescription = "",
contentScale = ContentScale.Crop
)
} else {
Box(
modifier = Modifier
.fillMaxWidth()
.height(400.dp)
.background(Color.Gray)
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
) {
Box(
modifier = Modifier.align(Alignment.TopEnd)
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
modifier = Modifier.noRippleClickable {
expanded = true
}
.align(Alignment.TopEnd)
.padding(
top = statusBarPaddingValues.calculateTopPadding(),
start = 16.dp,
end = 16.dp
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = {
scope.launch {
model.logout()
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Index.route) {
inclusive = true
}
) {
Icon(
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
contentDescription = "",
modifier = Modifier.noRippleClickable {
expanded = true
}
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(onClick = {
scope.launch {
model.logout()
navController.navigate(NavigationRoute.Login.route) {
popUpTo(NavigationRoute.Index.route) {
inclusive = true
}
}
}, text = {
Text("Logout")
})
DropdownMenuItem(onClick = {
scope.launch {
navController.navigate(NavigationRoute.AccountEdit.route)
}
}, text = {
Text("Edit")
})
DropdownMenuItem(onClick = {
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
}, text = {
Text("Change password")
})
}
}
}, text = {
Text("Logout")
})
DropdownMenuItem(onClick = {
scope.launch {
navController.navigate(NavigationRoute.AccountEdit.route)
}
}, text = {
Text("Edit")
})
DropdownMenuItem(onClick = {
scope.launch {
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
}
}, text = {
Text("Change password")
})
}
}
CarGroup()
model.profile?.let {
UserInformation(accountProfileEntity = it)
}
// RidingStyle()
}
// CarGroup()
model.profile?.let {
UserInformation(accountProfileEntity = it)
}
// RidingStyle()
}
items(moments.itemCount) { idx ->
val momentItem = moments[idx] ?: return@items

View File

@@ -6,10 +6,14 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
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.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
@@ -63,6 +67,8 @@ fun SearchScreen() {
val selectedTabIndex = remember { derivedStateOf { pagerState.currentPage } }
val keyboardController = LocalSoftwareKeyboardController.current
val systemUiController = rememberSystemUiController()
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
LaunchedEffect(Unit) {
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true)
}
@@ -72,6 +78,7 @@ fun SearchScreen() {
.fillMaxSize()
) {
Spacer(modifier = Modifier.height(statusBarPaddingValues.calculateTopPadding()))
SearchInput(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp, start = 24.dp, end = 24.dp),
text = model.searchText,