修改个人资料修改逻辑

This commit is contained in:
2024-10-04 00:02:26 +08:00
parent 775c6a3c14
commit c2764754fd
9 changed files with 372 additions and 185 deletions

View File

@@ -10,8 +10,25 @@ object ConstVars {
const val MOMENT_LIKE_CHANNEL_ID = "moment_like" const val MOMENT_LIKE_CHANNEL_ID = "moment_like"
const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like" const val MOMENT_LIKE_CHANNEL_NAME = "Moment Like"
} /**
* 上传头像图片大小限制
* 10M
*/
const val AVATAR_FILE_SIZE_LIMIT = 1024 * 1024 * 10
/**
* 上传头像图片压缩时最大的尺寸
* 512
*/
const val AVATAR_IMAGE_MAX_SIZE = 512
/**
* 上传 banner 图片大小限制
*/
const val BANNER_IMAGE_MAX_SIZE = 2048
}
//
enum class ErrorCode(val code: Int) { enum class ErrorCode(val code: Int) {
USER_EXIST(10001) USER_EXIST(10001)
} }

View File

@@ -1,39 +1,25 @@
package com.aiosman.riderpro.ui.account package com.aiosman.riderpro.ui.account
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -47,14 +33,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
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.dp
import androidx.compose.ui.unit.sp
import com.aiosman.riderpro.AppColors import com.aiosman.riderpro.AppColors
import com.aiosman.riderpro.ConstVars
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.data.AccountService import com.aiosman.riderpro.data.AccountService
@@ -64,21 +46,24 @@ import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.StatusBarSpacer import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.composables.form.FormTextInput
import com.aiosman.riderpro.ui.composables.pickupAndCompressLauncher
import com.aiosman.riderpro.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.NewPostViewModel.uriToFile import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
/** /**
* 编辑用户资料界面 * 编辑用户资料界面
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AccountEditScreen2() { fun AccountEditScreen2() {
val accountService: AccountService = AccountServiceImpl() val accountService: AccountService = AccountServiceImpl()
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
var bio by remember { mutableStateOf("") } var bio by remember { mutableStateOf("") }
var imageUrl by remember { mutableStateOf<Uri?>(null) } var imageUrl by remember { mutableStateOf<Uri?>(null) }
var bannerImageUrl by remember { mutableStateOf<Uri?>(null) } var imageFile by remember { mutableStateOf<File?>(null) }
var profile by remember { var profile by remember {
mutableStateOf<AccountProfileEntity?>( mutableStateOf<AccountProfileEntity?>(
null null
@@ -87,6 +72,29 @@ fun AccountEditScreen2() {
val navController = LocalNavController.current val navController = LocalNavController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
var usernameError by remember { mutableStateOf<String?>(null) }
var bioError by remember { mutableStateOf<String?>(null) }
fun onNicknameChange(value: String) {
name = value
usernameError = when {
value.isEmpty() -> "昵称不能为空"
value.length < 3 -> "昵称长度不能小于3"
value.length > 20 -> "昵称长度不能大于20"
else -> null
}
}
fun onBioChange(value: String) {
bio = value
bioError = when {
value.length > 100 -> "个人简介长度不能大于24"
else -> null
}
}
fun validate(): Boolean {
return usernameError == null && bioError == null
}
/** /**
* 加载用户资料 * 加载用户资料
@@ -99,69 +107,70 @@ fun AccountEditScreen2() {
} }
} }
/**
* 更新用户资料
*/
fun updateUserProfile() { fun updateUserProfile() {
if (!validate()) {
Toast.makeText(context, "请检查输入", Toast.LENGTH_SHORT).show()
return
}
scope.launch { scope.launch {
val newAvatar = imageUrl?.let { val newAvatar = imageUrl?.let {
// 检查图片文件
val avatarFile = imageFile ?: return@let null
// 读取文件名
val cursor = context.contentResolver.query(it, null, null, null, null) val cursor = context.contentResolver.query(it, null, null, null, null)
var newAvatar: UploadImage? = null var newAvatar: UploadImage? = null
cursor?.use { cur -> cursor?.use { cur ->
if (cur.moveToFirst()) { val columnIndex = cur.getColumnIndex("_display_name")
val displayName = cur.getString(cur.getColumnIndex("_display_name")) if (columnIndex != -1 && cur.moveToFirst()) {
val displayName = cur.getString(columnIndex)
val extension = displayName.substringAfterLast(".") val extension = displayName.substringAfterLast(".")
Log.d("NewPost", "File name: $displayName, extension: $extension") Log.d("Profile Edit", "File name: $displayName, extension: $extension")
// read as file // read as file
val file = uriToFile(context, it) Log.d("Profile Edit", "File size: ${avatarFile.length()}")
Log.d("NewPost", "File size: ${file.length()}") newAvatar = UploadImage(avatarFile, displayName, it.toString(), extension)
newAvatar = UploadImage(file, displayName, it.toString(), extension)
} }
} }
newAvatar 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 val newName = if (name == profile?.nickName) null else name
accountService.updateProfile( accountService.updateProfile(
avatar = newAvatar, avatar = newAvatar,
banner = newBanner, banner = null,
nickName = newName, nickName = newName,
bio = bio bio = bio
) )
// 刷新用户资料
reloadProfile() reloadProfile()
// 刷新个人资料页面的用户资料
MyProfileViewModel.loadUserProfile()
navController.popBackStack() navController.popBackStack()
} }
} }
val pickImageLauncher = rememberLauncherForActivityResult( val pickImageLauncher = pickupAndCompressLauncher(
contract = ActivityResultContracts.StartActivityForResult() context = context,
) { result -> scope = scope,
if (result.resultCode == Activity.RESULT_OK) { maxSize = ConstVars.AVATAR_IMAGE_MAX_SIZE
val uri = result.data?.data ) { uri, file ->
uri?.let { if (file.length() <= ConstVars.AVATAR_FILE_SIZE_LIMIT) {
imageUrl = it imageUrl = uri
imageFile = file
} else {
scope.launch(Dispatchers.Main) {
Toast.makeText(context, "图片过大", Toast.LENGTH_SHORT).show()
} }
} }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
reloadProfile() reloadProfile()
} }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize().background(Color.White), .fillMaxSize()
.background(Color.White),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
StatusBarSpacer() StatusBarSpacer()
@@ -180,25 +189,22 @@ fun AccountEditScreen2() {
updateUserProfile() updateUserProfile()
}, },
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = "保存" contentDescription = "保存",
tint = if (validate()) Color.Black else Color.Gray
) )
} }
} }
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
profile?.let { profile?.let {
Box( Box(
modifier = Modifier.size(width = 112.dp, height = 112.dp), modifier = Modifier.size(112.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.avatar_bold), contentDescription = ""
)
CustomAsyncImage( CustomAsyncImage(
context, context,
imageUrl?.toString() ?: it.avatar, imageUrl?.toString() ?: it.avatar,
modifier = Modifier modifier = Modifier
.size(width = 88.dp, height = 88.dp) .size(112.dp)
.clip( .clip(
RoundedCornerShape(88.dp) RoundedCornerShape(88.dp)
), ),
@@ -230,72 +236,27 @@ fun AccountEditScreen2() {
Spacer(modifier = Modifier.height(46.dp)) Spacer(modifier = Modifier.height(46.dp))
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Row( FormTextInput(
modifier = Modifier
.clip(RoundedCornerShape(24.dp))
.background(Color(0xfff8f8f8))
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.nickname),
modifier = Modifier
.widthIn(100.dp),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF333333)
)
)
BasicTextField(
maxLines = 1,
value = name, value = name,
onValueChange = { label = stringResource(R.string.nickname),
name = it hint = "Input nickname",
}, modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle( error = usernameError
fontSize = 16.sp, ) { value ->
fontWeight = FontWeight.Normal onNicknameChange(value)
),
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Row( FormTextInput(
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.background(Color(0xfff8f8f8))
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.bio),
modifier = Modifier
.widthIn(100.dp),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF333333)
)
)
BasicTextField(
value = bio, value = bio,
onValueChange = { label = stringResource(R.string.bio),
bio = it hint = "Input bio",
}, modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle( error = bioError
fontSize = 16.sp, ) { value ->
fontWeight = FontWeight.Normal onBioChange(value)
),
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
} }
} }
} }

