修改个人资料修改逻辑
This commit is contained in:
@@ -10,8 +10,25 @@ object ConstVars {
|
||||
const val MOMENT_LIKE_CHANNEL_ID = "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) {
|
||||
USER_EXIST(10001)
|
||||
}
|
||||
|
||||
@@ -1,39 +1,25 @@
|
||||
package com.aiosman.riderpro.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.Image
|
||||
import android.widget.Toast
|
||||
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.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.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.filled.Add
|
||||
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.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.LaunchedEffect
|
||||
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.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.sp
|
||||
import com.aiosman.riderpro.AppColors
|
||||
import com.aiosman.riderpro.ConstVars
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.R
|
||||
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.composables.CustomAsyncImage
|
||||
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.post.NewPostViewModel.uriToFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 编辑用户资料界面
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountEditScreen2() {
|
||||
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 imageFile by remember { mutableStateOf<File?>(null) }
|
||||
var profile by remember {
|
||||
mutableStateOf<AccountProfileEntity?>(
|
||||
null
|
||||
@@ -87,6 +72,29 @@ fun AccountEditScreen2() {
|
||||
val navController = LocalNavController.current
|
||||
val scope = rememberCoroutineScope()
|
||||
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() {
|
||||
if (!validate()) {
|
||||
Toast.makeText(context, "请检查输入", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
val newAvatar = imageUrl?.let {
|
||||
// 检查图片文件
|
||||
val avatarFile = imageFile ?: return@let null
|
||||
// 读取文件名
|
||||
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 columnIndex = cur.getColumnIndex("_display_name")
|
||||
if (columnIndex != -1 && cur.moveToFirst()) {
|
||||
val displayName = cur.getString(columnIndex)
|
||||
val extension = displayName.substringAfterLast(".")
|
||||
Log.d("NewPost", "File name: $displayName, extension: $extension")
|
||||
Log.d("Profile Edit", "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)
|
||||
Log.d("Profile Edit", "File size: ${avatarFile.length()}")
|
||||
newAvatar = UploadImage(avatarFile, 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,
|
||||
banner = null,
|
||||
nickName = newName,
|
||||
bio = bio
|
||||
)
|
||||
// 刷新用户资料
|
||||
reloadProfile()
|
||||
// 刷新个人资料页面的用户资料
|
||||
MyProfileViewModel.loadUserProfile()
|
||||
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 pickImageLauncher = pickupAndCompressLauncher(
|
||||
context = context,
|
||||
scope = scope,
|
||||
maxSize = ConstVars.AVATAR_IMAGE_MAX_SIZE
|
||||
) { uri, file ->
|
||||
if (file.length() <= ConstVars.AVATAR_FILE_SIZE_LIMIT) {
|
||||
imageUrl = uri
|
||||
imageFile = file
|
||||
} else {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, "图片过大", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
reloadProfile()
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize().background(Color.White),
|
||||
.fillMaxSize()
|
||||
.background(Color.White),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
@@ -180,25 +189,22 @@ fun AccountEditScreen2() {
|
||||
updateUserProfile()
|
||||
},
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "保存"
|
||||
contentDescription = "保存",
|
||||
tint = if (validate()) Color.Black else Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
profile?.let {
|
||||
Box(
|
||||
modifier = Modifier.size(width = 112.dp, height = 112.dp),
|
||||
modifier = Modifier.size(112.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
painter = painterResource(id = R.drawable.avatar_bold), contentDescription = ""
|
||||
)
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
imageUrl?.toString() ?: it.avatar,
|
||||
modifier = Modifier
|
||||
.size(width = 88.dp, height = 88.dp)
|
||||
.size(112.dp)
|
||||
.clip(
|
||||
RoundedCornerShape(88.dp)
|
||||
),
|
||||
@@ -230,72 +236,27 @@ fun AccountEditScreen2() {
|
||||
Spacer(modifier = Modifier.height(46.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
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,
|
||||
onValueChange = {
|
||||
name = it
|
||||
},
|
||||
textStyle = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp)
|
||||
)
|
||||
FormTextInput(
|
||||
value = name,
|
||||
label = stringResource(R.string.nickname),
|
||||
hint = "Input nickname",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
error = usernameError
|
||||
) { value ->
|
||||
onNicknameChange(value)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Row(
|
||||
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,
|
||||
onValueChange = {
|
||||
bio = it
|
||||
},
|
||||
textStyle = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal
|
||||
),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp)
|
||||
)
|
||||
FormTextInput(
|
||||
value = bio,
|
||||
label = stringResource(R.string.bio),
|
||||
hint = "Input bio",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
error = bioError
|
||||
) { value ->
|
||||
onBioChange(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
object MyProfileViewModel : ViewModel() {
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
@@ -38,33 +39,39 @@ object MyProfileViewModel : ViewModel() {
|
||||
|
||||
var refreshing by mutableStateOf(false)
|
||||
var firstLoad = true
|
||||
suspend fun loadUserProfile() {
|
||||
val profile = accountService.getMyAccountProfile()
|
||||
MyProfileViewModel.profile = profile
|
||||
}
|
||||
|
||||
fun loadProfile(pullRefresh: Boolean = false) {
|
||||
if (!firstLoad) return
|
||||
if (!firstLoad && !pullRefresh) return
|
||||
viewModelScope.launch {
|
||||
if (pullRefresh) {
|
||||
refreshing = true
|
||||
}
|
||||
firstLoad = false
|
||||
val profile = accountService.getMyAccountProfile()
|
||||
MyProfileViewModel.profile = profile
|
||||
loadUserProfile()
|
||||
refreshing = false
|
||||
try {
|
||||
// Collect shared flow
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
MomentPagingSource(
|
||||
MomentRemoteDataSource(momentService),
|
||||
author = profile.id
|
||||
)
|
||||
profile?.let {
|
||||
try {
|
||||
// Collect shared flow
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
MomentPagingSource(
|
||||
MomentRemoteDataSource(momentService),
|
||||
author = AppState.UserId
|
||||
)
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_sharedFlow.value = it
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_sharedFlow.value = it
|
||||
} catch (e: Exception) {
|
||||
Log.e("MyProfileViewModel", "loadProfile: ", e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("MyProfileViewModel", "loadProfile: ", e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,19 +84,19 @@ object MyProfileViewModel : ViewModel() {
|
||||
AppState.ReloadAppState()
|
||||
}
|
||||
|
||||
fun updateUserProfileBanner(bannerImageUrl: Uri?, context: Context) {
|
||||
fun updateUserProfileBanner(bannerImageUrl: Uri?,file:File, context: Context) {
|
||||
viewModelScope.launch {
|
||||
var newBanner = bannerImageUrl?.let {
|
||||
val 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 columnIndex = cur.getColumnIndex("_display_name")
|
||||
if (cur.moveToFirst() && columnIndex != -1) {
|
||||
val displayName = cur.getString(columnIndex)
|
||||
val extension = displayName.substringAfterLast(".")
|
||||
Log.d("NewPost", "File name: $displayName, extension: $extension")
|
||||
Log.d("Change banner", "File name: $displayName, extension: $extension")
|
||||
// read as file
|
||||
val file = uriToFile(context, it)
|
||||
Log.d("NewPost", "File size: ${file.length()}")
|
||||
Log.d("Change banner", "File size: ${file.length()}")
|
||||
newBanner = UploadImage(file, displayName, it.toString(), extension)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.width
|
||||
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.StaggeredGridCells
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -47,28 +47,21 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.compose.collectAsLazyPagingItems
|
||||
import com.aiosman.riderpro.AppState
|
||||
import com.aiosman.riderpro.ConstVars
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.R
|
||||
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.MenuItem
|
||||
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.ScrollStrategy
|
||||
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.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun ProfileV3(
|
||||
onUpdateBanner: ((Uri, Context) -> Unit)? = null,
|
||||
onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,
|
||||
profile: AccountProfileEntity? = null,
|
||||
onLogout: () -> Unit = {},
|
||||
onFollowClick: () -> Unit = {},
|
||||
@@ -108,6 +103,7 @@ fun ProfileV3(
|
||||
).asStateFlow(),
|
||||
isSelf: Boolean = true
|
||||
) {
|
||||
val model = MyProfileViewModel
|
||||
val state = rememberCollapsingToolbarScaffoldState()
|
||||
val pagerState = rememberPagerState(pageCount = { 2 })
|
||||
var enabled by remember { mutableStateOf(true) }
|
||||
@@ -117,19 +113,21 @@ fun ProfileV3(
|
||||
val scope = rememberCoroutineScope()
|
||||
val navController = LocalNavController.current
|
||||
var bannerHeight = 400
|
||||
val pickBannerImageLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val uri = result.data?.data
|
||||
uri?.let {
|
||||
onUpdateBanner?.invoke(it, context)
|
||||
}
|
||||
}
|
||||
val pickBannerImageLauncher = pickupAndCompressLauncher(
|
||||
context,
|
||||
scope,
|
||||
maxSize = ConstVars.BANNER_IMAGE_MAX_SIZE,
|
||||
quality = 100
|
||||
) { uri, file ->
|
||||
onUpdateBanner?.invoke(uri, file, context)
|
||||
}
|
||||
val moments = sharedFlow.collectAsLazyPagingItems()
|
||||
|
||||
Box {
|
||||
val refreshState = rememberPullRefreshState(model.refreshing, onRefresh = {
|
||||
model.loadProfile(pullRefresh = true)
|
||||
})
|
||||
Box(
|
||||
modifier = Modifier.pullRefresh(refreshState)
|
||||
) {
|
||||
CollapsingToolbarScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -447,5 +445,6 @@ fun ProfileV3(
|
||||
}
|
||||
|
||||
}
|
||||
PullRefreshIndicator(model.refreshing, refreshState, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ fun ProfileWrap(
|
||||
// sharedFlow = MyProfileViewModel.sharedFlow
|
||||
// )
|
||||
ProfileV3(
|
||||
onUpdateBanner = { uri, context ->
|
||||
MyProfileViewModel.updateUserProfileBanner(uri, context)
|
||||
onUpdateBanner = { uri, file, context ->
|
||||
MyProfileViewModel.updateUserProfileBanner(uri, file, context)
|
||||
},
|
||||
onLogout = {
|
||||
MyProfileViewModel.viewModelScope.launch {
|
||||
|
||||
@@ -77,6 +77,7 @@ import com.aiosman.riderpro.ui.NavigationRoute
|
||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.riderpro.ui.composables.DropdownMenu
|
||||
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.GalleryItem
|
||||
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.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ProfileV2(
|
||||
onUpdateBanner: ((Uri, Context) -> Unit)? = null,
|
||||
onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,
|
||||
profile: AccountProfileEntity? = null,
|
||||
onLogout: () -> Unit = {},
|
||||
onFollowClick: () -> Unit = {},
|
||||
@@ -121,15 +123,11 @@ fun ProfileV2(
|
||||
val navController = LocalNavController.current
|
||||
val moments = sharedFlow.collectAsLazyPagingItems()
|
||||
val rootScrollState = rememberScrollState()
|
||||
val pickBannerImageLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val uri = result.data?.data
|
||||
uri?.let {
|
||||
onUpdateBanner?.invoke(it, context)
|
||||
}
|
||||
}
|
||||
val pickBannerImageLauncher = pickupAndCompressLauncher(
|
||||
context,
|
||||
scope
|
||||
) { uri, file ->
|
||||
onUpdateBanner?.invoke(uri, file, context)
|
||||
}
|
||||
val parentScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
package com.aiosman.riderpro.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import coil.ImageLoader
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import coil.request.CachePolicy
|
||||
import com.aiosman.riderpro.data.api.AuthInterceptor
|
||||
import com.aiosman.riderpro.data.api.getUnsafeOkHttpClient
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object Utils {
|
||||
@@ -61,4 +67,32 @@ object Utils {
|
||||
fun getCurrentLanguage(): String {
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user