diff --git a/app/src/main/java/com/aiosman/riderpro/Const.kt b/app/src/main/java/com/aiosman/riderpro/Const.kt index 1852e4d..9e1edf3 100644 --- a/app/src/main/java/com/aiosman/riderpro/Const.kt +++ b/app/src/main/java/com/aiosman/riderpro/Const.kt @@ -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) } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/account/edit2.kt b/app/src/main/java/com/aiosman/riderpro/ui/account/edit2.kt index 8d6b0cb..1e33e99 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/account/edit2.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/account/edit2.kt @@ -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(null) } - var bannerImageUrl by remember { mutableStateOf(null) } + var imageFile by remember { mutableStateOf(null) } var profile by remember { mutableStateOf( null @@ -87,6 +72,29 @@ fun AccountEditScreen2() { val navController = LocalNavController.current val scope = rememberCoroutineScope() val context = LocalContext.current + var usernameError by remember { mutableStateOf(null) } + var bioError by remember { mutableStateOf(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) } } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/PickupAndCompressLauhcner.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/PickupAndCompressLauhcner.kt new file mode 100644 index 0000000..6d1874f --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/PickupAndCompressLauhcner.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/composables/form/FormTextInput.kt b/app/src/main/java/com/aiosman/riderpro/ui/composables/form/FormTextInput.kt new file mode 100644 index 0000000..6f55291 --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/composables/form/FormTextInput.kt @@ -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) + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt index 6aabe29..db800c7 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/MyProfileViewModel.kt @@ -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) } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/ProfileV3.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/ProfileV3.kt index 0d48f50..47a6580 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/ProfileV3.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/ProfileV3.kt @@ -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)) } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/ProfileWrap.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/ProfileWrap.kt index 1a11a42..23db28f 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/ProfileWrap.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/ProfileWrap.kt @@ -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 { diff --git a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profilev2.kt b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profilev2.kt index 6476fa1..6070108 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profilev2.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/index/tabs/profile/Profilev2.kt @@ -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 { diff --git a/app/src/main/java/com/aiosman/riderpro/utils/Utils.kt b/app/src/main/java/com/aiosman/riderpro/utils/Utils.kt index ddec716..b5cbdb2 100644 --- a/app/src/main/java/com/aiosman/riderpro/utils/Utils.kt +++ b/app/src/main/java/com/aiosman/riderpro/utils/Utils.kt @@ -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 + } } \ No newline at end of file