View File

@@ -0,0 +1,38 @@
package com.aiosman.riderpro.ui.composables
import android.app.Activity
import android.content.Context
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import com.aiosman.riderpro.utils.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.io.File
/**
* 选择图片并压缩
*/
@Composable
fun pickupAndCompressLauncher(
context: Context,
scope: CoroutineScope,
maxSize: Int = 512,
quality: Int = 85,
onImagePicked: (Uri, File) -> Unit
) = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
uri?.let {
scope.launch {
// Compress the image
val file = Utils.compressImage(context, it, maxSize = maxSize, quality = quality)
// Check the compressed image size
onImagePicked(it, file)
}
}
}
}

View File

@@ -0,0 +1,133 @@
package com.aiosman.riderpro.ui.composables.form
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.res.painterResource
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.riderpro.R
@Composable
fun FormTextInput(
modifier: Modifier = Modifier,
value: String,
label: String? = null,
error: String? = null,
hint: String? = null,
onValueChange: (String) -> Unit
) {
Column(
modifier = modifier
) {
Row(
modifier = Modifier.fillMaxWidth()
.clip(RoundedCornerShape(24.dp))
.background(Color(0xfff8f8f8))
.let {
if (error != null) {
it.border(1.dp, Color(0xFFE53935), RoundedCornerShape(24.dp))
} else {
it
}
}
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
label?.let {
Text(
text = it,
modifier = Modifier
.widthIn(100.dp),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF333333)
)
)
}
Box(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
) {
if (value.isEmpty()) {
Text(
text = hint ?: "",
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
color = Color(0xFFCCCCCC)
)
)
}
BasicTextField(
maxLines = 1,
value = value,
onValueChange = {
onValueChange(it)
},
singleLine = true,
textStyle = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal
),
)
}
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(
visible = error != null,
enter = fadeIn(),
exit = fadeOut()
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = painterResource(id = R.mipmap.rider_pro_input_error),
contentDescription = "Error",
modifier = Modifier.size(8.dp)
)
Spacer(modifier = Modifier.size(4.dp))
AnimatedContent(targetState = error) { targetError ->
Text(targetError ?: "", color = Color(0xFFE53935), fontSize = 12.sp)
}
}
}
}
}
}

