Add progress and success to send process.

This commit is contained in:
Thomas Andres Gomez 2022-10-24 10:42:21 +02:00
parent b3f9da7dc7
commit 896c917348
6 changed files with 196 additions and 71 deletions

View file

@ -1,6 +1,5 @@
package com.pixelized.biblib.ui.screen.home.detail package com.pixelized.biblib.ui.screen.home.detail
import android.util.Log
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -13,6 +12,7 @@ import com.pixelized.biblib.utils.extention.toDetailUio
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -32,31 +32,43 @@ class BookDetailViewModel @Inject constructor(
private val _sendStatus = MutableSharedFlow<Boolean>() private val _sendStatus = MutableSharedFlow<Boolean>()
val sendStatus: Flow<Boolean> get() = _sendStatus val sendStatus: Flow<Boolean> get() = _sendStatus
private val _sendingFlow = MutableSharedFlow<Boolean>()
val sendingFlow: Flow<Boolean> get() = _sendingFlow
private val _error = MutableSharedFlow<BookDetailUioErrorUio>() private val _error = MutableSharedFlow<BookDetailUioErrorUio>()
val error: Flow<BookDetailUioErrorUio> get() = _error val error: Flow<BookDetailUioErrorUio> get() = _error
fun getDetail(id: Int) { fun getDetail(bookId: Int) {
detailJob?.cancel() detailJob?.cancel()
detailJob = viewModelScope.launch(Dispatchers.IO) { detailJob = viewModelScope.launch(Dispatchers.IO) {
try { try {
_detail.value = getCacheBookDetail(id = id) _detail.value = getCacheBookDetail(id = bookId)
_detail.value = getBookDetail(id = id) _detail.value = getBookDetail(id = bookId)
} catch (exception: Exception) { } catch (exception: Exception) {
_error.emit(toDetailErrorUio(bookId = id)) _error.emit(toDetailErrorUio(bookId = bookId))
} }
} }
} }
@Suppress("LiftReturnOrAssignment")
fun send(bookId: Int, email: String, format: String) { fun send(bookId: Int, email: String, format: String) {
sendJob?.cancel() sendJob?.cancel()
sendJob = viewModelScope.launch(Dispatchers.IO) { sendJob = viewModelScope.launch(Dispatchers.IO) {
_sendingFlow.emit(true)
var isSuccess = false
try { try {
val data = client.service.send(bookId = bookId, mail = email, format = format) val data = client.service.send(bookId = bookId, mail = email, format = format)
_sendStatus.emit(true) isSuccess = data.containsKey("ok")
Log.d("send", data.toString())
} catch (exception: Exception) { } catch (exception: Exception) {
Log.d("send", exception.message, exception) _error.emit(toSendBookUio(bookId = bookId, mail = email, format = format))
_error.emit(toSendBookUio(bookId = bookId, mail = email, format)) isSuccess = false
} finally {
_sendingFlow.emit(false)
if (isSuccess) {
_sendStatus.emit(true)
delay(300 + 2000)
_sendStatus.emit(false)
}
} }
} }
} }

View file

@ -14,12 +14,12 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.ui.LocalSnackHostState import com.pixelized.biblib.ui.LocalSnackHostState
import com.pixelized.biblib.ui.composable.isSuccessful import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState
import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel
import com.pixelized.biblib.ui.screen.home.page.profile.UserUio
import com.pixelized.biblib.ui.theme.color.ShadowPalette import com.pixelized.biblib.ui.theme.color.ShadowPalette
import com.pixelized.biblib.utils.extention.bibLib import com.pixelized.biblib.utils.extention.bibLib
import com.pixelized.biblib.utils.extention.showToast
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Stable @Stable
@ -92,28 +92,27 @@ fun DetailScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.systemBarsPadding()
.padding(all = MaterialTheme.bibLib.dimen.dp16) .padding(all = MaterialTheme.bibLib.dimen.dp16)
.animateContentSize(), .systemBarsPadding(),
book = detail, book = detail,
onSend = { onSend = {
// check scope.launch {
val user = profileViewModel.user val user = profileViewModel.user as? StateUio.Success<UserUio>
if (user.isSuccessful()) { if (user?.value?.amazonEmails?.isEmpty() == true) {
val mails = user.value.amazonEmails snackBarHost.showSnackbar(
when { message = context.getString(R.string.error_no_amazon_email),
mails.isEmpty() -> { duration = SnackbarDuration.Long,
context.showToast(context.getString(R.string.error_no_amazon_email)) )
} } else {
else -> { emailSheetState.show()
scope.launch { emailSheetState.show() }
}
} }
} else {
// TODO()
} }
} }
) )
DetailScreenSendLoader(
loadingVisibility = detailViewModel.sendStatus,
successVisibility = detailViewModel.sendingFlow,
)
} else { } else {
EmptyDetail() EmptyDetail()
} }
@ -123,38 +122,33 @@ fun DetailScreen(
} }
if (detailState.bottomSheetState.isVisible || detailState.bottomSheetState.isAnimationRunning) { if (detailState.bottomSheetState.isVisible || detailState.bottomSheetState.isAnimationRunning) {
LaunchedEffect(key1 = "DetailScreenError$bookId") { LaunchedEffect(key1 = "DetailScreen$bookId") {
detailViewModel.error.collect { launch {
val result = snackBarHost.showSnackbar( detailViewModel.error.collect {
message = context.getString(it.message), val result = snackBarHost.showSnackbar(
actionLabel = context.getString(it.action), message = context.getString(it.message),
) actionLabel = context.getString(it.action),
if (result == SnackbarResult.ActionPerformed) { )
when (it) { if (result == SnackbarResult.ActionPerformed) {
is BookDetailUioErrorUio.GetDetailInput -> detailViewModel.getDetail( when (it) {
id = it.bookId is BookDetailUioErrorUio.GetDetailInput -> detailViewModel.getDetail(
) bookId = it.bookId
is BookDetailUioErrorUio.SendBookInput -> detailViewModel.send( )
bookId = it.bookId, is BookDetailUioErrorUio.SendBookInput -> detailViewModel.send(
email = it.mail, bookId = it.bookId,
format = it.format, email = it.mail,
) format = it.format,
)
}
} }
} }
} }
} launch {
bookId?.let { detailViewModel.getDetail(it) }
LaunchedEffect(key1 = "DetailScreenSuccess:$bookId") { }
detailViewModel.sendStatus.collect { launch {
snackBarHost.showSnackbar( emailSheetState.hide()
message = context.getString(R.string.detail_send_success)
)
} }
}
LaunchedEffect(key1 = bookId) {
bookId?.let { detailViewModel.getDetail(it) }
emailSheetState.hide()
} }
BackHandler(emailSheetState.isVisible || emailSheetState.isAnimationRunning) { BackHandler(emailSheetState.isVisible || emailSheetState.isAnimationRunning) {

View file

@ -25,7 +25,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.material.placeholder import com.google.accompanist.placeholder.material.placeholder
import com.google.accompanist.placeholder.shimmer
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.SpannedText import com.pixelized.biblib.ui.composable.SpannedText
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
@ -204,11 +206,18 @@ fun DetailScreenContent(
SpannedText( SpannedText(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.placeholder(visible = book.placeHolder), .placeholder(
visible = book.placeHolder,
shape = MaterialTheme.bibLib.shapes.base.medium,
color = MaterialTheme.bibLib.colors.placeHolder,
highlight = PlaceholderHighlight.shimmer(
highlightColor = MaterialTheme.bibLib.colors.shimmer,
),
),
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface, color = MaterialTheme.colors.onSurface,
text = HtmlCompat.fromHtml( text = HtmlCompat.fromHtml(
book.description.ifEmpty { "placeholder" }, book.description,
HtmlCompat.FROM_HTML_MODE_COMPACT, HtmlCompat.FROM_HTML_MODE_COMPACT,
).toSpannable(), ).toSpannable(),
) )

View file

@ -0,0 +1,120 @@
package com.pixelized.biblib.ui.screen.home.detail
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.theme.color.BibLibColorPalette
import com.pixelized.biblib.ui.theme.color.ShadowPalette
import com.pixelized.biblib.utils.extention.bibLib
import kotlinx.coroutines.flow.Flow
@Composable
fun DetailScreenSendLoader(
loadingVisibility: Flow<Boolean>,
successVisibility: Flow<Boolean>,
) {
val isLoadingVisible by successVisibility.collectAsState(initial = false)
val isSuccessVisible by loadingVisibility.collectAsState(initial = false)
DetailScreenSendLoaderContent(
modifier = Modifier.fillMaxSize(),
isLoadingVisible = isLoadingVisible,
isSuccessVisible = isSuccessVisible,
)
}
@Composable
fun DetailScreenSendLoaderContent(
modifier: Modifier = Modifier,
isLoadingVisible: Boolean,
isSuccessVisible: Boolean,
) {
Box(modifier = modifier) {
AnimatedVisibility(
visible = isLoadingVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = ShadowPalette.scrim)
.padding(all = MaterialTheme.bibLib.dimen.dp16),
)
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = isLoadingVisible,
enter = fadeIn() + slideInVertically { it },
exit = fadeOut() + slideOutVertically { it },
) {
CircularProgressIndicator(
modifier = Modifier.size(MaterialTheme.bibLib.dimen.dialog.iconSize),
)
}
AnimatedVisibility(
modifier = Modifier.align(alignment = Alignment.BottomCenter),
visible = isSuccessVisible,
enter = fadeIn() + slideInVertically { it },
exit = fadeOut(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.padding(all = MaterialTheme.bibLib.dimen.dp16)
.background(
color = BibLibColorPalette.LightGreen,
shape = MaterialTheme.shapes.small,
)
.padding(
vertical = MaterialTheme.bibLib.dimen.dp16,
horizontal = MaterialTheme.bibLib.dimen.dp24,
),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.dp16)
) {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24),
tint = Color.White,
contentDescription = null
)
Text(
style = MaterialTheme.typography.body1,
color = Color.White,
text = stringResource(id = R.string.detail_send_success)
)
}
}
}
}
@Composable
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
fun DetailScreenSendLoaderPreview() {
BibLibTheme {
DetailScreenSendLoaderContent(
isLoadingVisible = true,
isSuccessVisible = true,
)
}
}

