Skip to content

Commit

Permalink
Merge pull request #64 from Automattic/hamorillo/4-image-picker
Browse files Browse the repository at this point in the history
SDK - Component to pick, edit and upload avatar image
  • Loading branch information
hamorillo committed Mar 1, 2024
2 parents 2df5c23 + f7e603b commit 03c676d
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 28 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ dependencies {
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
implementation(project(":gravatar"))
implementation("io.coil-kt:coil-compose:2.5.0")

Expand Down
110 changes: 110 additions & 0 deletions app/src/main/java/com/gravatar/demoapp/ui/AvatarUpdateTab.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.gravatar.demoapp.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AccountCircle
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.gravatar.GravatarApi
import com.gravatar.R
import com.gravatar.demoapp.ui.components.GravatarEmailInput
import com.gravatar.demoapp.ui.components.GravatarPasswordInput
import com.gravatar.ui.GravatarImagePickerWrapper
import com.gravatar.ui.GravatarImagePickerWrapperListener

@Composable
fun AvatarUpdateTab(showSnackBar: (String?, Throwable?) -> Unit, modifier: Modifier = Modifier) {
var email by remember { mutableStateOf("gravatar@automattic.com") }
var accessToken by remember { mutableStateOf("") }
var accessTokenVisible by rememberSaveable { mutableStateOf(false) }
var isUploading by remember { mutableStateOf(false) }

Column(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val context = LocalContext.current
GravatarEmailInput(email = email, onValueChange = { email = it }, Modifier.fillMaxWidth())
GravatarPasswordInput(
password = accessToken,
passwordIsVisible = accessTokenVisible,
onValueChange = { accessToken = it },
onVisibilityChange = { accessTokenVisible = it },
label = { Text(stringResource(R.string.access_token_label)) },
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth(),
)
GravatarImagePickerWrapper(
{ UpdateAvatarComposable(isUploading) },
email,
accessToken,
object : GravatarImagePickerWrapperListener {
override fun onAvatarUploadStarted() {
isUploading = true
}

override fun onSuccess(response: Unit) {
isUploading = false
showSnackBar(context.getString(R.string.avatar_update_upload_success_toast), null)
}

override fun onError(errorType: GravatarApi.ErrorType) {
isUploading = false
showSnackBar(context.getString(R.string.avatar_update_upload_failed_toast, errorType), null)
}
},
modifier = Modifier.padding(top = 16.dp),
)
}
}

@Composable
private fun UpdateAvatarComposable(isUploading: Boolean) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
if (isUploading) {
CircularProgressIndicator()
} else {
Icon(
Icons.Rounded.AccountCircle,
contentDescription = "",
modifier = Modifier.size(128.dp),
)
Text(text = stringResource(R.string.update_avatar_button_label))
}
}
}

@Preview
@Composable
private fun UpdateAvatarComposablePreview() = UpdateAvatarComposable(false)

@Preview
@Composable
private fun UpdateAvatarLoadingComposablePreview() = UpdateAvatarComposable(true)

@Preview
@Composable
private fun AvatarUpdateTabPreview() = AvatarUpdateTab(showSnackBar = { _, _ -> })
51 changes: 24 additions & 27 deletions app/src/main/java/com/gravatar/demoapp/ui/DemoGravatarApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -47,6 +46,7 @@ import com.gravatar.GravatarApi
import com.gravatar.ImageRating
import com.gravatar.R
import com.gravatar.demoapp.theme.GravatarDemoAppTheme
import com.gravatar.demoapp.ui.components.GravatarEmailInput
import com.gravatar.demoapp.ui.components.ProfileCard
import com.gravatar.demoapp.ui.model.SettingsState
import com.gravatar.emailAddressToGravatarUrl
Expand All @@ -66,13 +66,12 @@ fun DemoGravatarApp() {
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { innerPadding ->
val defaultErrorMessage = stringResource(R.string.snackbar_unknown_error_message)

GravatarTabs(
modifier = Modifier.padding(innerPadding),
gravatarUrl,
{ gravatarUrl = it },
) { errorMessage, exception ->
onError(scope, snackbarHostState, errorMessage, exception, defaultErrorMessage)
) { message, exception ->
showSnackBar(scope, snackbarHostState, message, exception, defaultErrorMessage)
}
}
}
Expand All @@ -94,17 +93,17 @@ val defaultAvatarImages by lazy {
)
}

