Skip to content

Commit d486a81

Browse files
Pick image from gallery and upload it
1 parent 59a62ea commit d486a81

File tree

13 files changed

+365
-42
lines changed

13 files changed

+365
-42
lines changed

gravatar-quickeditor/api/gravatar-quickeditor.api

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@ public final class com/gravatar/quickeditor/GravatarQuickEditor {
99
public final class com/gravatar/quickeditor/ui/avatarpicker/ComposableSingletons$AvatarPickerKt {
1010
public static final field INSTANCE Lcom/gravatar/quickeditor/ui/avatarpicker/ComposableSingletons$AvatarPickerKt;
1111
public static field lambda-1 Lkotlin/jvm/functions/Function3;
12-
public static field lambda-2 Lkotlin/jvm/functions/Function3;
12+
public static field lambda-2 Lkotlin/jvm/functions/Function2;
1313
public static field lambda-3 Lkotlin/jvm/functions/Function2;
14-
public static field lambda-4 Lkotlin/jvm/functions/Function2;
15-
public static field lambda-5 Lkotlin/jvm/functions/Function2;
1614
public fun <init> ()V
1715
public final fun getLambda-1$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function3;
18-
public final fun getLambda-2$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function3;
16+
public final fun getLambda-2$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function2;
1917
public final fun getLambda-3$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function2;
20-
public final fun getLambda-4$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function2;
21-
public final fun getLambda-5$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function2;
18+
}
19+
20+
public final class com/gravatar/quickeditor/ui/components/ComposableSingletons$AvatarsSectionKt {
21+
public static final field INSTANCE Lcom/gravatar/quickeditor/ui/components/ComposableSingletons$AvatarsSectionKt;
22+
public static field lambda-1 Lkotlin/jvm/functions/Function3;
23+
public static field lambda-2 Lkotlin/jvm/functions/Function2;
24+
public fun <init> ()V
25+
public final fun getLambda-1$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function3;
26+
public final fun getLambda-2$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function2;
2227
}
2328

2429
public final class com/gravatar/quickeditor/ui/components/ComposableSingletons$EmailLabelKt {

gravatar-quickeditor/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ dependencies {
102102
implementation(libs.coil.compose)
103103
implementation(libs.retrofit)
104104
implementation(libs.retrofit.gson.converter)
105+
implementation(libs.ucrop)
105106

106107
// Jetpack Compose
107108
implementation(platform(libs.compose.bom))

gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/QuickEditorContainer.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
44
import android.content.Context
55
import androidx.datastore.preferences.preferencesDataStore
66
import com.google.gson.GsonBuilder
7+
import com.gravatar.quickeditor.data.FileUtils
78
import com.gravatar.quickeditor.data.repository.AvatarRepository
89
import com.gravatar.quickeditor.data.service.WordPressOAuthApi
910
import com.gravatar.quickeditor.data.service.WordPressOAuthService
@@ -60,6 +61,10 @@ internal class QuickEditorContainer private constructor(
6061
IdentityService()
6162
}
6263

64+
val fileUtils: FileUtils by lazy {
65+
FileUtils(context)
66+
}
67+
6368
val avatarRepository: AvatarRepository by lazy {
6469
AvatarRepository(
6570
avatarService = avatarService,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.gravatar.quickeditor.data
2+
3+
import android.content.Context
4+
import android.net.Uri
5+
import androidx.core.net.toFile
6+
import java.io.File
7+
8+
internal class FileUtils(
9+
private val context: Context,
10+
) {
11+
fun createTempFile(): File {
12+
return File(context.cacheDir, "gravatar_${System.currentTimeMillis()}")
13+
}
14+
15+
fun deleteFile(uri: Uri) {
16+
val toFile = uri.toFile()
17+
toFile.delete()
18+
}
19+
}

gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/data/models/QuickEditorError.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ internal sealed class QuickEditorError {
66
data object Unknown : QuickEditorError()
77

88
data object Server : QuickEditorError()
9+
10+
data object AvatarUploadFailed : QuickEditorError()
911
}

gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/data/repository/AvatarRepository.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.gravatar.quickeditor.data.repository
22

3+
import android.net.Uri
4+
import androidx.core.net.toFile
35
import com.gravatar.quickeditor.data.models.QuickEditorError
46
import com.gravatar.quickeditor.data.storage.TokenStorage
57
import com.gravatar.restapi.models.Avatar
@@ -44,6 +46,16 @@ internal class AvatarRepository(
4446
} ?: Result.Failure(QuickEditorError.TokenNotFound)
4547
}
4648

49+
suspend fun uploadAvatar(email: Email, avatarUri: Uri): Result<Unit, QuickEditorError> = withContext(dispatcher) {
50+
val token = tokenStorage.getToken(email.hash().toString())
51+
token?.let {
52+
when (avatarService.uploadCatching(avatarUri.toFile(), token)) {
53+
is Result.Success -> Result.Success(Unit)
54+
is Result.Failure -> Result.Failure(QuickEditorError.AvatarUploadFailed)
55+
}
56+
} ?: Result.Failure(QuickEditorError.TokenNotFound)
57+
}
58+
4759
private suspend fun getAvatarsAsync(token: String): Deferred<List<Avatar>> = coroutineScope {
4860
async { avatarService.retrieve(token) }
4961
}
@@ -55,5 +67,5 @@ internal class AvatarRepository(
5567

5668
internal data class IdentityAvatars(
5769
val avatars: List<Avatar>,
58-
val selectedAvatarId: String,
70+
val selectedAvatarId: String?,
5971
)

gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/avatarpicker/AvatarPicker.kt

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package com.gravatar.quickeditor.ui.avatarpicker
22

3-
import androidx.compose.foundation.border
4-
import androidx.compose.foundation.layout.Arrangement
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.graphics.Color
6+
import android.net.Uri
7+
import androidx.activity.compose.rememberLauncherForActivityResult
8+
import androidx.activity.result.ActivityResultLauncher
9+
import androidx.activity.result.contract.ActivityResultContracts
510
import androidx.compose.foundation.layout.Box
611
import androidx.compose.foundation.layout.Column
7-
import androidx.compose.foundation.layout.PaddingValues
812
import androidx.compose.foundation.layout.Spacer
913
import androidx.compose.foundation.layout.fillMaxWidth
1014
import androidx.compose.foundation.layout.height
1115
import androidx.compose.foundation.layout.padding
12-
import androidx.compose.foundation.layout.size
1316
import androidx.compose.foundation.layout.wrapContentSize
1417
import androidx.compose.material3.CircularProgressIndicator
1518
import androidx.compose.material3.MaterialTheme
@@ -47,8 +50,11 @@ import com.gravatar.restapi.models.Avatar
4750
import com.gravatar.types.Email
4851
import com.gravatar.ui.GravatarTheme
4952
import com.gravatar.ui.components.ComponentState
53+
import com.yalantis.ucrop.UCrop
54+
import com.yalantis.ucrop.UCropActivity
5055
import kotlinx.coroutines.Dispatchers
5156
import kotlinx.coroutines.withContext
57+
import java.io.File
5258
import java.time.Instant
5359

5460
@Composable
@@ -62,6 +68,14 @@ internal fun AvatarPicker(
6268
val context = LocalContext.current
6369
val uiState by viewModel.uiState.collectAsState()
6470

71+
val uCropLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
72+
it.data?.let { intentData ->
73+
UCrop.getOutput(intentData)?.let { croppedImageUri ->
74+
viewModel.uploadAvatar(croppedImageUri)
75+
}
76+
}
77+
}
78+
6579
LaunchedEffect(Unit) {
6680
withContext(Dispatchers.Main.immediate) {
6781
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -75,6 +89,10 @@ internal fun AvatarPicker(
7589
duration = SnackbarDuration.Long,
7690
)
7791
}
92+
93+
is AvatarPickerAction.LaunchImageCropper -> {
94+
uCropLauncher.launchAvatarCrop(action.imageUri, action.tempFile, context)
95+
}
7896
}
7997
}
8098
}
@@ -86,6 +104,7 @@ internal fun AvatarPicker(
86104
AvatarPicker(
87105
uiState = uiState,
88106
onAvatarSelected = viewModel::selectAvatar,
107+
onLocalImageSelected = viewModel::localImageSelected,
89108
)
90109
SnackbarHost(
91110
modifier = Modifier
@@ -103,7 +122,11 @@ internal fun AvatarPicker(
103122
}
104123

105124
@Composable
106-
internal fun AvatarPicker(uiState: AvatarPickerUiState, onAvatarSelected: (Avatar) -> Unit) {
125+
internal fun AvatarPicker(
126+
uiState: AvatarPickerUiState,
127+
onAvatarSelected: (Avatar) -> Unit,
128+
onLocalImageSelected: (Uri) -> Unit,
129+
) {
107130
Surface(Modifier.fillMaxWidth()) {
108131
Column {
109132
EmailLabel(
@@ -127,10 +150,11 @@ internal fun AvatarPicker(uiState: AvatarPickerUiState, onAvatarSelected: (Avata
127150
}
128151

129152
uiState.error -> Text(text = "There was an error loading avatars", textAlign = TextAlign.Center)
130-
uiState.avatars != null ->
153+
uiState.avatarsSectionUiState != null ->
131154
AvatarsSection(
132-
uiState.avatars,
155+
uiState.avatarsSectionUiState,
133156
onAvatarSelected,
157+
onLocalImageSelected,
134158
Modifier.padding(horizontal = 16.dp),
135159
)
136160
}
@@ -188,6 +212,7 @@ private fun AvatarPickerPreview() {
188212
),
189213
),
190214
onAvatarSelected = { },
215+
onLocalImageSelected = { },
191216
)
192217
}
193218
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.gravatar.quickeditor.ui.avatarpicker
22

3+
import android.net.Uri
34
import com.gravatar.restapi.models.Avatar
5+
import java.io.File
46

57
internal sealed class AvatarPickerAction {
68
data class AvatarSelected(val avatar: Avatar) : AvatarPickerAction()
9+
10+
data class LaunchImageCropper(val imageUri: Uri, val tempFile: File) : AvatarPickerAction()
711
}
Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.gravatar.quickeditor.ui.avatarpicker
22

3+
import android.net.Uri
34
import com.gravatar.quickeditor.data.repository.IdentityAvatars
45
import com.gravatar.restapi.models.Avatar
56
import com.gravatar.restapi.models.Profile
@@ -13,24 +14,47 @@ internal data class AvatarPickerUiState(
1314
val profile: ComponentState<Profile>? = null,
1415
val identityAvatars: IdentityAvatars? = null,
1516
val selectingAvatarId: String? = null,
17+
val uploadingAvatar: Uri? = null,
18+
val scrollToIndex: Int? = null,
1619
) {
17-
val avatars: List<AvatarUi>? = identityAvatars?.mapToUiModel()
20+
val avatarsSectionUiState: AvatarsSectionUiState? = identityAvatars?.mapToUiModel()?.let {
21+
AvatarsSectionUiState(
22+
avatars = it,
23+
scrollToIndex = scrollToIndex,
24+
uploadButtonEnabled = uploadingAvatar == null,
25+
)
26+
}
1827

19-
private fun IdentityAvatars.mapToUiModel(): List<AvatarUi.Uploaded> {
20-
return this.avatars.map { avatar ->
21-
AvatarUi.Uploaded(
22-
avatar = avatar,
23-
isSelected = avatar.imageId == (selectingAvatarId ?: selectedAvatarId),
24-
isLoading = avatar.imageId == selectingAvatarId,
28+
private fun IdentityAvatars.mapToUiModel(): List<AvatarUi> {
29+
return mutableListOf<AvatarUi>().apply {
30+
if (uploadingAvatar != null) add(AvatarUi.Local(uploadingAvatar))
31+
addAll(
32+
this@mapToUiModel.avatars.map { avatar ->
33+
AvatarUi.Uploaded(
34+
avatar = avatar,
35+
isSelected = avatar.imageId == (selectingAvatarId ?: selectedAvatarId),
36+
isLoading = avatar.imageId == selectingAvatarId,
37+
)
38+
},
2539
)
26-
}
40+
}.toList()
2741
}
2842
}
2943

44+
internal data class AvatarsSectionUiState(
45+
val avatars: List<AvatarUi>,
46+
val scrollToIndex: Int?,
47+
val uploadButtonEnabled: Boolean,
48+
)
49+
3050
internal sealed class AvatarUi(val avatarId: String) {
3151
data class Uploaded(
3252
val avatar: Avatar,
3353
val isSelected: Boolean,
3454
val isLoading: Boolean,
3555
) : AvatarUi(avatar.imageId)
56+
57+
data class Local(
58+
val uri: Uri,
59+
) : AvatarUi(uri.toString())
3660
}

0 commit comments

Comments
 (0)