diff --git a/app/src/main/java/com/isolaatti/images/common/data/remote/ImagesApi.kt b/app/src/main/java/com/isolaatti/images/common/data/remote/ImagesApi.kt index 8ef1362..6332bef 100644 --- a/app/src/main/java/com/isolaatti/images/common/data/remote/ImagesApi.kt +++ b/app/src/main/java/com/isolaatti/images/common/data/remote/ImagesApi.kt @@ -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 @POST("images/delete/delete_many") diff --git a/app/src/main/java/com/isolaatti/profile/Module.kt b/app/src/main/java/com/isolaatti/profile/Module.kt index c02ed11..525aefc 100644 --- a/app/src/main/java/com/isolaatti/profile/Module.kt +++ b/app/src/main/java/com/isolaatti/profile/Module.kt @@ -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) } diff --git a/app/src/main/java/com/isolaatti/profile/data/remote/ProfileApi.kt b/app/src/main/java/com/isolaatti/profile/data/remote/ProfileApi.kt index 45a51cc..1548e04 100644 --- a/app/src/main/java/com/isolaatti/profile/data/remote/ProfileApi.kt +++ b/app/src/main/java/com/isolaatti/profile/data/remote/ProfileApi.kt @@ -12,8 +12,6 @@ interface ProfileApi { @GET("Fetch/UserProfile/{userId}") fun userProfile(@Path("userId") userId: Int): Call - @POST("EditProfile/SetProfilePhoto") - fun setProfileImage(@Query("imageId") imageId: String): Call @POST("EditProfile/UpdateProfile") fun updateProfile(@Body updateProfileDto: UpdateProfileDto): Call diff --git a/app/src/main/java/com/isolaatti/profile/data/repository/ProfileRepositoryImpl.kt b/app/src/main/java/com/isolaatti/profile/data/repository/ProfileRepositoryImpl.kt index 26ac711..3c9df30 100644 --- a/app/src/main/java/com/isolaatti/profile/data/repository/ProfileRepositoryImpl.kt +++ b/app/src/main/java/com/isolaatti/profile/data/repository/ProfileRepositoryImpl.kt @@ -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> = flow { try { val result = profileApi.userProfile(userId).awaitResponse() @@ -37,16 +51,41 @@ class ProfileRepositoryImpl @Inject constructor( } } - override fun setProfileImage(image: RemoteImage): Flow> = flow { - + override fun setProfileImage(image: LocalImage): Flow> = flow { + emit(Resource.Loading()) try { - val result = profileApi.setProfileImage(image.id).awaitResponse() + var imageInputStream: InputStream? = null - if(result.isSuccessful) { - emit(Resource.Success(true)) - } else { - emit(Resource.Error(Resource.Error.mapErrorCode(result.code()))) + 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) { + 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)) diff --git a/app/src/main/java/com/isolaatti/profile/domain/ProfileRepository.kt b/app/src/main/java/com/isolaatti/profile/domain/ProfileRepository.kt index 6012360..315ce83 100644 --- a/app/src/main/java/com/isolaatti/profile/domain/ProfileRepository.kt +++ b/app/src/main/java/com/isolaatti/profile/domain/ProfileRepository.kt @@ -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> - fun setProfileImage(image: RemoteImage): Flow> + fun setProfileImage(image: LocalImage): Flow> fun updateProfile(newDisplayName: String, newDescription: String): Flow> diff --git a/app/src/main/java/com/isolaatti/profile/domain/use_case/SetProfileImage.kt b/app/src/main/java/com/isolaatti/profile/domain/use_case/SetProfileImage.kt index d12fbf6..1bda718 100644 --- a/app/src/main/java/com/isolaatti/profile/domain/use_case/SetProfileImage.kt +++ b/app/src/main/java/com/isolaatti/profile/domain/use_case/SetProfileImage.kt @@ -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> = profileRepository.setProfileImage(image) + operator fun invoke(image: LocalImage): Flow> = profileRepository.setProfileImage(image) } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt b/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt index c90401d..e361430 100644 --- a/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt +++ b/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt @@ -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 = MutableStateFlow(false) + val showConfirmChangeProfilePictureBottomSheet: MutableStateFlow = MutableStateFlow(false) + + val newProfileImage: MutableStateFlow = MutableStateFlow(null) + + val uploadingProfilePicture: MutableStateFlow = MutableStateFlow(false) + val errorProfilePicture: MutableStateFlow?> = MutableStateFlow(null) // runs the lists of "Runnable" one by one and clears list. After this is executed, // caller should report as handled @@ -91,11 +103,29 @@ class ProfileViewModel @Inject constructor( } } - fun setProfileImage(image: RemoteImage) { + fun setProfileImage() { viewModelScope.launch { - setProfileImageUC(image).onEach { - _profile.postValue(_profile.value?.copy(profileImageId = image.id)) - }.flowOn(Dispatchers.IO).launchIn(this) + newProfileImage.value?.let { + setProfileImageUC(LocalImage(it)).onEach { resource -> + when(resource) { + is Resource.Error -> { + errorProfilePicture.value = resource + uploadingProfilePicture.value = false + } + is Resource.Loading -> { + errorProfilePicture.value = null + uploadingProfilePicture.value = true + } + is Resource.Success -> { + errorProfilePicture.value = null + uploadingProfilePicture.value = false + showConfirmChangeProfilePictureBottomSheet.value = false + _profile.postValue(_profile.value?.copy(profileImageId = resource.data?.id)) + } + } + + }.flowOn(Dispatchers.IO).launchIn(this) + } } } @@ -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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt b/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt index 77b453b..6c69a2b 100644 --- a/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt +++ b/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt @@ -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 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 diff --git a/app/src/main/java/com/isolaatti/profile/ui/components/ConfirmChangeProfilePictureBottomSheet.kt b/app/src/main/java/com/isolaatti/profile/ui/components/ConfirmChangeProfilePictureBottomSheet.kt new file mode 100644 index 0000000..a424f2f --- /dev/null +++ b/app/src/main/java/com/isolaatti/profile/ui/components/ConfirmChangeProfilePictureBottomSheet.kt @@ -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)) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/profile/ui/components/ProfileHeader.kt b/app/src/main/java/com/isolaatti/profile/ui/components/ProfileHeader.kt index 4fb65e9..927db55 100644 --- a/app/src/main/java/com/isolaatti/profile/ui/components/ProfileHeader.kt +++ b/app/src/main/java/com/isolaatti/profile/ui/components/ProfileHeader.kt @@ -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 = {} ) } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/profile/ui/components/ProfilePictureBottomSheet.kt b/app/src/main/java/com/isolaatti/profile/ui/components/ProfilePictureBottomSheet.kt new file mode 100644 index 0000000..2897654 --- /dev/null +++ b/app/src/main/java/com/isolaatti/profile/ui/components/ProfilePictureBottomSheet.kt @@ -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)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b70e16b..73157f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -230,6 +230,10 @@ Profile Add post button When %s posts something you will see it here + Old picture + Change profile picture? + Uploading photo + You have no audio description Spam Explicit content