private fun onError(
private fun showSnackBar(
scope: CoroutineScope,
snackbarHostState: SnackbarHostState,
errorMessage: String?,
message: String?,
throwable: Throwable?,
defaultErrorMessage: String,
defaultMessage: String,
) {
Log.e("DemoGravatarApp", "${errorMessage.orEmpty()}\n${throwable?.stackTraceToString().orEmpty()}")
Log.e("DemoGravatarApp", "${message.orEmpty()}\n${throwable?.stackTraceToString().orEmpty()}")
scope.launch {
snackbarHostState.showSnackbar(
message = errorMessage ?: throwable?.message ?: defaultErrorMessage,
message = message ?: throwable?.message ?: defaultMessage,
duration = SnackbarDuration.Short,
)
}
Expand All @@ -115,11 +114,15 @@ private fun GravatarTabs(
modifier: Modifier = Modifier,
gravatarUrl: String,
onGravatarUrlChanged: (String) -> Unit,
onError: (String?, Throwable?) -> Unit,
showSnackBar: (String?, Throwable?) -> Unit,
) {
var tabIndex by remember { mutableStateOf(0) }

val tabs = listOf(stringResource(R.string.tab_label_avatar), stringResource(R.string.tab_label_profile))
val tabs = listOf(
stringResource(R.string.tab_label_avatar),
stringResource(R.string.tab_label_profile),
stringResource(R.string.tab_label_avatar_update),
)

Column(modifier = Modifier.fillMaxSize()) {
TabRow(selectedTabIndex = tabIndex) {
Expand All @@ -132,8 +135,9 @@ private fun GravatarTabs(
}
}
when (tabIndex) {
0 -> AvatarTab(modifier, gravatarUrl, onGravatarUrlChanged, onError)
1 -> ProfileTab(modifier, onError)
0 -> AvatarTab(modifier, gravatarUrl, onGravatarUrlChanged, showSnackBar)
1 -> ProfileTab(modifier, showSnackBar)
2 -> AvatarUpdateTab(showSnackBar, modifier)
}
}
}
Expand Down Expand Up @@ -194,9 +198,13 @@ private fun ProfileTab(modifier: Modifier = Modifier, onError: (String?, Throwab
if (!loading && error.isEmpty() && profiles.entry.size > 0) {
ProfileCard(
profiles.entry.first(),
Modifier.clip(
RoundedCornerShape(8.dp),
).background(MaterialTheme.colorScheme.surfaceContainer).fillMaxWidth().padding(16.dp),
Modifier
.clip(
RoundedCornerShape(8.dp),
)
.background(MaterialTheme.colorScheme.surfaceContainer)
.fillMaxWidth()
.padding(16.dp),
)
} else {
if (error.isNotEmpty()) {
Expand Down Expand Up @@ -327,14 +335,3 @@ fun GravatarImage(gravatarUrl: String, onError: (String?, Throwable?) -> Unit) {
contentDescription = "",
)
}

@Composable
fun GravatarEmailInput(email: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier) {
TextField(
value = email,
onValueChange = onValueChange,
label = { Text(stringResource(R.string.gravatar_email_input_label)) },
maxLines = 1,
modifier = modifier,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.gravatar.DefaultAvatarImage
import com.gravatar.ImageRating
import com.gravatar.R
import com.gravatar.demoapp.theme.GravatarDemoAppTheme
import com.gravatar.demoapp.ui.components.GravatarEmailInput
import com.gravatar.demoapp.ui.model.SettingsState

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.gravatar.demoapp.ui.components

import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.gravatar.R

@Composable
fun GravatarEmailInput(email: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier) {
TextField(
value = email,
onValueChange = onValueChange,
label = { Text(stringResource(R.string.gravatar_email_input_label)) },
maxLines = 1,
modifier = modifier,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.gravatar.demoapp.ui.components

import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation

@Composable
fun GravatarPasswordInput(
password: String,
passwordIsVisible: Boolean,
onValueChange: (String) -> Unit,
onVisibilityChange: (Boolean) -> Unit,
label: @Composable (() -> Unit),
modifier: Modifier = Modifier,
) {
TextField(
value = password,
onValueChange = onValueChange,
label = label,
maxLines = 1,
modifier = modifier,
visualTransformation = if (passwordIsVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
trailingIcon = {
val image = if (passwordIsVisible) {
Icons.Filled.Visibility
} else {
Icons.Filled.VisibilityOff
}

IconButton(onClick = { onVisibilityChange(!passwordIsVisible) }) {
Icon(imageVector = image, "")
}
},
)
}
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
<string name="snackbar_unknown_error_message">Unknown error</string>
<string name="tab_label_profile">Profile</string>
<string name="tab_label_avatar">Avatar</string>
<string name="tab_label_avatar_update">Avatar Update</string>
<string name="button_get_profile">Get Profile</string>
<string name="text_display_name">Display Name: %1$s</string>
<string name="text_url">Url: %1$s</string>
<string name="button_load_gravatar">Load Gravatar</string>
<string name="access_token_label">Access Token</string>
<string name="update_avatar_button_label">Update Avatar</string>
<string name="avatar_update_upload_success_toast">Upload success</string>
<string name="avatar_update_upload_failed_toast">Upload error: %1$s</string>
</resources>
16 changes: 15 additions & 1 deletion gravatar/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}

buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
detekt {
config.setFrom("${project.rootDir}/config/detekt/detekt.yml")
source.setFrom("src")
Expand Down Expand Up @@ -70,6 +75,15 @@ dependencies {
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("com.github.yalantis:ucrop:2.2.8")

// Jetpack Compose
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
debugImplementation("androidx.compose.ui:ui-tooling:1.6.2")

testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.11.1")
Expand Down
6 changes: 6 additions & 0 deletions gravatar/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<!-- Lib activities-->
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.NoActionBar" />
</application>
</manifest>
Loading

0 comments on commit 03c676d

Please sign in to comment.