WIP: se agrega configurar foto de perfil y otro trabajo en progreso

This commit is contained in:
erik 2025-03-17 22:33:10 -06:00
parent b019c79bcb
commit 0e553e05e6
12 changed files with 503 additions and 43 deletions

View File

@ -21,8 +21,8 @@ interface ImagesApi {
@POST("images/create")
@Multipart
fun postImage(@Part file: MultipartBody.Part,
@Part postId: MultipartBody.Part,
@Part setAsProfile: MultipartBody.Part? = null,
@Part postId: MultipartBody.Part? = null,
@Query("setAsProfile") setAsProfile: Boolean = false,
@Part squadId: MultipartBody.Part? = null): Call<ImageDto>
@POST("images/delete/delete_many")

View File

@ -1,8 +1,10 @@
package com.isolaatti.profile
import android.content.ContentResolver
import com.isolaatti.auth.data.local.UserInfoDao
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.database.AppDatabase
import com.isolaatti.images.common.data.remote.ImagesApi
import com.isolaatti.profile.data.remote.ProfileApi
import com.isolaatti.profile.data.repository.ProfileRepositoryImpl
import com.isolaatti.profile.domain.ProfileRepository
@ -25,8 +27,8 @@ class Module {
}
@Provides
fun provideProfileRepository(profileApi: ProfileApi, userInfoDao: UserInfoDao): ProfileRepository {
return ProfileRepositoryImpl(profileApi, userInfoDao)
fun provideProfileRepository(profileApi: ProfileApi, userInfoDao: UserInfoDao, imagesApi: ImagesApi, contentResolver: ContentResolver): ProfileRepository {
return ProfileRepositoryImpl(profileApi, userInfoDao, imagesApi, contentResolver)
}

View File

@ -12,8 +12,6 @@ interface ProfileApi {
@GET("Fetch/UserProfile/{userId}")
fun userProfile(@Path("userId") userId: Int): Call<UserProfileDto>
@POST("EditProfile/SetProfilePhoto")
fun setProfileImage(@Query("imageId") imageId: String): Call<Void>
@POST("EditProfile/UpdateProfile")
fun updateProfile(@Body updateProfileDto: UpdateProfileDto): Call<UserProfileDto>

View File

@ -1,8 +1,14 @@
package com.isolaatti.profile.data.repository
import android.content.ContentResolver
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.util.Log
import com.isolaatti.auth.data.local.UserInfoDao
import com.isolaatti.auth.data.local.UserInfoEntity
import com.isolaatti.images.common.data.remote.ImagesApi
import com.isolaatti.images.common.domain.entity.LocalImage
import com.isolaatti.images.common.domain.entity.RemoteImage
import com.isolaatti.profile.data.remote.ProfileApi
import com.isolaatti.profile.data.remote.UpdateProfileDto
@ -11,12 +17,20 @@ import com.isolaatti.profile.domain.entity.UserProfile
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.awaitResponse
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import javax.inject.Inject
class ProfileRepositoryImpl @Inject constructor(
private val profileApi: ProfileApi,
private val userInfoDao: UserInfoDao) : ProfileRepository {
private val userInfoDao: UserInfoDao,
private val imagesApi: ImagesApi,
private val contentResolver: ContentResolver
) : ProfileRepository {
override fun getProfile(userId: Int): Flow<Resource<UserProfile>> = flow {
try {
val result = profileApi.userProfile(userId).awaitResponse()
@ -37,16 +51,41 @@ class ProfileRepositoryImpl @Inject constructor(
}
}
override fun setProfileImage(image: RemoteImage): Flow<Resource<Boolean>> = flow {
override fun setProfileImage(image: LocalImage): Flow<Resource<RemoteImage>> = flow {
emit(Resource.Loading())
try {
val result = profileApi.setProfileImage(image.id).awaitResponse()
var imageInputStream: InputStream? = null
contentResolver.openInputStream(image.uri).use {
imageInputStream = it
val bitmap = BitmapFactory.decodeStream(imageInputStream)
val outputStream = ByteArrayOutputStream()
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
bitmap.compress(Bitmap.CompressFormat.WEBP, 50, outputStream)
} else {
bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 50, outputStream)
}
val result = imagesApi
.postImage(
file = MultipartBody.Part.createFormData("file", "profile-${System.currentTimeMillis()}",outputStream.toByteArray().toRequestBody()),
setAsProfile = true
)
.awaitResponse()
if(result.isSuccessful) {
emit(Resource.Success(true))
val imageDto = result.body()
emit(Resource.Success(RemoteImage(imageDto!!.id)))
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
}
}
} catch (e: Exception) {
Log.e("ProfileRepositoryImpl", e.message.toString())
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))

View File

@ -1,5 +1,7 @@
package com.isolaatti.profile.domain
import com.isolaatti.images.common.data.remote.ImageDto
import com.isolaatti.images.common.domain.entity.LocalImage
import com.isolaatti.images.common.domain.entity.RemoteImage
import com.isolaatti.profile.domain.entity.UserProfile
import com.isolaatti.utils.Resource
@ -8,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
interface ProfileRepository {
fun getProfile(userId: Int): Flow<Resource<UserProfile>>
fun setProfileImage(image: RemoteImage): Flow<Resource<Boolean>>
fun setProfileImage(image: LocalImage): Flow<Resource<RemoteImage>>
fun updateProfile(newDisplayName: String, newDescription: String): Flow<Resource<Boolean>>

View File

@ -1,5 +1,6 @@
package com.isolaatti.profile.domain.use_case
import com.isolaatti.images.common.domain.entity.LocalImage
import com.isolaatti.images.common.domain.entity.RemoteImage
import com.isolaatti.profile.domain.ProfileRepository
import com.isolaatti.utils.Resource
@ -7,5 +8,5 @@ import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class SetProfileImage @Inject constructor(private val profileRepository: ProfileRepository) {
operator fun invoke(image: RemoteImage): Flow<Resource<Boolean>> = profileRepository.setProfileImage(image)
operator fun invoke(image: LocalImage): Flow<Resource<RemoteImage>> = profileRepository.setProfileImage(image)
}

View File

@ -1,10 +1,16 @@
package com.isolaatti.profile.presentation
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.isolaatti.followers.domain.FollowingState
import com.isolaatti.images.common.domain.entity.LocalImage
import com.isolaatti.images.common.domain.entity.RemoteImage
import com.isolaatti.posting.posts.presentation.PostListingViewModelBase
import com.isolaatti.profile.domain.entity.UserProfile
@ -45,6 +51,12 @@ class ProfileViewModel @Inject constructor(
val isRefreshing: MutableStateFlow<Boolean> = MutableStateFlow(false)
val showConfirmChangeProfilePictureBottomSheet: MutableStateFlow<Boolean> = MutableStateFlow(false)
val newProfileImage: MutableStateFlow<Uri?> = MutableStateFlow(null)
val uploadingProfilePicture: MutableStateFlow<Boolean> = MutableStateFlow(false)
val errorProfilePicture: MutableStateFlow<Resource.Error<RemoteImage>?> = MutableStateFlow(null)
// runs the lists of "Runnable" one by one and clears list. After this is executed,
// caller should report as handled
@ -91,13 +103,31 @@ class ProfileViewModel @Inject constructor(
}
}
fun setProfileImage(image: RemoteImage) {
fun setProfileImage() {
viewModelScope.launch {
setProfileImageUC(image).onEach {
_profile.postValue(_profile.value?.copy(profileImageId = image.id))
newProfileImage.value?.let {
setProfileImageUC(LocalImage(it)).onEach { resource ->
when(resource) {
is Resource.Error<RemoteImage> -> {
errorProfilePicture.value = resource
uploadingProfilePicture.value = false
}
is Resource.Loading<RemoteImage> -> {
errorProfilePicture.value = null
uploadingProfilePicture.value = true
}
is Resource.Success<RemoteImage> -> {
errorProfilePicture.value = null
uploadingProfilePicture.value = false
showConfirmChangeProfilePictureBottomSheet.value = false
_profile.postValue(_profile.value?.copy(profileImageId = resource.data?.id))
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}
fun followUser() {
val currentProfile = _profile.value ?: return
@ -181,4 +211,18 @@ class ProfileViewModel @Inject constructor(
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun hideConfirmPictureSheet() {
viewModelScope.launch {
showConfirmChangeProfilePictureBottomSheet.value = false
newProfileImage.value = null
}
}
fun onProfilePictureSelected(uri: Uri) {
viewModelScope.launch {
showConfirmChangeProfilePictureBottomSheet.value = true
newProfileImage.value = uri
}
}
}

View File

@ -2,12 +2,17 @@ package com.isolaatti.profile.ui
import android.content.ComponentName
import android.content.Intent
import android.media.MediaRecorder
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
@ -23,10 +28,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
@ -34,6 +41,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@ -45,6 +53,8 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
@ -54,10 +64,13 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.navigation.findNavController
import coil3.toUri
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.common.util.concurrent.ListenableFuture
import com.isolaatti.BuildConfig
import com.isolaatti.MyApplication
import com.isolaatti.R
import com.isolaatti.audio.common.components.AudioRecorder
import com.isolaatti.audio.player.MediaService
import com.isolaatti.common.Dialogs
import com.isolaatti.common.ErrorMessageViewModel
@ -67,22 +80,30 @@ import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOpt
import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
import com.isolaatti.followers.domain.FollowingState
import com.isolaatti.hashtags.ui.HashtagsPostsActivity
import com.isolaatti.images.common.domain.entity.LocalImage
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
import com.isolaatti.posting.posts.components.PostComponent
import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.posting.posts.presentation.CreatePostContract
import com.isolaatti.posting.posts.ui.CreatePostActivity
import com.isolaatti.posting.posts.ui.PostInfoActivity
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
import com.isolaatti.profile.domain.entity.UserProfile
import com.isolaatti.profile.presentation.EditProfileContract
import com.isolaatti.profile.presentation.ProfileViewModel
import com.isolaatti.profile.ui.components.ConfirmChangeProfilePictureBottomSheet
import com.isolaatti.profile.ui.components.ProfileHeader
import com.isolaatti.profile.ui.components.ProfilePictureBottomSheet
import com.isolaatti.profile.ui.components.ProfilePictureBottomSheetActions
import com.isolaatti.profile.ui.components.ProfilePictureState
import com.isolaatti.reports.data.ContentType
import com.isolaatti.reports.ui.NewReportBottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
import java.io.File
import java.util.Calendar
@AndroidEntryPoint
class ProfileMainFragment : Fragment() {
@ -91,6 +112,19 @@ class ProfileMainFragment : Fragment() {
val errorViewModel: ErrorMessageViewModel by activityViewModels()
private var userId: Int? = null
private var cameraPhotoUri: Uri? = null
private fun makePhotoUri(): Uri {
val cacheFile = File(requireContext().filesDir, "temp_picture_${Calendar.getInstance().timeInMillis}")
return FileProvider.getUriForFile(requireContext(), "${MyApplication.myApp.packageName}.provider", cacheFile)
}
private val takePhotoLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) {
if(it && cameraPhotoUri != null) {
viewModel.onProfilePictureSelected(cameraPhotoUri!!)
}
}
private val createDiscussion = registerForActivityResult(CreatePostContract()) {
if(it == null)
return@registerForActivityResult
@ -106,7 +140,11 @@ class ProfileMainFragment : Fragment() {
}
}
private val choosePictureLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
if(it != null) {
viewModel.onProfilePictureSelected(it)
}
}
private fun getData() {
@ -119,6 +157,7 @@ class ProfileMainFragment : Fragment() {
private lateinit var mediaControllerFuture: ListenableFuture<MediaController>
private lateinit var mediaController: MediaController
private lateinit var mediaRecorder: MediaRecorder
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -131,6 +170,8 @@ class ProfileMainFragment : Fragment() {
}
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreateView(
inflater: LayoutInflater,
@ -174,8 +215,25 @@ class ProfileMainFragment : Fragment() {
}
}
/* Profile picture bottom sheet */
var showProfilePictureBottomSheet by remember { mutableStateOf(false) }
val profilePictureSheetState = rememberModalBottomSheetState()
var profilePictureState by remember { mutableStateOf(ProfilePictureState()) }
val scope = rememberCoroutineScope()
val showConfirmChangeProfilePicture by viewModel.showConfirmChangeProfilePictureBottomSheet.collectAsState()
val newProfilePicture by viewModel.newProfileImage.collectAsState()
val uploadingProfilePicture by viewModel.uploadingProfilePicture.collectAsState()
var showAudioRecorder by remember { mutableStateOf(false) }
LaunchedEffect(profile) {
showAddPostButton = profile?.isUserItself == true
profilePictureState = ProfilePictureState(
showViewImage = profile?.profileImage != null,
showChangeProfileImage = profile?.isUserItself == true,
showRemoveImage = profile?.isUserItself == true
)
}
val followingText = when(followingState) {
@ -199,7 +257,6 @@ class ProfileMainFragment : Fragment() {
val posts by viewModel.posts.collectAsState()
val loadingProfile by viewModel.loadingProfile.collectAsState()
val playingAudio by remember { mutableStateOf(true) }
IsolaattiTheme {
Scaffold(
topBar = {
@ -245,6 +302,80 @@ class ProfileMainFragment : Fragment() {
}
) { insets ->
if(showProfilePictureBottomSheet) {
ProfilePictureBottomSheet(
sheetState = profilePictureSheetState,
onDismissRequest = {
showProfilePictureBottomSheet = false
},
onAction = { action ->
when(action) {
ProfilePictureBottomSheetActions.ViewImage -> {
profile?.profileImage?.let {
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(it))
}
}
ProfilePictureBottomSheetActions.ChangeImage -> {
choosePictureLauncher
.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
ProfilePictureBottomSheetActions.RemoveImage -> {
showRemoveProfileImageDialog()
}
ProfilePictureBottomSheetActions.TakeAPicture -> {
cameraPhotoUri = makePhotoUri()
takePhotoLauncher.launch(cameraPhotoUri)
}
}
scope.launch { profilePictureSheetState.hide() }.invokeOnCompletion {
if (!profilePictureSheetState.isVisible) {
showProfilePictureBottomSheet = false
}
}
},
state = profilePictureState
)
}
if(showConfirmChangeProfilePicture) {
newProfilePicture?.let {
ConfirmChangeProfilePictureBottomSheet(
onDismiss = {
viewModel.hideConfirmPictureSheet()
},
oldImage = profile?.profileImage?.imageUrl?.toUri(),
newImage = it,
onAccept = {
viewModel.setProfileImage()
},
loading = uploadingProfilePicture
)
}
}
if(showAudioRecorder) {
ModalBottomSheet(onDismissRequest = { showAudioRecorder = false }) {
AudioRecorder(
onDismiss = {},
onPlay = TODO(),
onPause = TODO(),
onStopRecording = TODO(),
onPauseRecording = TODO(),
onStartRecording = TODO(),
isRecording = TODO(),
recordIsStopped = TODO(),
recordIsPaused = TODO(),
isPlaying = TODO(),
isLoading = TODO(),
durationSeconds = TODO(),
position = TODO(),
onSeek = TODO(),
modifier = TODO()
)
}
}
PullToRefreshBox(
isRefreshing,
onRefresh = {
@ -273,9 +404,7 @@ class ProfileMainFragment : Fragment() {
followerCount = it.numberOfFollowers,
loading = loadingProfile,
onImageClick = {
optionsViewModel.setOptions(Options.PROFILE_PHOTO_OPTIONS, CALLER_ID, it)
val fragment = BottomSheetPostOptionsFragment()
fragment.show(parentFragmentManager, BottomSheetPostOptionsFragment.TAG)
showProfilePictureBottomSheet = true
},
onFollowersClick = {
findNavController().navigate(ProfileMainFragmentDirections.actionDiscussionsFragmentToMainFollowersFragment(it.userId))
@ -290,6 +419,11 @@ class ProfileMainFragment : Fragment() {
showProfileAudioProgress = true,
onEditProfileClick = {
editProfile.launch(it)
},
onAddAudioDescription = {
mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(requireContext()) else MediaRecorder()
showAudioRecorder = true
}
)
}
@ -376,23 +510,6 @@ class ProfileMainFragment : Fragment() {
Log.d("ProfileMainFragment", optionClicked.toString())
when(optionClicked.optionsId) {
Options.PROFILE_PHOTO_OPTIONS -> {
val profile = optionClicked.payload as? UserProfile
when(optionClicked.optionId) {
Options.Option.OPTION_PROFILE_PHOTO_CHANGE_PHOTO -> {
}
Options.Option.OPTION_PROFILE_PHOTO_REMOVE_PHOTO -> {
showRemoveProfileImageDialog()
}
Options.Option.OPTION_PROFILE_PHOTO_VIEW_PHOTO -> {
profile?.profileImage?.let {
PictureViewerActivity.startActivityWithImages(requireContext(), arrayOf(it))
}
}
}
optionsViewModel.handle()
}
Options.POST_OPTIONS -> {
// post id should come as payload
val post = optionClicked.payload as? Post ?: return@observe

View File

@ -0,0 +1,120 @@
package com.isolaatti.profile.ui.components
import android.net.Uri
import androidx.compose.foundation.layout.Arrangement
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.isolaatti.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConfirmChangeProfilePictureBottomSheet(
onDismiss: () -> Unit,
oldImage: Uri?,
newImage: Uri,
onAccept: () -> Unit,
loading: Boolean
) {
ModalBottomSheet(onDismissRequest = onDismiss) {
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(R.string.change_profile_photo_question),
fontSize = 20.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(16.dp).fillMaxWidth()
)
if(!loading) {
Box(Modifier.padding(8.dp).fillMaxWidth()) {
if(oldImage != null) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
AsyncImage(
model = oldImage,
contentDescription = stringResource(R.string.old_picture),
modifier = Modifier.size(70.dp)
.clip(RoundedCornerShape(50))
)
Icon(Icons.Default.ArrowForward, contentDescription = null, modifier = Modifier.size(40.dp))
AsyncImage(
model = newImage,
contentDescription = stringResource(R.string.old_picture),
modifier = Modifier
.clip(RoundedCornerShape(50))
.size(70.dp)
)
}
} else {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
){
AsyncImage(
model = newImage,
contentDescription = stringResource(R.string.old_picture),
modifier = Modifier
.clip(RoundedCornerShape(50))
.size(70.dp)
)
}
}
}
Row(modifier = Modifier.padding(8.dp).fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
FilledTonalButton(
onClick = {
onDismiss()
}
) {
Text(stringResource(R.string.cancel))
}
Spacer(Modifier.width(8.dp))
Button(
onClick = {
onAccept()
}
) {
Text(stringResource(R.string.accept))
}
}
} else {
CircularProgressIndicator()
Text(stringResource(R.string.uploading_photo))
}
}
}
}

View File

@ -6,7 +6,9 @@ import androidx.compose.animation.core.animateValue
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -18,8 +20,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -70,6 +74,7 @@ fun ProfileHeader(
onFollowersClick: () -> Unit,
onEditProfileClick: () -> Unit,
onFollowChange: (Boolean) -> Unit,
onAddAudioDescription: () -> Unit,
loading: Boolean = false,
isOwnUser: Boolean,
followingThisUser: Boolean,
@ -125,6 +130,27 @@ fun ProfileHeader(
Text(stringResource(R.string.go_to_followers_btn_text, followerCount, followingCount))
}
when {
isOwnUser && audio == null -> {
OutlinedCard(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
Column(modifier = Modifier.padding(8.dp).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Text(stringResource(R.string.you_have_now_audio))
FilledTonalButton(
onClick = {
}
) {
Text(stringResource(R.string.set_audio_description))
}
}
}
}
audio != null -> {
}
}
when {
isOwnUser -> {
OutlinedButton(onClick = onEditProfileClick) {
@ -168,6 +194,7 @@ fun ProfileHeaderPreview() {
showProfileAudioProgress = true,
onFollowersClick = {},
onFollowChange = {},
onEditProfileClick = {}
onEditProfileClick = {},
onAddAudioDescription = {}
)
}

View File

@ -0,0 +1,106 @@
package com.isolaatti.profile.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.isolaatti.R
data class ProfilePictureState(
val showViewImage: Boolean = true,
val showChangeProfileImage: Boolean = true,
val showRemoveImage: Boolean = true
)
enum class ProfilePictureBottomSheetActions {
ViewImage, ChangeImage, RemoveImage, TakeAPicture
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProfilePictureBottomSheet(
onDismissRequest: () -> Unit,
sheetState: SheetState = rememberModalBottomSheetState(),
state: ProfilePictureState,
onAction: (ProfilePictureBottomSheetActions) -> Unit
) {
ModalBottomSheet(onDismissRequest = onDismissRequest, sheetState = sheetState) {
Column {
Text(
text = stringResource(R.string.profile_photo),
fontSize = 20.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(16.dp).fillMaxWidth()
)
if(state.showViewImage) {
TextButton(
onClick = { onAction(ProfilePictureBottomSheetActions.ViewImage) },
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.view_photo))
}
}
if(state.showChangeProfileImage) {
Box {
var expanded by remember { mutableStateOf(false) }
TextButton(
onClick = {
expanded = true
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.change_profile_photo))
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.upload_a_picture)) },
onClick = {
onAction(ProfilePictureBottomSheetActions.ChangeImage)
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.take_a_photo)) },
onClick = {
onAction(ProfilePictureBottomSheetActions.TakeAPicture)
}
)
}
}
}
if(state.showRemoveImage) {
TextButton(
onClick = { onAction(ProfilePictureBottomSheetActions.RemoveImage) },
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.remove_photo))
}
}
}
}
}

View File

@ -230,6 +230,10 @@
<string name="profile">Profile</string>
<string name="add_post_button_desc">Add post button</string>
<string name="you_will_see_what_user_posts_here">When %s posts something you will see it here</string>
<string name="old_picture">Old picture</string>
<string name="change_profile_photo_question">Change profile picture?</string>
<string name="uploading_photo">Uploading photo</string>
<string name="you_have_now_audio">You have no audio description</string>
<string-array name="report_reasons">
<item>Spam</item>
<item>Explicit content</item>