View File

@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
object MyProfileViewModel : ViewModel() { object MyProfileViewModel : ViewModel() {
val accountService: AccountService = AccountServiceImpl() val accountService: AccountService = AccountServiceImpl()
@@ -38,17 +39,21 @@ object MyProfileViewModel : ViewModel() {
var refreshing by mutableStateOf(false) var refreshing by mutableStateOf(false)
var firstLoad = true var firstLoad = true
suspend fun loadUserProfile() {
val profile = accountService.getMyAccountProfile()
MyProfileViewModel.profile = profile
}
fun loadProfile(pullRefresh: Boolean = false) { fun loadProfile(pullRefresh: Boolean = false) {
if (!firstLoad) return if (!firstLoad && !pullRefresh) return
viewModelScope.launch { viewModelScope.launch {
if (pullRefresh) { if (pullRefresh) {
refreshing = true refreshing = true
} }
firstLoad = false firstLoad = false
val profile = accountService.getMyAccountProfile() loadUserProfile()
MyProfileViewModel.profile = profile
refreshing = false refreshing = false
profile?.let {
try { try {
// Collect shared flow // Collect shared flow
Pager( Pager(
@@ -56,7 +61,7 @@ object MyProfileViewModel : ViewModel() {
pagingSourceFactory = { pagingSourceFactory = {
MomentPagingSource( MomentPagingSource(
MomentRemoteDataSource(momentService), MomentRemoteDataSource(momentService),
author = profile.id author = AppState.UserId
) )
} }
).flow.cachedIn(viewModelScope).collectLatest { ).flow.cachedIn(viewModelScope).collectLatest {
@@ -66,6 +71,8 @@ object MyProfileViewModel : ViewModel() {
Log.e("MyProfileViewModel", "loadProfile: ", e) Log.e("MyProfileViewModel", "loadProfile: ", e)
} }
} }
}
} }
suspend fun logout() { suspend fun logout() {
@@ -77,19 +84,19 @@ object MyProfileViewModel : ViewModel() {
AppState.ReloadAppState() AppState.ReloadAppState()
} }
fun updateUserProfileBanner(bannerImageUrl: Uri?, context: Context) { fun updateUserProfileBanner(bannerImageUrl: Uri?,file:File, context: Context) {
viewModelScope.launch { viewModelScope.launch {
var newBanner = bannerImageUrl?.let { val newBanner = bannerImageUrl?.let {
val cursor = context.contentResolver.query(it, null, null, null, null) val cursor = context.contentResolver.query(it, null, null, null, null)
var newBanner: UploadImage? = null var newBanner: UploadImage? = null
cursor?.use { cur -> cursor?.use { cur ->
if (cur.moveToFirst()) { val columnIndex = cur.getColumnIndex("_display_name")
val displayName = cur.getString(cur.getColumnIndex("_display_name")) if (cur.moveToFirst() && columnIndex != -1) {
val displayName = cur.getString(columnIndex)
val extension = displayName.substringAfterLast(".") val extension = displayName.substringAfterLast(".")
Log.d("NewPost", "File name: $displayName, extension: $extension") Log.d("Change banner", "File name: $displayName, extension: $extension")
// read as file // read as file
val file = uriToFile(context, it) Log.d("Change banner", "File size: ${file.length()}")
Log.d("NewPost", "File size: ${file.length()}")
newBanner = UploadImage(file, displayName, it.toString(), extension) newBanner = UploadImage(file, displayName, it.toString(), extension)
} }
} }

View File

@@ -8,7 +8,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -27,17 +26,18 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -47,28 +47,21 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.media3.common.util.Log
import androidx.media3.common.util.UnstableApi
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.AppState import com.aiosman.riderpro.AppState
import com.aiosman.riderpro.ConstVars
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.entity.AccountProfileEntity import com.aiosman.riderpro.entity.AccountProfileEntity
@@ -78,6 +71,7 @@ import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.DropdownMenu import com.aiosman.riderpro.ui.composables.DropdownMenu
import com.aiosman.riderpro.ui.composables.MenuItem import com.aiosman.riderpro.ui.composables.MenuItem
import com.aiosman.riderpro.ui.composables.StatusBarSpacer import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.composables.pickupAndCompressLauncher
import com.aiosman.riderpro.ui.composables.toolbar.CollapsingToolbarScaffold import com.aiosman.riderpro.ui.composables.toolbar.CollapsingToolbarScaffold
import com.aiosman.riderpro.ui.composables.toolbar.ScrollStrategy import com.aiosman.riderpro.ui.composables.toolbar.ScrollStrategy
import com.aiosman.riderpro.ui.composables.toolbar.rememberCollapsingToolbarScaffoldState import com.aiosman.riderpro.ui.composables.toolbar.rememberCollapsingToolbarScaffoldState
@@ -94,11 +88,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable @Composable
fun ProfileV3( fun ProfileV3(
onUpdateBanner: ((Uri, Context) -> Unit)? = null, onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,
profile: AccountProfileEntity? = null, profile: AccountProfileEntity? = null,
onLogout: () -> Unit = {}, onLogout: () -> Unit = {},
onFollowClick: () -> Unit = {}, onFollowClick: () -> Unit = {},
@@ -108,6 +103,7 @@ fun ProfileV3(
).asStateFlow(), ).asStateFlow(),
isSelf: Boolean = true isSelf: Boolean = true
) { ) {
val model = MyProfileViewModel
val state = rememberCollapsingToolbarScaffoldState() val state = rememberCollapsingToolbarScaffoldState()
val pagerState = rememberPagerState(pageCount = { 2 }) val pagerState = rememberPagerState(pageCount = { 2 })
var enabled by remember { mutableStateOf(true) } var enabled by remember { mutableStateOf(true) }
@@ -117,19 +113,21 @@ fun ProfileV3(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val navController = LocalNavController.current val navController = LocalNavController.current
var bannerHeight = 400 var bannerHeight = 400
val pickBannerImageLauncher = rememberLauncherForActivityResult( val pickBannerImageLauncher = pickupAndCompressLauncher(
contract = ActivityResultContracts.StartActivityForResult() context,
) { result -> scope,
if (result.resultCode == Activity.RESULT_OK) { maxSize = ConstVars.BANNER_IMAGE_MAX_SIZE,
val uri = result.data?.data quality = 100
uri?.let { ) { uri, file ->
onUpdateBanner?.invoke(it, context) onUpdateBanner?.invoke(uri, file, context)
}
}
} }
val moments = sharedFlow.collectAsLazyPagingItems() val moments = sharedFlow.collectAsLazyPagingItems()
val refreshState = rememberPullRefreshState(model.refreshing, onRefresh = {
Box { model.loadProfile(pullRefresh = true)
})
Box(
modifier = Modifier.pullRefresh(refreshState)
) {
CollapsingToolbarScaffold( CollapsingToolbarScaffold(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -447,5 +445,6 @@ fun ProfileV3(
} }
} }
PullRefreshIndicator(model.refreshing, refreshState, Modifier.align(Alignment.TopCenter))
} }
} }

View File

@@ -26,8 +26,8 @@ fun ProfileWrap(
// sharedFlow = MyProfileViewModel.sharedFlow // sharedFlow = MyProfileViewModel.sharedFlow
// ) // )
ProfileV3( ProfileV3(
onUpdateBanner = { uri, context -> onUpdateBanner = { uri, file, context ->
MyProfileViewModel.updateUserProfileBanner(uri, context) MyProfileViewModel.updateUserProfileBanner(uri, file, context)
}, },
onLogout = { onLogout = {
MyProfileViewModel.viewModelScope.launch { MyProfileViewModel.viewModelScope.launch {

View File

@@ -77,6 +77,7 @@ import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.DropdownMenu import com.aiosman.riderpro.ui.composables.DropdownMenu
import com.aiosman.riderpro.ui.composables.MenuItem import com.aiosman.riderpro.ui.composables.MenuItem
import com.aiosman.riderpro.ui.composables.pickupAndCompressLauncher
import com.aiosman.riderpro.ui.index.tabs.profile.composable.EmptyMomentPostUnit import com.aiosman.riderpro.ui.index.tabs.profile.composable.EmptyMomentPostUnit
import com.aiosman.riderpro.ui.index.tabs.profile.composable.GalleryItem import com.aiosman.riderpro.ui.index.tabs.profile.composable.GalleryItem
import com.aiosman.riderpro.ui.index.tabs.profile.composable.MomentPostUnit import com.aiosman.riderpro.ui.index.tabs.profile.composable.MomentPostUnit
@@ -90,12 +91,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ProfileV2( fun ProfileV2(
onUpdateBanner: ((Uri, Context) -> Unit)? = null, onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,
profile: AccountProfileEntity? = null, profile: AccountProfileEntity? = null,
onLogout: () -> Unit = {}, onLogout: () -> Unit = {},
onFollowClick: () -> Unit = {}, onFollowClick: () -> Unit = {},
@@ -121,15 +123,11 @@ fun ProfileV2(
val navController = LocalNavController.current val navController = LocalNavController.current
val moments = sharedFlow.collectAsLazyPagingItems() val moments = sharedFlow.collectAsLazyPagingItems()
val rootScrollState = rememberScrollState() val rootScrollState = rememberScrollState()
val pickBannerImageLauncher = rememberLauncherForActivityResult( val pickBannerImageLauncher = pickupAndCompressLauncher(
contract = ActivityResultContracts.StartActivityForResult() context,
) { result -> scope
if (result.resultCode == Activity.RESULT_OK) { ) { uri, file ->
val uri = result.data?.data onUpdateBanner?.invoke(uri, file, context)
uri?.let {
onUpdateBanner?.invoke(it, context)
}
}
} }
val parentScrollConnection = remember { val parentScrollConnection = remember {
object : NestedScrollConnection { object : NestedScrollConnection {

View File

@@ -1,14 +1,20 @@
package com.aiosman.riderpro.utils package com.aiosman.riderpro.utils
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import coil.ImageLoader import coil.ImageLoader
import coil.disk.DiskCache import coil.disk.DiskCache
import coil.memory.MemoryCache import coil.memory.MemoryCache
import coil.request.CachePolicy import coil.request.CachePolicy
import com.aiosman.riderpro.data.api.AuthInterceptor import com.aiosman.riderpro.data.api.AuthInterceptor
import com.aiosman.riderpro.data.api.getUnsafeOkHttpClient import com.aiosman.riderpro.data.api.getUnsafeOkHttpClient
import java.io.File
import java.io.FileOutputStream
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
object Utils { object Utils {
@@ -61,4 +67,32 @@ object Utils {
fun getCurrentLanguage(): String { fun getCurrentLanguage(): String {
return Locale.getDefault().language return Locale.getDefault().language
} }
fun compressImage(context: Context, uri: Uri, maxSize: Int = 512, quality: Int = 85): File {
val inputStream = context.contentResolver.openInputStream(uri)
val originalBitmap = BitmapFactory.decodeStream(inputStream)
val (width, height) = originalBitmap.width to originalBitmap.height
val (newWidth, newHeight) = if (width > height) {
maxSize to (height * maxSize / width)
} else {
(width * maxSize / height) to maxSize
}
val scaledBitmap = Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true)
val uuidImageName = UUID.randomUUID().toString().let { "$it.jpg" }
val compressedFile = File(context.cacheDir, uuidImageName)
val outputStream = FileOutputStream(compressedFile)
if (quality > 100) {
throw IllegalArgumentException("Quality must be less than 100")
}
if (quality < 0) {
throw IllegalArgumentException("Quality must be greater than 0")
}
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
outputStream.flush()
outputStream.close()
return compressedFile
}
} }