修改个人资料修改逻辑

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

View File

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

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

View File

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

View File

@@ -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 {

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.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 {

View File

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