Add progress and success to send process.
This commit is contained in:
		
							parent
							
								
									b3f9da7dc7
								
							
						
					
					
						commit
						896c917348
					
				
					 6 changed files with 196 additions and 71 deletions
				
			
		| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
package com.pixelized.biblib.ui.screen.home.detail
 | 
			
		||||
 | 
			
		||||
import android.util.Log
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +12,7 @@ import com.pixelized.biblib.utils.extention.toDetailUio
 | 
			
		|||
import dagger.hilt.android.lifecycle.HiltViewModel
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.delay
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.MutableSharedFlow
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
| 
						 | 
				
			
			@ -32,31 +32,43 @@ class BookDetailViewModel @Inject constructor(
 | 
			
		|||
    private val _sendStatus = MutableSharedFlow<Boolean>()
 | 
			
		||||
    val sendStatus: Flow<Boolean> get() = _sendStatus
 | 
			
		||||
 | 
			
		||||
    private val _sendingFlow = MutableSharedFlow<Boolean>()
 | 
			
		||||
    val sendingFlow: Flow<Boolean> get() = _sendingFlow
 | 
			
		||||
 | 
			
		||||
    private val _error = MutableSharedFlow<BookDetailUioErrorUio>()
 | 
			
		||||
    val error: Flow<BookDetailUioErrorUio> get() = _error
 | 
			
		||||
 | 
			
		||||
    fun getDetail(id: Int) {
 | 
			
		||||
    fun getDetail(bookId: Int) {
 | 
			
		||||
        detailJob?.cancel()
 | 
			
		||||
        detailJob = viewModelScope.launch(Dispatchers.IO) {
 | 
			
		||||
            try {
 | 
			
		||||
                _detail.value = getCacheBookDetail(id = id)
 | 
			
		||||
                _detail.value = getBookDetail(id = id)
 | 
			
		||||
                _detail.value = getCacheBookDetail(id = bookId)
 | 
			
		||||
                _detail.value = getBookDetail(id = bookId)
 | 
			
		||||
            } catch (exception: Exception) {
 | 
			
		||||
                _error.emit(toDetailErrorUio(bookId = id))
 | 
			
		||||
                _error.emit(toDetailErrorUio(bookId = bookId))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Suppress("LiftReturnOrAssignment")
 | 
			
		||||
    fun send(bookId: Int, email: String, format: String) {
 | 
			
		||||
        sendJob?.cancel()
 | 
			
		||||
        sendJob = viewModelScope.launch(Dispatchers.IO) {
 | 
			
		||||
            _sendingFlow.emit(true)
 | 
			
		||||
            var isSuccess = false
 | 
			
		||||
            try {
 | 
			
		||||
                val data = client.service.send(bookId = bookId, mail = email, format = format)
 | 
			
		||||
                _sendStatus.emit(true)
 | 
			
		||||
                Log.d("send", data.toString())
 | 
			
		||||
                isSuccess = data.containsKey("ok")
 | 
			
		||||
            } catch (exception: Exception) {
 | 
			
		||||
                Log.d("send", exception.message, exception)
 | 
			
		||||
                _error.emit(toSendBookUio(bookId = bookId, mail = email, format))
 | 
			
		||||
                _error.emit(toSendBookUio(bookId = bookId, mail = email, format = format))
 | 
			
		||||
                isSuccess = false
 | 
			
		||||
            } finally {
 | 
			
		||||
                _sendingFlow.emit(false)
 | 
			
		||||
                if (isSuccess) {
 | 
			
		||||
                    _sendStatus.emit(true)
 | 
			
		||||
                    delay(300 + 2000)
 | 
			
		||||
                    _sendStatus.emit(false)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,12 +14,12 @@ import androidx.compose.ui.platform.LocalUriHandler
 | 
			
		|||
import androidx.hilt.navigation.compose.hiltViewModel
 | 
			
		||||
import com.pixelized.biblib.R
 | 
			
		||||
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.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.utils.extention.bibLib
 | 
			
		||||
import com.pixelized.biblib.utils.extention.showToast
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
 | 
			
		||||
@Stable
 | 
			
		||||
| 
						 | 
				
			
			@ -92,28 +92,27 @@ fun DetailScreen(
 | 
			
		|||
                            modifier = Modifier
 | 
			
		||||
                                .fillMaxSize()
 | 
			
		||||
                                .verticalScroll(rememberScrollState())
 | 
			
		||||
                                .systemBarsPadding()
 | 
			
		||||
                                .padding(all = MaterialTheme.bibLib.dimen.dp16)
 | 
			
		||||
                                .animateContentSize(),
 | 
			
		||||
                                .systemBarsPadding(),
 | 
			
		||||
                            book = detail,
 | 
			
		||||
                            onSend = {
 | 
			
		||||
                                // check
 | 
			
		||||
                                val user = profileViewModel.user
 | 
			
		||||
                                if (user.isSuccessful()) {
 | 
			
		||||
                                    val mails = user.value.amazonEmails
 | 
			
		||||
                                    when {
 | 
			
		||||
                                        mails.isEmpty() -> {
 | 
			
		||||
                                            context.showToast(context.getString(R.string.error_no_amazon_email))
 | 
			
		||||
                                        }
 | 
			
		||||
                                        else -> {
 | 
			
		||||
                                            scope.launch { emailSheetState.show() }
 | 
			
		||||
                                        }
 | 
			
		||||
                                scope.launch {
 | 
			
		||||
                                    val user = profileViewModel.user as? StateUio.Success<UserUio>
 | 
			
		||||
                                    if (user?.value?.amazonEmails?.isEmpty() == true) {
 | 
			
		||||
                                        snackBarHost.showSnackbar(
 | 
			
		||||
                                            message = context.getString(R.string.error_no_amazon_email),
 | 
			
		||||
                                            duration = SnackbarDuration.Long,
 | 
			
		||||
                                        )
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        emailSheetState.show()
 | 
			
		||||
                                    }
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    // TODO()
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        )
 | 
			
		||||
                        DetailScreenSendLoader(
 | 
			
		||||
                            loadingVisibility = detailViewModel.sendStatus,
 | 
			
		||||
                            successVisibility = detailViewModel.sendingFlow,
 | 
			
		||||
                        )
 | 
			
		||||
                    } else {
 | 
			
		||||
                        EmptyDetail()
 | 
			
		||||
                    }
 | 
			
		||||
| 
						 | 
				
			
			@ -123,38 +122,33 @@ fun DetailScreen(
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (detailState.bottomSheetState.isVisible || detailState.bottomSheetState.isAnimationRunning) {
 | 
			
		||||
        LaunchedEffect(key1 = "DetailScreenError$bookId") {
 | 
			
		||||
            detailViewModel.error.collect {
 | 
			
		||||
                val result = snackBarHost.showSnackbar(
 | 
			
		||||
                    message = context.getString(it.message),
 | 
			
		||||
                    actionLabel = context.getString(it.action),
 | 
			
		||||
                )
 | 
			
		||||
                if (result == SnackbarResult.ActionPerformed) {
 | 
			
		||||
                    when (it) {
 | 
			
		||||
                        is BookDetailUioErrorUio.GetDetailInput -> detailViewModel.getDetail(
 | 
			
		||||
                            id = it.bookId
 | 
			
		||||
                        )
 | 
			
		||||
                        is BookDetailUioErrorUio.SendBookInput -> detailViewModel.send(
 | 
			
		||||
                            bookId = it.bookId,
 | 
			
		||||
                            email = it.mail,
 | 
			
		||||
                            format = it.format,
 | 
			
		||||
                        )
 | 
			
		||||
        LaunchedEffect(key1 = "DetailScreen$bookId") {
 | 
			
		||||
            launch {
 | 
			
		||||
                detailViewModel.error.collect {
 | 
			
		||||
                    val result = snackBarHost.showSnackbar(
 | 
			
		||||
                        message = context.getString(it.message),
 | 
			
		||||
                        actionLabel = context.getString(it.action),
 | 
			
		||||
                    )
 | 
			
		||||
                    if (result == SnackbarResult.ActionPerformed) {
 | 
			
		||||
                        when (it) {
 | 
			
		||||
                            is BookDetailUioErrorUio.GetDetailInput -> detailViewModel.getDetail(
 | 
			
		||||
                                bookId = it.bookId
 | 
			
		||||
                            )
 | 
			
		||||
                            is BookDetailUioErrorUio.SendBookInput -> detailViewModel.send(
 | 
			
		||||
                                bookId = it.bookId,
 | 
			
		||||
                                email = it.mail,
 | 
			
		||||
                                format = it.format,
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LaunchedEffect(key1 = "DetailScreenSuccess:$bookId") {
 | 
			
		||||
            detailViewModel.sendStatus.collect {
 | 
			
		||||
                snackBarHost.showSnackbar(
 | 
			
		||||
                    message = context.getString(R.string.detail_send_success)
 | 
			
		||||
                )
 | 
			
		||||
            launch {
 | 
			
		||||
                bookId?.let { detailViewModel.getDetail(it) }
 | 
			
		||||
            }
 | 
			
		||||
            launch {
 | 
			
		||||
                emailSheetState.hide()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LaunchedEffect(key1 = bookId) {
 | 
			
		||||
            bookId?.let { detailViewModel.getDetail(it) }
 | 
			
		||||
            emailSheetState.hide()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        BackHandler(emailSheetState.isVisible || emailSheetState.isAnimationRunning) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,9 @@ import androidx.compose.ui.tooling.preview.Preview
 | 
			
		|||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import androidx.core.text.HtmlCompat
 | 
			
		||||
import androidx.core.text.toSpannable
 | 
			
		||||
import com.google.accompanist.placeholder.PlaceholderHighlight
 | 
			
		||||
import com.google.accompanist.placeholder.material.placeholder
 | 
			
		||||
import com.google.accompanist.placeholder.shimmer
 | 
			
		||||
import com.pixelized.biblib.R
 | 
			
		||||
import com.pixelized.biblib.ui.composable.SpannedText
 | 
			
		||||
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
 | 
			
		||||
| 
						 | 
				
			
			@ -204,11 +206,18 @@ fun DetailScreenContent(
 | 
			
		|||
                SpannedText(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .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,
 | 
			
		||||
                    color = MaterialTheme.colors.onSurface,
 | 
			
		||||
                    text = HtmlCompat.fromHtml(
 | 
			
		||||
                        book.description.ifEmpty { "placeholder" },
 | 
			
		||||
                        book.description,
 | 
			
		||||
                        HtmlCompat.FROM_HTML_MODE_COMPACT,
 | 
			
		||||
                    ).toSpannable(),
 | 
			
		||||
                )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ data class BibLibColor(
 | 
			
		|||
    val base: Colors,
 | 
			
		||||
    val typography: Typography,
 | 
			
		||||
    val placeHolder: Color,
 | 
			
		||||
    val shimmer: Color,
 | 
			
		||||
    val dialogBackground: Color,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -41,11 +42,13 @@ fun bibLibDarkColors(
 | 
			
		|||
        strong = base.primary,
 | 
			
		||||
    ),
 | 
			
		||||
    placeHolder: Color = BibLibColorPalette.DarkGrey,
 | 
			
		||||
    shimmer: Color = BibLibColorPalette.Grey,
 | 
			
		||||
    dialogBackground: Color = BibLibColorPalette.VeryDarkGrey,
 | 
			
		||||
) = BibLibColor(
 | 
			
		||||
    base = base,
 | 
			
		||||
    typography = typography,
 | 
			
		||||
    placeHolder = placeHolder,
 | 
			
		||||
    shimmer = shimmer,
 | 
			
		||||
    dialogBackground = dialogBackground,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -64,10 +67,12 @@ fun bibLibLightColors(
 | 
			
		|||
        strong = base.primary,
 | 
			
		||||
    ),
 | 
			
		||||
    placeHolder: Color = BibLibColorPalette.LightGrey,
 | 
			
		||||
    shimmer: Color = BibLibColorPalette.Grey,
 | 
			
		||||
    dialogBackground: Color = Color.White,
 | 
			
		||||
) = BibLibColor(
 | 
			
		||||
    base = base,
 | 
			
		||||
    typography = typography,
 | 
			
		||||
    placeHolder = placeHolder,
 | 
			
		||||
    shimmer = shimmer,
 | 
			
		||||
    dialogBackground = dialogBackground,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -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(""))
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue