diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c21045e4..4a913487 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -152,6 +152,7 @@ dependencies {
implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
implementation("com.google.firebase:firebase-auth")
implementation("com.google.android.gms:play-services-auth:20.7.0")
+ implementation("com.google.firebase:firebase-storage")
// Crashlytics
implementation("com.google.firebase:firebase-crashlytics")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 53c2a7f5..4a574e33 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -44,6 +44,10 @@
android:name=".data.receiver.location.LocationUpdateReceiver"
android:exported="false"/>
+
\ No newline at end of file
diff --git a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileScreen.kt b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileScreen.kt
index 61080a26..c1047ef1 100644
--- a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileScreen.kt
+++ b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileScreen.kt
@@ -134,7 +134,8 @@ private fun EditProfileScreenContent(modifier: Modifier) {
dismissProfileChooser = {
viewModel.showProfileChooser(false)
},
- state.showProfileChooser
+ state.showProfileChooser,
+ state.isImageUploadInProgress
)
Spacer(modifier = Modifier.height(35.dp))
diff --git a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileViewModel.kt b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileViewModel.kt
index b43b335a..1a78cdb3 100644
--- a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileViewModel.kt
+++ b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/EditProfileViewModel.kt
@@ -1,5 +1,6 @@
package com.canopas.yourspace.ui.flow.settings.profile
+import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.canopas.yourspace.data.models.user.ApiUser
@@ -10,12 +11,12 @@ import com.canopas.yourspace.data.service.auth.AuthService
import com.canopas.yourspace.data.utils.AppDispatcher
import com.canopas.yourspace.ui.navigation.AppDestinations
import com.canopas.yourspace.ui.navigation.AppNavigator
+import com.google.firebase.storage.FirebaseStorage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
-import java.io.File
import javax.inject.Inject
@HiltViewModel
@@ -45,7 +46,7 @@ class EditProfileViewModel @Inject constructor(
lastName = user?.last_name,
email = user?.email,
phone = user?.phone,
- profileUrl = null,
+ profileUrl = user?.profile_image,
enablePhone = user?.auth_type != LOGIN_TYPE_PHONE,
enableEmail = user?.auth_type != LOGIN_TYPE_GOOGLE
)
@@ -102,9 +103,41 @@ class EditProfileViewModel @Inject constructor(
_state.value = _state.value.copy(showProfileChooser = show)
}
- fun onProfileImageChanged(profileUrl: File?) {
- _state.value = _state.value.copy(profileUrl = profileUrl?.path)
- onChange()
+ fun onProfileImageChanged(profileUri: Uri?) {
+ profileUri?.let { uri ->
+ uploadProfileImage(uri)
+ } ?: run {
+ _state.value = _state.value.copy(profileUrl = null)
+ onChange()
+ }
+ }
+
+ private fun uploadProfileImage(uri: Uri) = viewModelScope.launch(appDispatcher.IO) {
+ try {
+ val storage = FirebaseStorage.getInstance()
+ val storageRef = storage.reference
+ val fileName = "IMG_${System.currentTimeMillis()}.jpg"
+ val imageRef = storageRef.child("profile_images/${user?.id}/$fileName")
+ val uploadTask = imageRef.putFile(uri)
+ uploadTask.addOnProgressListener {
+ _state.value = _state.value.copy(isImageUploadInProgress = true)
+ }.addOnSuccessListener {
+ imageRef.downloadUrl.addOnSuccessListener { uri ->
+ _state.value = _state.value.copy(
+ profileUrl = uri.toString(),
+ isImageUploadInProgress = false
+ )
+ onChange()
+ }
+ }.addOnFailureListener {
+ Timber.e(it, "Failed to upload profile image")
+ _state.value = _state.value.copy(profileUrl = null, isImageUploadInProgress = false)
+ onChange()
+ }
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to upload profile image")
+ _state.emit(_state.value.copy(isImageUploadInProgress = false, error = e.message))
+ }
}
fun onFirstNameChanged(firstName: String) {
@@ -167,5 +200,6 @@ data class EditProfileState(
val lastName: String? = null,
val email: String? = null,
val phone: String? = null,
- val profileUrl: String? = null
+ val profileUrl: String? = null,
+ val isImageUploadInProgress: Boolean = false
)
diff --git a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/component/UserProfileView.kt b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/component/UserProfileView.kt
index bfef1bf0..700de323 100644
--- a/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/component/UserProfileView.kt
+++ b/app/src/main/java/com/canopas/yourspace/ui/flow/settings/profile/component/UserProfileView.kt
@@ -2,6 +2,7 @@ package com.canopas.yourspace.ui.flow.settings.profile.component
import android.graphics.Bitmap
import android.net.Uri
+import android.provider.MediaStore
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.launch
@@ -18,9 +19,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@@ -28,7 +31,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -46,38 +48,32 @@ import com.canhub.cropper.CropImageContractOptions
import com.canhub.cropper.CropImageOptions
import com.canopas.yourspace.R
import com.canopas.yourspace.data.models.user.ApiUser
+import com.canopas.yourspace.ui.component.AppProgressIndicator
import com.canopas.yourspace.ui.component.motionClickEvent
import com.canopas.yourspace.ui.flow.settings.ProfileImageView
import com.canopas.yourspace.ui.theme.AppTheme
-import java.io.File
-import java.io.FileOutputStream
+import java.io.ByteArrayOutputStream
@Composable
fun UserProfileView(
modifier: Modifier,
profileUrl: String?,
- onProfileChanged: (File?) -> Unit,
+ onProfileChanged: (Uri?) -> Unit,
onProfileImageClicked: () -> Unit,
dismissProfileChooser: () -> Unit,
- showProfileChooser: Boolean = false
+ showProfileChooser: Boolean = false,
+ isImageUploading: Boolean = false
) {
val context = LocalContext.current
- val scope = rememberCoroutineScope()
var imageUri by remember { mutableStateOf(null) }
val imageCropLauncher = rememberLauncherForActivityResult(contract = CropImageContract()) { result ->
- result.uriContent?.let {
- imageUri = it
- val fileName = "IMG_${System.currentTimeMillis()}.jpg"
- val file = File(context.cacheDir, "images/$fileName")
- val inputStream = context.contentResolver.openInputStream(imageUri!!)
-
- val outputStream = FileOutputStream(file)
- inputStream?.copyTo(outputStream)
- inputStream?.close()
- outputStream.close()
- onProfileChanged(file)
+ if (result.isSuccessful) {
+ result.uriContent?.let {
+ imageUri = it
+ onProfileChanged(imageUri)
+ }
}
}
@@ -92,19 +88,16 @@ fun UserProfileView(
val cameraLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.TakePicturePreview()) { bitmap ->
-
bitmap?.let {
- val fileName = "IMG_${System.currentTimeMillis()}.jpg"
- val file = File(context.cacheDir, "images/$fileName")
- try {
- val out = FileOutputStream(file)
- it.compress(Bitmap.CompressFormat.JPEG, 100, out)
- out.flush()
- out.close()
- } catch (e: Exception) {
- e.printStackTrace()
- }
- onProfileChanged(file)
+ val bytes = ByteArrayOutputStream()
+ it.compress(Bitmap.CompressFormat.JPEG, 100, bytes)
+ val path: String = MediaStore.Images.Media.insertImage(
+ context.contentResolver,
+ it,
+ "Title",
+ null
+ )
+ onProfileChanged(Uri.parse(path))
}
}
@@ -135,7 +128,9 @@ fun UserProfileView(
painter = rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = setProfile).build()
),
- modifier = Modifier.fillMaxSize()
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(CircleShape)
.border(1.dp, AppTheme.colorScheme.textPrimary, CircleShape)
.background(AppTheme.colorScheme.containerHigh, CircleShape)
.padding(if (profileUrl == null) 32.dp else 0.dp),
@@ -143,14 +138,40 @@ fun UserProfileView(
contentDescription = "ProfileImage"
)
-// Image(
-// painter = painterResource(id = R.drawable.ic_edit_user_profile),
-// contentDescription = null,
-// modifier = Modifier
-// .align(Alignment.BottomEnd)
-// .size(32.dp)
-// .motionClickEvent { onProfileImageClicked() }
-// )
+ Box(
+ modifier = Modifier
+ .wrapContentSize()
+ .align(Alignment.BottomEnd)
+ .clip(CircleShape)
+ .border(1.dp, AppTheme.colorScheme.textPrimary, CircleShape)
+ .background(AppTheme.colorScheme.onPrimary, CircleShape)
+ ) {
+ Icon(
+ Icons.Default.Edit,
+ contentDescription = null,
+ modifier = Modifier
+ .align(Alignment.Center)
+ .padding(8.dp)
+ .size(20.dp)
+ .motionClickEvent {
+ if (!isImageUploading) {
+ onProfileImageClicked()
+ }
+ }
+ )
+ }
+
+ if (isImageUploading) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(CircleShape)
+ .background(AppTheme.colorScheme.onPrimary.copy(0.5f), CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ AppProgressIndicator()
+ }
+ }
}
}