View file

@ -14,6 +14,7 @@ data class BibLibColor(
val base: Colors, val base: Colors,
val typography: Typography, val typography: Typography,
val placeHolder: Color, val placeHolder: Color,
val shimmer: Color,
val dialogBackground: Color, val dialogBackground: Color,
) { ) {
@ -41,11 +42,13 @@ fun bibLibDarkColors(
strong = base.primary, strong = base.primary,
), ),
placeHolder: Color = BibLibColorPalette.DarkGrey, placeHolder: Color = BibLibColorPalette.DarkGrey,
shimmer: Color = BibLibColorPalette.Grey,
dialogBackground: Color = BibLibColorPalette.VeryDarkGrey, dialogBackground: Color = BibLibColorPalette.VeryDarkGrey,
) = BibLibColor( ) = BibLibColor(
base = base, base = base,
typography = typography, typography = typography,
placeHolder = placeHolder, placeHolder = placeHolder,
shimmer = shimmer,
dialogBackground = dialogBackground, dialogBackground = dialogBackground,
) )
@ -64,10 +67,12 @@ fun bibLibLightColors(
strong = base.primary, strong = base.primary,
), ),
placeHolder: Color = BibLibColorPalette.LightGrey, placeHolder: Color = BibLibColorPalette.LightGrey,
shimmer: Color = BibLibColorPalette.Grey,
dialogBackground: Color = Color.White, dialogBackground: Color = Color.White,
) = BibLibColor( ) = BibLibColor(
base = base, base = base,
typography = typography, typography = typography,
placeHolder = placeHolder, placeHolder = placeHolder,
shimmer = shimmer,
dialogBackground = dialogBackground, dialogBackground = dialogBackground,
) )

View file

@ -1,15 +0,0 @@
package com.pixelized.biblib.utils
import android.content.Context
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.drawablepainter.DrawablePainter
fun painterResource(context: Context, @DrawableRes res: Int): Painter {
val drawable = AppCompatResources.getDrawable(context, res)
return DrawablePainter(drawable = drawable ?: error(""))
}