diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt index 9b9beef..920d6d8 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt @@ -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() val sendStatus: Flow get() = _sendStatus + private val _sendingFlow = MutableSharedFlow() + val sendingFlow: Flow get() = _sendingFlow + private val _error = MutableSharedFlow() val error: Flow 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) + } } } } diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt index cdddaa2..e805966 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt @@ -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 + 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) { diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreenContent.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreenContent.kt index 439a495..8b9e9d7 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreenContent.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreenContent.kt @@ -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(), ) diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreenSendLoader.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreenSendLoader.kt new file mode 100644 index 0000000..5f5af79 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreenSendLoader.kt @@ -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, + successVisibility: Flow, +) { + 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, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/theme/color/BibLibColor.kt b/app/src/main/java/com/pixelized/biblib/ui/theme/color/BibLibColor.kt index aa5c3b5..d9fb623 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/theme/color/BibLibColor.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/theme/color/BibLibColor.kt @@ -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, ) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/utils/painterResource.kt b/app/src/main/java/com/pixelized/biblib/utils/painterResource.kt deleted file mode 100644 index b0476dc..0000000 --- a/app/src/main/java/com/pixelized/biblib/utils/painterResource.kt +++ /dev/null @@ -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("")) -} \ No newline at end of file