Merge confirm & option dialog / bottomsheet

This commit is contained in:
Thomas Andres Gomez 2022-10-22 13:26:42 +02:00
parent 3e234cc37d
commit 2a92fd2b8b
6 changed files with 103 additions and 272 deletions

View file

@ -1,19 +1,18 @@
package com.pixelized.biblib.ui.screen.home.detail
import android.app.Application
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.R
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.repository.book.BookRepository
import com.pixelized.biblib.utils.extention.stringResource
import com.pixelized.biblib.utils.extention.toDetailUio
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
@ -21,14 +20,15 @@ import javax.inject.Inject
@HiltViewModel
class BookDetailViewModel @Inject constructor(
application: Application,
private val bookRepository: BookRepository,
private val client: IBibLibClient,
) : AndroidViewModel(application) {
) : ViewModel() {
private var detailJob: Job? = null
private val _detail = mutableStateOf<BookDetailUio?>(null)
val detail: State<BookDetailUio?> get() = _detail
private var sendJob: Job? = null
private val _sendStatus = MutableSharedFlow<Boolean>()
val sendStatus: Flow<Boolean> get() = _sendStatus
@ -36,7 +36,8 @@ class BookDetailViewModel @Inject constructor(
val error: Flow<BookDetailUioErrorUio> get() = _error
fun getDetail(id: Int) {
viewModelScope.launch(Dispatchers.IO) {
detailJob?.cancel()
detailJob = viewModelScope.launch(Dispatchers.IO) {
try {
_detail.value = getCacheBookDetail(id = id)
_detail.value = getBookDetail(id = id)
@ -46,8 +47,9 @@ class BookDetailViewModel @Inject constructor(
}
}
suspend fun send(bookId: Int, email: String, format: String) {
viewModelScope.launch(Dispatchers.IO) {
fun send(bookId: Int, email: String, format: String) {
sendJob?.cancel()
sendJob = viewModelScope.launch(Dispatchers.IO) {
try {
val data = client.service.send(bookId = bookId, mail = email, format = format)
_sendStatus.emit(true)
@ -72,8 +74,8 @@ class BookDetailViewModel @Inject constructor(
}
private fun toDetailErrorUio(bookId: Int) = BookDetailUioErrorUio.GetDetailInput(
message = stringResource(R.string.error_get_book_detail_message),
action = stringResource(R.string.error_get_book_detail_action),
message = R.string.error_get_book_detail_message,
action = R.string.error_get_book_detail_action,
bookId = bookId,
)
@ -82,8 +84,8 @@ class BookDetailViewModel @Inject constructor(
mail: String,
format: String
) = BookDetailUioErrorUio.SendBookInput(
message = stringResource(R.string.error_send_book_message),
action = stringResource(R.string.error_send_book_action),
message = R.string.error_send_book_message,
action = R.string.error_send_book_action,
bookId = bookId,
mail = mail,
format = format,

View file

@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.UserFactory
import com.pixelized.biblib.utils.extention.capitalize
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -21,7 +22,7 @@ class BookOptionViewModel @Inject constructor(
var emails by mutableStateOf(listOf<OptionUio>())
private set
var formats by mutableStateOf(listOf(OptionUio(Format.EPUB, true), OptionUio(Format.MOBI)))
var formats by mutableStateOf(initialFormat())
private set
init {
@ -51,4 +52,9 @@ class BookOptionViewModel @Inject constructor(
it.copy(selected = it.value == format)
}
}
private fun initialFormat() = listOf(
OptionUio(Format.EPUB.capitalize(), true),
OptionUio(Format.MOBI.capitalize()),
)
}

View file

@ -1,30 +0,0 @@
package com.pixelized.biblib.ui.screen.home.detail
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ConfirmDialogViewModel @Inject constructor(
application: Application,
) : AndroidViewModel(application) {
var dialog by mutableStateOf<ConfirmDialogUio?>(null)
private set
fun show(bookId: Int, email: String, @Format format: String) {
this.dialog = ConfirmDialogUio(
bookId = bookId,
email = email,
format = format,
)
}
fun hide() {
dialog = null
}
}

View file

@ -1,6 +1,7 @@
package com.pixelized.biblib.ui.screen.home.detail
import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@ -19,7 +20,6 @@ import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel
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.flow.collect
import kotlinx.coroutines.launch
import java.io.Serializable
@ -54,22 +54,22 @@ data class BookDetailUio(
@Stable
@Immutable
sealed class BookDetailUioErrorUio(
val message: String,
val action: String?,
@StringRes val message: Int,
@StringRes val action: Int,
) {
@Stable
@Immutable
class GetDetailInput(
message: String,
action: String?,
@StringRes message: Int,
@StringRes action: Int,
val bookId: Int,
) : BookDetailUioErrorUio(message, action)
@Stable
@Immutable
class SendBookInput(
message: String,
action: String?,
@StringRes message: Int,
@StringRes action: Int,
val bookId: Int,
val mail: String,
val format: String,
@ -80,7 +80,6 @@ sealed class BookDetailUioErrorUio(
@Composable
fun DetailScreen(
detailViewModel: BookDetailViewModel = hiltViewModel(),
confirmViewModel: ConfirmDialogViewModel = hiltViewModel(),
profileViewModel: ProfileViewModel = hiltViewModel(),
bookId: Int? = null,
) {
@ -107,8 +106,12 @@ fun DetailScreen(
modifier = Modifier
.navigationBarsPadding()
.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
onHelp = { uri ->
uriHandler.openUri(uri)
},
onSend = { mail, format ->
confirmViewModel.show(bookId = detail.id, email = mail, format = format)
scope.launch { emailSheetState.hide() }
detailViewModel.send(bookId = detail.id, email = mail, format = format)
}
)
},
@ -131,13 +134,6 @@ fun DetailScreen(
mails.isEmpty() -> {
context.showToast(context.getString(R.string.error_no_amazon_email))
}
mails.size == 1 -> {
confirmViewModel.show(
bookId = detail.id,
email = mails.first(),
format = Format.EPUB,
)
}
else -> {
scope.launch { emailSheetState.show() }
}
@ -152,20 +148,6 @@ fun DetailScreen(
}
},
)
ConfirmDialog(
modifier = Modifier.padding(all = MaterialTheme.bibLib.dimen.dp32),
confirmViewModel = confirmViewModel,
onConfirm = { id, email, format ->
scope.launch {
confirmViewModel.hide()
emailSheetState.hide()
detailViewModel.send(bookId = id, email = email, format = format)
}
},
onDismiss = { confirmViewModel.hide() },
onHelp = { url -> uriHandler.openUri(url) }
)
}
}
@ -173,9 +155,8 @@ fun DetailScreen(
LaunchedEffect(key1 = "DetailScreenError") {
detailViewModel.error.collect {
val result = snackBarHost.showSnackbar(
message = it.message,
actionLabel = it.action,
duration = SnackbarDuration.Indefinite,
message = context.getString(it.message),
actionLabel = context.getString(it.action),
)
if (result == SnackbarResult.ActionPerformed) {
when (it) {

View file

@ -1,191 +0,0 @@
package com.pixelized.biblib.ui.screen.home.detail
import android.content.Context.*
import android.content.res.Configuration
import androidx.annotation.StringDef
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib
import com.pixelized.biblib.utils.extention.default
import com.pixelized.biblib.utils.extention.highlight
import com.pixelized.biblib.utils.extention.stringRegex
@Retention(AnnotationRetention.SOURCE)
@StringDef(Format.EPUB, Format.MOBI)
annotation class Format {
companion object {
const val EPUB = "epub"
const val MOBI = "mobi"
}
}
@Stable
@Immutable
data class ConfirmDialogUio(
val bookId: Int,
val email: String,
@Format val format: String,
) {
companion object {
@Composable
fun preview() = ConfirmDialogUio(
bookId = 90,
email = "R.Daneel.Olivaw.Kindle@gmail.com",
format = Format.EPUB,
)
}
}
@Composable
fun ConfirmDialog(
modifier: Modifier = Modifier,
confirmViewModel: ConfirmDialogViewModel,
onConfirm: (bookId: Int, mail: String, format: String) -> Unit = { _, _, _ -> },
onHelp: (url: String) -> Unit = default<String>(),
onDismiss: () -> Unit
) {
confirmViewModel.dialog?.let { dialog ->
ConfirmDialog(
modifier = modifier,
uio = dialog,
onDismissRequest = onDismiss,
onHelp = onHelp,
onConfirm = onConfirm,
)
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ConfirmDialog(
modifier: Modifier = Modifier,
uio: ConfirmDialogUio,
onDismissRequest: () -> Unit = default(),
onHelp: (url: String) -> Unit = default<String>(),
onConfirm: (bookId: Int, mail: String, format: String) -> Unit = { _, _, _ -> },
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = remember { DialogProperties(usePlatformDefaultWidth = false) },
) {
ConfirmDialogContent(
modifier = modifier,
uio = uio,
onDismissRequest = onDismissRequest,
onHelp = onHelp,
onConfirm = onConfirm,
)
}
}
@Composable
fun ConfirmDialogContent(
modifier: Modifier = Modifier,
uio: ConfirmDialogUio,
onDismissRequest: () -> Unit = default(),
onHelp: (url: String) -> Unit = default<String>(),
onConfirm: (bookId: Int, mail: String, format: String) -> Unit = { _, _, _ -> },
) {
val amazonHelpUri = stringResource(R.string.detail_send_confirm_help_url)
Card(
modifier = modifier,
backgroundColor = MaterialTheme.bibLib.colors.dialogBackground,
) {
Column(modifier = Modifier.padding(all = MaterialTheme.bibLib.dimen.dp16)) {
Text(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
style = MaterialTheme.typography.h6,
color = MaterialTheme.bibLib.colors.typography.medium,
text = stringResource(R.string.detail_send_confirm_title)
)
Text(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
style = MaterialTheme.typography.body1,
color = MaterialTheme.bibLib.colors.typography.medium,
text = rememberDescription()
)
Text(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp8),
style = MaterialTheme.typography.caption,
color = MaterialTheme.bibLib.colors.typography.easy,
text = uio.email
)
Text(
modifier = Modifier
.clickable(onClick = { onHelp(amazonHelpUri) })
.padding(vertical = MaterialTheme.bibLib.dimen.dp8),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.caption,
color = MaterialTheme.bibLib.colors.typography.strong,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(R.string.detail_send_confirm_help),
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = MaterialTheme.bibLib.dimen.dp8),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
) {
Button(
colors = ButtonDefaults.outlinedButtonColors(),
onClick = onDismissRequest,
) {
Text(
text = stringResource(R.string.detail_send_confirm_cancel_action)
)
}
Button(
colors = ButtonDefaults.buttonColors(),
onClick = { onConfirm(uio.bookId, uio.email, uio.format) }
) {
Text(
text = stringResource(R.string.detail_send_confirm_confirm_action)
)
}
}
}
}
}
@Composable
private fun rememberDescription(): AnnotatedString {
val email = stringResource(id = R.string.martin_sender)
val description = stringResource(R.string.detail_send_confirm_description, email)
return description.highlight(highlight = stringRegex(email))
}
@Composable
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun DetailConfirmPreview() {
BibLibTheme {
ConfirmDialogContent(
uio = ConfirmDialogUio.preview(),
)
}
}

View file

@ -1,10 +1,11 @@
package com.pixelized.biblib.ui.screen.home.detail
import android.content.res.Configuration
import androidx.annotation.StringDef
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@ -18,13 +19,15 @@ import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib
import com.pixelized.biblib.utils.extention.capitalize
import com.pixelized.biblib.utils.extention.*
@Stable
@Immutable
@ -33,10 +36,20 @@ data class OptionUio(
val selected: Boolean = false,
)
@Retention(AnnotationRetention.SOURCE)
@StringDef(Format.EPUB, Format.MOBI)
annotation class Format {
companion object {
const val EPUB = "epub"
const val MOBI = "mobi"
}
}
@Composable
fun DetailScreenSendOption(
modifier: Modifier = Modifier,
optionViewModel: BookOptionViewModel = hiltViewModel(),
onHelp: (url: String) -> Unit = default<String>(),
onSend: (email: String, format: String) -> Unit = { _, _ -> },
) {
DetailScreenSendOption(
@ -45,6 +58,7 @@ fun DetailScreenSendOption(
formats = optionViewModel.formats,
onEmail = { optionViewModel.selectMail(it.value) },
onFormat = { optionViewModel.selectFormat(it.value) },
onHelp = onHelp,
onSend = onSend,
)
}
@ -54,19 +68,56 @@ fun DetailScreenSendOption(
modifier: Modifier = Modifier,
emails: List<OptionUio>,
formats: List<OptionUio>,
onHelp: (url: String) -> Unit = default<String>(),
onEmail: (OptionUio) -> Unit = { },
onFormat: (OptionUio) -> Unit = { },
onSend: (email: String, format: String) -> Unit = { _, _ -> },
) {
val amazonHelpUri = stringResource(R.string.detail_send_confirm_help_url)
LazyColumn(
modifier = modifier.fillMaxWidth(),
) {
item {
Column(
modifier = Modifier
.padding(top = MaterialTheme.bibLib.dimen.dp16)
.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
) {
Text(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
style = MaterialTheme.typography.h6,
color = MaterialTheme.bibLib.colors.typography.medium,
text = stringResource(R.string.detail_send_confirm_title)
)
Text(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp8),
style = MaterialTheme.typography.caption,
color = MaterialTheme.bibLib.colors.typography.easy,
text = rememberDescription()
)
Text(
modifier = Modifier
.clickable(onClick = { onHelp(amazonHelpUri) })
.padding(vertical = MaterialTheme.bibLib.dimen.dp8),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.caption,
color = MaterialTheme.bibLib.colors.typography.strong,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(R.string.detail_send_confirm_help),
)
}
}
item {
Text(
modifier = Modifier
.padding(vertical = MaterialTheme.bibLib.dimen.dp8)
.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
color = MaterialTheme.colors.primary,
color = MaterialTheme.bibLib.colors.typography.easy,
style = MaterialTheme.typography.caption,
text = stringResource(id = R.string.detail_option_mail),
)
@ -85,7 +136,7 @@ fun DetailScreenSendOption(
modifier = Modifier
.padding(vertical = MaterialTheme.bibLib.dimen.dp8)
.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
color = MaterialTheme.colors.primary,
color = MaterialTheme.bibLib.colors.typography.easy,
style = MaterialTheme.typography.caption,
text = stringResource(id = R.string.detail_option_mail),
)
@ -147,6 +198,18 @@ private fun OptionItem(
}
}
@Composable
private fun rememberDescription(
style: TextStyle = MaterialTheme.typography.caption,
): AnnotatedString {
val email = stringResource(id = R.string.martin_sender)
val description = stringResource(R.string.detail_send_confirm_description, email)
return description.highlight(
defaultStyle = style.toSpanStyle(),
highlight = stringRegex(email),
)
}
@Composable
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)