WIP: se agrega configurar foto de perfil y otro trabajo en progreso
This commit is contained in:
parent
b019c79bcb
commit
0e553e05e6
@ -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")
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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>>
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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 = {}
|
||||
)
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user