Prefil BookDetail with availlable detail from the list.

This commit is contained in:
Thomas Andres Gomez 2022-10-20 17:41:54 +02:00
parent af89e153ef
commit 2774f93b6c
17 changed files with 585 additions and 478 deletions

View file

@ -123,6 +123,7 @@ dependencies {
implementation "com.google.accompanist:accompanist-drawablepainter:0.26.5-rc"
implementation "com.google.accompanist:accompanist-insets:0.26.5-rc"
implementation "com.google.accompanist:accompanist-pager:0.26.5-rc"
implementation "com.google.accompanist:accompanist-placeholder-material:0.26.5-rc"
// Landscapist
implementation "com.github.skydoves:landscapist-glide:1.5.2"

View file

@ -23,6 +23,10 @@ interface BookDao {
@Query("SELECT * FROM ${BookDbo.TABLE} ORDER BY ${BookDbo.SORT}")
fun getBook(): DataSource.Factory<Int, BookRelation>
@Transaction
@Query("SELECT * FROM ${BookDbo.TABLE} WHERE ${BookDbo.ID} LIKE :id")
fun getBook(id: Int): BookRelation
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg books: BookDbo)

View file

@ -6,8 +6,8 @@ import com.pixelized.biblib.database.BibLibDatabase
import com.pixelized.biblib.database.crossref.BookAuthorCrossRef
import com.pixelized.biblib.database.crossref.BookGenreCrossRef
import com.pixelized.biblib.database.data.*
import com.pixelized.biblib.model.book.*
import com.pixelized.biblib.database.factory.*
import com.pixelized.biblib.model.book.*
import javax.inject.Inject
class BookRepository @Inject constructor(
@ -17,6 +17,9 @@ class BookRepository @Inject constructor(
override fun getAll(): List<Book> =
database.bookDao().getAll().map { it.toBook() }
override fun getBook(id: Int): Book =
database.bookDao().getBook(id = id).toBook()
override fun getBookCount(): Int =
database.bookDao().count()

View file

@ -7,6 +7,8 @@ interface IBookRepository {
fun getAll(): List<Book>
fun getBook(id: Int): Book
fun getBookCount(): Int
fun getNewsSource(): DataSource.Factory<Int, Book>

View file

@ -19,8 +19,8 @@ import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
sealed class StateUio<T> {
class Progress<T>(val progress: Float? = null) : StateUio<T>()
class Failure<T>(val exception: Exception) : StateUio<T>()
class Progress<T>(val progress: Float? = null, val cache : T? = null) : StateUio<T>()
class Failure<T>(val exception: Exception, val cache: T? = null) : StateUio<T>()
class Success<T>(val value: T) : StateUio<T>()
}

View file

@ -11,13 +11,9 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.ui.screen.home.detail.BookDetailUio
import com.pixelized.biblib.ui.screen.home.detail.BookDetailViewModel
import com.pixelized.biblib.ui.screen.home.detail.DetailScreen
import com.pixelized.biblib.ui.theme.color.ShadowPalette
import com.pixelized.biblib.utils.extention.showToast
import kotlinx.coroutines.launch
val LocalDetailBottomSheetState = staticCompositionLocalOf<DetailBottomSheetState> {
@ -39,10 +35,7 @@ fun DetailBottomSheet(
scrimColor = ShadowPalette.scrim,
sheetState = bottomDetailState.bottomSheetState,
sheetContent = {
DetailScreen(
detailViewModel = bottomDetailState.viewModel,
detail = bottomDetailState.bookDetail,
)
DetailScreen(bookId = bottomDetailState.bookId)
},
content = content,
)
@ -58,48 +51,34 @@ fun DetailBottomSheet(
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun rememberDetailBottomSheetState(
viewModel: BookDetailViewModel = hiltViewModel(),
bottomSheetState: ModalBottomSheetState = rememberModalBottomSheetState(
initialValue = Hidden,
skipHalfExpanded = true,
),
): DetailBottomSheetState {
val context: Context = LocalContext.current
val detail = rememberSaveable(viewModel, bottomSheetState) {
mutableStateOf<BookDetailUio?>(null)
val detail = rememberSaveable(bottomSheetState) {
mutableStateOf<Int?>(null)
}
return remember(bottomSheetState) {
DetailBottomSheetState(
bottomSheetState = bottomSheetState,
bookDetail = detail,
)
}
val controller = DetailBottomSheetState(
context = context,
viewModel = viewModel,
bottomSheetState = bottomSheetState,
bookDetail = detail,
)
return remember(viewModel, bottomSheetState) { controller }
}
@OptIn(ExperimentalMaterialApi::class)
@Stable
class DetailBottomSheetState constructor(
private val context: Context,
val viewModel: BookDetailViewModel,
val bottomSheetState: ModalBottomSheetState,
bookDetail: MutableState<BookDetailUio?>,
bookDetail: MutableState<Int?>,
) {
var bookDetail: BookDetailUio? by bookDetail
var bookId: Int? by bookDetail
private set
suspend fun expandBookDetail(id: Int) {
when (val book = viewModel.getDetail(id)) {
is StateUio.Failure -> {
val mes = book.exception.message ?: context.getString(R.string.error_generic)
context.showToast(message = mes)
}
is StateUio.Success -> {
bookDetail = book.value
bottomSheetState.show()
}
else -> Unit
}
bookId = id
bottomSheetState.show()
}
suspend fun collapse() {

View file

@ -1,36 +1,62 @@
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
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.repository.book.BookRepository
import com.pixelized.biblib.ui.screen.home.detail.BookDetailUioErrorUio.Type
import com.pixelized.biblib.utils.extention.toDetailUio
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class BookDetailViewModel @Inject constructor(
private val bookRepository: BookRepository,
private val client: IBibLibClient,
) : ViewModel() {
suspend fun getDetail(id: Int): StateUio<BookDetailUio> {
return withContext(Dispatchers.IO) {
try {
val book = getBookDetail(id = id)
StateUio.Success(book)
} catch (exception: Exception) {
Log.e("BookDetailViewModel", exception.message, exception)
StateUio.Failure(exception)
private val _error = MutableSharedFlow<BookDetailUioErrorUio>()
val error: Flow<BookDetailUioErrorUio> get() = _error
fun getDetail(id: Int?): State<BookDetailUio?> {
return mutableStateOf<BookDetailUio?>(null).apply {
viewModelScope.launch(Dispatchers.IO) {
try {
requireNotNull(id)
value = getCacheBookDetail(id = id)
value = getBookDetail(id = id)
} catch (exception: Exception) {
_error.emit(exception.toUio(Type.GET_DETAIL))
}
}
}
}
suspend fun send(bookId: Int, email: String) {
val data = client.service.send(bookId = bookId, mail = email)
Log.d("send", data.toString())
suspend fun send(bookId: Int, email: String): State<Boolean?> {
return mutableStateOf<Boolean?>(null).apply {
viewModelScope.launch(Dispatchers.IO) {
try {
val data = client.service.send(bookId = bookId, mail = email)
Log.d("send", data.toString())
} catch (exception: Exception) {
Log.d("send", exception.message, exception)
_error.emit(exception.toUio(Type.SEND_BOOK))
}
}
}
}
private fun getCacheBookDetail(id: Int): BookDetailUio {
return bookRepository.getBook(id = id).toDetailUio()
}
private suspend fun getBookDetail(id: Int): BookDetailUio {
@ -39,4 +65,9 @@ class BookDetailViewModel @Inject constructor(
val book = factory.fromDetailResponseToBook(response)
return book.toDetailUio()
}
private fun Exception.toUio(type: Type) = BookDetailUioErrorUio(
type = type,
message = this.message ?: "An error occurred."
)
}

View file

@ -1,39 +1,33 @@
package com.pixelized.biblib.ui.screen.home.detail
import androidx.compose.runtime.derivedStateOf
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.AndroidViewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.utils.extention.context
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ConfirmDialogViewModel @Inject constructor() : ViewModel() {
val shouldDisplayConfirmDialog by derivedStateOf { dialog != null }
class ConfirmDialogViewModel @Inject constructor(
application: Application,
) : AndroidViewModel(application) {
var dialog by mutableStateOf<ConfirmDialogUio?>(null)
private set
fun show(
email: String,
title: String? = null,
description: String? = null,
help: String? = null,
options: List<ConfirmDialogUio.Option>,
) = show(
ConfirmDialogUio(
fun show(email: String) {
this.dialog = ConfirmDialogUio(
email = email,
title = title,
description = description,
help = help,
options = options,
title = context.getString(R.string.detail_send_confirm_title),
description = context.getString(R.string.detail_send_confirm_description),
help = context.getString(R.string.detail_send_confirm_help),
helpUrl = context.getString(R.string.detail_send_confirm_help_url),
confirm = context.getString(R.string.detail_send_confirm_confirm_action),
cancel = context.getString(R.string.detail_send_confirm_cancel_action),
)
)
fun show(dialog: ConfirmDialogUio?) {
this.dialog = dialog
}
fun hide() {

View file

@ -1,49 +1,23 @@
package com.pixelized.biblib.ui.screen.home.detail
import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.text.HtmlCompat
import androidx.core.text.toSpannable
import androidx.compose.ui.platform.LocalUriHandler
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.SpannedText
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
import com.pixelized.biblib.ui.composable.isSuccessful
import com.pixelized.biblib.ui.scaffold.DetailBottomSheetState
import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState
import com.pixelized.biblib.ui.screen.home.detail.ConfirmDialogUio.Companion.option
import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.theme.color.ShadowPalette
import com.pixelized.biblib.utils.extention.bibLib
import com.pixelized.biblib.utils.extention.default
import com.pixelized.biblib.utils.extention.showToast
import com.skydoves.landscapist.CircularReveal
import com.skydoves.landscapist.glide.GlideImage
import kotlinx.coroutines.launch
import java.io.Serializable
@ -59,332 +33,138 @@ data class BookDetailUio(
val series: String?,
val description: String,
val cover: String,
) : Serializable
) : Serializable {
companion object {
fun preview() = BookDetailUio(
id = 90,
title = "Foundation",
author = "Asimov",
date = "1951",
series = "Foundation - 1",
description = "", // "En ce début de treizième millénaire, l'Empire n'a jamais été aussi puissant, aussi étendu à travers toute la galaxie. C'est dans sa capitale, Trantor, que l'éminent savant Hari Seldon invente la psychohistoire, une science nouvelle permettant de prédire l'avenir. Grâce à elle, Seldon prévoit l'effondrement de l'Empire d'ici cinq siècles, suivi d'une ère de ténèbres de trente mille ans. Réduire cette période à mille ans est peut-être possible, à condition de mener à terme son projet : la Fondation, chargée de rassembler toutes les connaissances humaines. Une entreprise visionnaire qui rencontre de nombreux et puissants détracteurs...",
rating = 4.5f,
language = "Français",
cover = "",
)
}
}
private const val CONFIRM_OPTION_YES = 0
private const val CONFIRM_OPTION_NO = 1
@Stable
@Immutable
data class BookDetailUioErrorUio(
val type: Type,
val message: String,
) {
enum class Type {
GET_DETAIL,
SEND_BOOK,
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun DetailScreen(
detailState: DetailBottomSheetState = LocalDetailBottomSheetState.current,
detailViewModel: BookDetailViewModel = hiltViewModel(),
confirmViewModel: ConfirmDialogViewModel = hiltViewModel(),
profileViewModel: ProfileViewModel = hiltViewModel(),
detailViewModel: BookDetailViewModel = hiltViewModel(),
detail: BookDetailUio? = null,
bookId: Int? = null,
) {
val detailState = LocalDetailBottomSheetState.current
val uriHandler = LocalUriHandler.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
val emailSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
skipHalfExpanded = true,
)
ModalBottomSheetLayout(
sheetState = emailSheetState,
scrimColor = ShadowPalette.scrim,
sheetContent = {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 1.dp)
.animateContentSize()
) {
val user = profileViewModel.user
if (user.isSuccessful()) {
val bookDetailState: State<BookDetailUio?> = rememberSaveable(bookId) {
detailViewModel.getDetail(bookId)
}
when (val detail = bookDetailState.value) {
null -> EmptyDetail()
else -> {
ModalBottomSheetLayout(
modifier = Modifier.fillMaxSize(),
sheetState = emailSheetState,
scrimColor = ShadowPalette.scrim,
sheetContent = {
DetailScreenEmailList(
modifier = Modifier
.navigationBarsPadding()
.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
emails = user.value.amazonEmails,
profileViewModel = profileViewModel,
onEmail = { mail ->
confirmViewModel.show(email = mail)
}
)
}
}
},
content = {
if (detailState.bottomSheetState.isVisible && detail != null) {
DetailScreenContent(
modifier = Modifier.fillMaxSize(),
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))
}
mails.size == 1 -> {
confirmViewModel.show(email = mails.first())
}
else -> {
scope.launch { emailSheetState.show() }
},
content = {
if (detailState.bottomSheetState.isVisible) {
DetailScreenContent(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.systemBarsPadding()
.padding(all = MaterialTheme.bibLib.dimen.dp16)
.animateContentSize(),
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))
}
mails.size == 1 -> {
confirmViewModel.show(email = mails.first())
}
else -> {
scope.launch { emailSheetState.show() }
}
}
} else {
// TODO()
}
}
} else {
// TODO()
}
}
)
} else {
Box(modifier = Modifier.fillMaxSize())
}
},
)
ConfirmDialogHandler(
detail = detail,
confirmViewModel = confirmViewModel,
onConfirm = { bookId, email ->
scope.launch {
confirmViewModel.hide()
emailSheetState.hide()
detailViewModel.send(bookId = bookId, email = email)
}
},
onDismiss = {
confirmViewModel.hide()
}
)
}
@Composable
fun ConfirmDialogHandler(
detail: BookDetailUio? = null,
confirmViewModel: ConfirmDialogViewModel,
onConfirm: (bookId: Int, mail: String) -> Unit = { _, _ -> },
onDismiss: () -> Unit,
) {
val context = LocalContext.current
confirmViewModel.dialog?.let { dialog ->
ConfirmDialog(
uio = dialog,
onDismissRequest = onDismiss,
onOption = { option ->
when (option.id) {
CONFIRM_OPTION_YES -> detail?.id?.let { onConfirm(it, dialog.email) }
else -> onDismiss()
}
},
onHelp = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(dialog.help))
context.startActivity(intent)
},
)
}
}
@Composable
private fun DetailScreenContent(
modifier: Modifier = Modifier,
book: BookDetailUio,
onMobi: () -> Unit = default(),
onEpub: () -> Unit = default(),
onSend: () -> Unit = default(),
) {
AnimatedDelayer(
targetState = book,
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.systemBarsPadding()
.padding(all = MaterialTheme.bibLib.dimen.dp16)
.then(modifier)
) {
GlideImage(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MaterialTheme.bibLib.dimen.dp16)
.height(MaterialTheme.bibLib.dimen.detail.cover),
loading = {
Box(modifier = Modifier.matchParentSize()) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
} else {
EmptyDetail()
}
},
previewPlaceholder = R.drawable.ic_fondatoin_cover,
circularReveal = CircularReveal(duration = 1000),
contentScale = ContentScale.FillHeight,
imageModel = book.cover,
)
Row(modifier = Modifier.padding(vertical = MaterialTheme.bibLib.dimen.dp16)) {
AnimatedOffset(
modifier = Modifier.weight(1f),
) {
Button(
modifier = Modifier.fillMaxSize(),
onClick = onMobi,
) {
Icon(imageVector = Icons.Default.Download, contentDescription = null)
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.dp4))
Text(text = stringResource(id = R.string.action_mobi))
ConfirmDialog(
modifier = Modifier.padding(all = MaterialTheme.bibLib.dimen.dp32),
confirmViewModel = confirmViewModel,
detail = detail,
onConfirm = { id, email ->
scope.launch {
confirmViewModel.hide()
emailSheetState.hide()
detailViewModel.send(bookId = id, email = email)
}
}
},
onDismiss = { confirmViewModel.hide() },
onHelp = { url -> uriHandler.openUri(url) }
)
}
}
Spacer(modifier = Modifier.padding(all = MaterialTheme.bibLib.dimen.dp4))
AnimatedOffset(
modifier = Modifier.weight(1f),
) {
Button(
modifier = Modifier.fillMaxSize(),
onClick = onEpub,
) {
Icon(imageVector = Icons.Default.Download, contentDescription = null)
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.dp4))
Text(text = stringResource(id = R.string.action_epub))
}
}
Spacer(modifier = Modifier.padding(all = MaterialTheme.bibLib.dimen.dp4))
AnimatedOffset(
modifier = Modifier.weight(1f),
) {
Button(
modifier = Modifier.fillMaxSize(),
onClick = onSend,
) {
Icon(imageVector = Icons.Default.Send, contentDescription = "")
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.dp4))
Text(text = stringResource(id = R.string.action_send))
}
}
}
AnimatedOffset(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(bottom = 4.dp),
) {
Text(
style = MaterialTheme.typography.h5,
color = MaterialTheme.colors.onSurface,
text = book.title,
)
}
AnimatedOffset(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
) {
Text(
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onSurface,
text = book.author,
)
}
Row(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp8),
) {
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
title = stringResource(id = R.string.detail_rating),
label = book.rating.toString(),
)
}
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
title = stringResource(id = R.string.detail_language),
label = book.language,
)
}
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
title = stringResource(id = R.string.detail_release),
label = book.date ?: "-",
)
}
}
AnimatedOffset(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
) {
TitleLabel(
title = stringResource(id = R.string.detail_series),
label = book.series ?: "-",
)
}
AnimatedOffset {
SpannedText(
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface,
text = HtmlCompat.fromHtml(
book.description,
HtmlCompat.FROM_HTML_MODE_COMPACT
).toSpannable(),
)
}
LaunchedEffect(key1 = "DetailScreenError") {
detailViewModel.error.collect {
context.showToast(message = it.message)
}
}
}
@Composable
private fun TitleLabel(
title: String,
label: String,
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
text = title,
)
Text(
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface,
text = label,
)
}
}
private fun ConfirmDialogViewModel.show(email: String) = show(
email = email,
title = "Envoyer sur votre Kindle",
description = "Assurez-vous que votre Kindle dispose d'une connexion Internet et que l'adresse e-mail suivante est correctement configurée.",
help = "https://www.amazon.fr/gp/help/customer/display.html?nodeId=G7NECT4B4ZWHQ8WV",
options = listOf(
option(id = CONFIRM_OPTION_YES, label = "Oui"),
option(id = CONFIRM_OPTION_NO, label = "Non")
)
)
@Composable
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
private fun DetailScreenContentPreview() {
val book = BookDetailUio(
id = 90,
title = "Foundation",
author = "Asimov",
date = "1951",
series = "Foundation - 1",
description = "En ce début de treizième millénaire, l'Empire n'a jamais été aussi puissant, aussi étendu à travers toute la galaxie. C'est dans sa capitale, Trantor, que l'éminent savant Hari Seldon invente la psychohistoire, une science nouvelle permettant de prédire l'avenir. Grâce à elle, Seldon prévoit l'effondrement de l'Empire d'ici cinq siècles, suivi d'une ère de ténèbres de trente mille ans. Réduire cette période à mille ans est peut-être possible, à condition de mener à terme son projet : la Fondation, chargée de rassembler toutes les connaissances humaines. Une entreprise visionnaire qui rencontre de nombreux et puissants détracteurs...",
rating = 4.5f,
language = "Français",
cover = "",
)
BibLibTheme {
DetailScreenContent(book = book)
}
}
private fun EmptyDetail(
modifier : Modifier = Modifier,
) = Box(
modifier = modifier.fillMaxSize()
)

View file

@ -7,11 +7,18 @@ 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.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 com.pixelized.biblib.ui.screen.home.detail.ConfirmDialogUio.Companion.option
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
@ -20,99 +27,133 @@ import com.pixelized.biblib.utils.extention.default
@Immutable
data class ConfirmDialogUio(
val email: String,
val title: String? = null,
val description: String? = null,
val help: String? = null,
val options: List<Option>,
val title: String,
val description: String,
val help: String,
val helpUrl: String,
val confirm: String,
val cancel: String,
) {
@Stable
@Immutable
data class Option(
val id: Int,
val label: String,
)
companion object {
fun option(
id: Int,
label: String,
) = Option(
id = id,
label = label
@Composable
fun preview() = ConfirmDialogUio(
email = "R.Daneel.Olivaw.Kindle@gmail.com",
title = stringResource(R.string.detail_send_confirm_title),
description = stringResource(R.string.detail_send_confirm_description),
help = stringResource(R.string.detail_send_confirm_help),
helpUrl = stringResource(R.string.detail_send_confirm_help_url),
confirm = stringResource(R.string.detail_send_confirm_confirm_action),
cancel = stringResource(R.string.detail_send_confirm_cancel_action),
)
}
}
@Composable
fun ConfirmDialog(
modifier: Modifier = Modifier,
confirmViewModel: ConfirmDialogViewModel,
detail: BookDetailUio,
onConfirm: (bookId: Int, mail: String) -> Unit = { _, _ -> },
onHelp: (url: String) -> Unit = default<String>(),
onDismiss: () -> Unit
) {
confirmViewModel.dialog?.let { dialog ->
ConfirmDialog(
modifier = modifier,
uio = dialog,
onDismissRequest = onDismiss,
onConfirm = { onConfirm(detail.id, dialog.email) },
onHelp = onHelp,
)
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ConfirmDialog(
modifier: Modifier = Modifier,
uio: ConfirmDialogUio,
onDismissRequest: () -> Unit = default(),
onOption: (ConfirmDialogUio.Option) -> Unit = default<ConfirmDialogUio.Option>(),
onHelp: () -> Unit = default(),
onHelp: (url: String) -> Unit = default<String>(),
onConfirm: (mail: String) -> Unit = default<String>(),
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = remember { DialogProperties(usePlatformDefaultWidth = false) },
) {
Card(modifier = modifier) {
Column(modifier = Modifier.padding(all = MaterialTheme.bibLib.dimen.dp16)) {
if (uio.title != null) {
Text(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
style = MaterialTheme.typography.h6,
color = MaterialTheme.bibLib.colors.typography.medium,
text = uio.title
)
}
ConfirmDialogContent(
modifier = modifier,
uio = uio,
onDismissRequest = onDismissRequest,
onHelp = onHelp,
onConfirm = onConfirm,
)
}
}
if (uio.description != null) {
Text(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
style = MaterialTheme.typography.body1,
color = MaterialTheme.bibLib.colors.typography.medium,
text = uio.description
)
}
@Composable
fun ConfirmDialogContent(
modifier: Modifier = Modifier,
uio: ConfirmDialogUio,
onDismissRequest: () -> Unit = default(),
onHelp: (url: String) -> Unit = default<String>(),
onConfirm: (mail: String) -> Unit = default<String>(),
) {
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 = uio.title
)
Text(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
style = MaterialTheme.typography.caption,
color = MaterialTheme.bibLib.colors.typography.easy,
text = uio.email
)
Text(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
style = MaterialTheme.typography.body1,
color = MaterialTheme.bibLib.colors.typography.medium,
text = uio.description
)
if (uio.help != null) {
Text(
modifier = Modifier
.clickable(onClick = onHelp)
.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
style = MaterialTheme.typography.caption,
color = MaterialTheme.bibLib.colors.typography.strong,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = uio.help
)
}
Text(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp8),
style = MaterialTheme.typography.caption,
color = MaterialTheme.bibLib.colors.typography.easy,
text = uio.email
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
Text(
modifier = Modifier
.clickable(onClick = { onHelp(uio.helpUrl) })
.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 = uio.help
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = MaterialTheme.bibLib.dimen.dp8),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
) {
Button(
colors = ButtonDefaults.outlinedButtonColors(),
onClick = onDismissRequest,
) {
uio.options.reversed().forEachIndexed { index, option ->
Button(
modifier = when (index) {
uio.options.lastIndex -> Modifier
else -> Modifier.padding(end = MaterialTheme.bibLib.dimen.dp8)
},
colors = when (index) {
uio.options.lastIndex -> ButtonDefaults.buttonColors()
else -> ButtonDefaults.outlinedButtonColors()
},
onClick = { onOption(option) }
) {
Text(text = option.label)
}
}
Text(text = uio.cancel)
}
Button(
colors = ButtonDefaults.buttonColors(),
onClick = { onConfirm(uio.email) }
) {
Text(text = uio.confirm)
}
}
}
@ -124,17 +165,8 @@ fun ConfirmDialog(
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun DetailConfirmPreview() {
BibLibTheme {
ConfirmDialog(
uio = ConfirmDialogUio(
email = "R.Daneel.Olivaw.Kindle@gmail.com",
title = "Envoyer sur votre Kindle",
description = "Assurez-vous que votre Kindle dispose d'une connexion Internet et que l'adresse e-mail suivante est correctement configurée.",
help = "https://www.amazon.fr/gp/help/customer/display.html?nodeId=G7NECT4B4ZWHQ8WV",
options = listOf(
option(id = 0, "Oui"),
option(id = 1, "Non")
)
)
ConfirmDialogContent(
uio = ConfirmDialogUio.preview(),
)
}
}

View file

@ -0,0 +1,224 @@
package com.pixelized.biblib.ui.screen.home.detail
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
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.material.placeholder
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.SpannedText
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib
import com.pixelized.biblib.utils.extention.default
import com.skydoves.landscapist.CircularReveal
import com.skydoves.landscapist.glide.GlideImage
@Composable
fun DetailScreenContent(
modifier: Modifier = Modifier,
book: BookDetailUio,
onMobi: () -> Unit = default(),
onEpub: () -> Unit = default(),
onSend: () -> Unit = default(),
) {
AnimatedDelayer(
targetState = book.id,
) {
Column(
modifier = modifier,
) {
GlideImage(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MaterialTheme.bibLib.dimen.dp16)
.height(MaterialTheme.bibLib.dimen.detail.cover),
previewPlaceholder = R.drawable.ic_fondatoin_cover,
circularReveal = CircularReveal(duration = 1000),
contentScale = ContentScale.FillHeight,
imageModel = book.cover,
)
Row(modifier = Modifier.padding(vertical = MaterialTheme.bibLib.dimen.dp16)) {
AnimatedOffset(
modifier = Modifier.weight(1f),
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onMobi,
) {
Icon(imageVector = Icons.Default.Download, contentDescription = null)
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.dp4))
Text(text = stringResource(id = R.string.action_mobi))
}
}
Spacer(modifier = Modifier.padding(all = MaterialTheme.bibLib.dimen.dp4))
AnimatedOffset(
modifier = Modifier.weight(1f),
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onEpub,
) {
Icon(imageVector = Icons.Default.Download, contentDescription = null)
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.dp4))
Text(text = stringResource(id = R.string.action_epub))
}
}
Spacer(modifier = Modifier.padding(all = MaterialTheme.bibLib.dimen.dp4))
AnimatedOffset(
modifier = Modifier.weight(1f),
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onSend,
) {
Icon(imageVector = Icons.Default.Send, contentDescription = "")
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.dp4))
Text(text = stringResource(id = R.string.action_send))
}
}
}
AnimatedOffset(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(bottom = 4.dp),
) {
Text(
style = MaterialTheme.typography.h5,
color = MaterialTheme.colors.onSurface,
text = book.title,
)
}
AnimatedOffset(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
) {
Text(
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onSurface,
text = book.author,
)
}
Row(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp8),
) {
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
modifier = Modifier.fillMaxWidth(),
title = stringResource(id = R.string.detail_rating),
label = book.rating.toString(),
)
}
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
modifier = Modifier.fillMaxWidth(),
title = stringResource(id = R.string.detail_language),
label = book.language,
)
}
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
modifier = Modifier.fillMaxWidth(),
title = stringResource(id = R.string.detail_release),
label = book.date ?: "-",
)
}
}
AnimatedOffset(
modifier = Modifier.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
) {
TitleLabel(
modifier = Modifier.fillMaxWidth(),
title = stringResource(id = R.string.detail_series),
label = book.series ?: "-",
)
}
AnimatedOffset {
SpannedText(
modifier = Modifier
.fillMaxWidth()
.placeholder(visible = book.description.isEmpty()),
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface,
text = HtmlCompat.fromHtml(
book.description.ifEmpty { "placeholder" },
HtmlCompat.FROM_HTML_MODE_COMPACT,
).toSpannable(),
)
}
}
}
}
@Composable
private fun TitleLabel(
modifier: Modifier = Modifier,
title: String,
label: String,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface,
text = title,
)
Text(
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface,
text = label,
)
}
}
@Composable
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO, heightDp = 1000)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES, heightDp = 1000)
private fun DetailScreenContentPreview() {
BibLibTheme {
DetailScreenContent(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(all = MaterialTheme.bibLib.dimen.dp16),
book = BookDetailUio.preview(),
)
}
}

View file

@ -1,6 +1,7 @@
package com.pixelized.biblib.ui.screen.home.detail
import android.content.res.Configuration
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@ -15,10 +16,37 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.ui.composable.isSuccessful
import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib
import com.pixelized.biblib.utils.extention.default
@Composable
fun DetailScreenEmailList(
modifier: Modifier = Modifier,
profileViewModel: ProfileViewModel,
onEmail: (email: String) -> Unit = default<String>(),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 1.dp)
.animateContentSize()
) {
val user = profileViewModel.user
if (user.isSuccessful()) {
DetailScreenEmailList(
modifier = modifier,
emails = user.value.amazonEmails,
onEmail = onEmail,
)
}
}
}
@Composable
fun DetailScreenEmailList(
modifier: Modifier = Modifier,

View file

@ -14,6 +14,7 @@ data class BibLibColor(
val base: Colors,
val typography: Typography,
val placeHolder: Color,
val dialogBackground: Color,
) {
@Stable
@ -40,10 +41,12 @@ fun bibLibDarkColors(
strong = base.primary,
),
placeHolder: Color = BibLibColorPalette.DarkGrey,
dialogBackground: Color = BibLibColorPalette.VeryDarkGrey,
) = BibLibColor(
base = base,
typography = typography,
placeHolder = placeHolder,
dialogBackground = dialogBackground,
)
fun bibLibLightColors(
@ -61,8 +64,10 @@ fun bibLibLightColors(
strong = base.primary,
),
placeHolder: Color = BibLibColorPalette.LightGrey,
dialogBackground: Color = Color.White,
) = BibLibColor(
base = base,
typography = typography,
placeHolder = placeHolder,
dialogBackground = dialogBackground,
)

View file

@ -15,6 +15,7 @@ data class BibLibDimen(
val dp8: Dp = 8.dp,
val dp12: Dp = 12.dp,
val dp16: Dp = 16.dp,
val dp24: Dp = 24.dp,
val dp32: Dp = 32.dp,
val dp48: Dp = 48.dp,
val dp52: Dp = 52.dp,

View file

@ -1,6 +1,15 @@
package com.pixelized.biblib.utils.extention
import android.content.Context
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
val AndroidViewModel.context: Context get() = this.getApplication()
val AndroidViewModel.context: Context get() = this.getApplication()
fun AndroidViewModel.stringResource(@StringRes id: Int): String {
return context.getString(id)
}
fun AndroidViewModel.stringResource(@StringRes id: Int, vararg formatArgs: Any): String {
return context.getString(id, formatArgs)
}

View file

@ -42,6 +42,13 @@
<string name="detail_series">Séries</string>
<string name="detail_emails_title">Envoyer cet eBook à :</string>
<string name="detail_send_confirm_title">Envoyer sur votre Kindle</string>
<string name="detail_send_confirm_description">Assurez-vous que votre Kindle dispose d\'une connexion Internet et que l\'adresse e-mail suivante est correctement configurée.</string>
<string name="detail_send_confirm_help">Aide à la configuration du kindle</string>
<string name="detail_send_confirm_help_url">https://www.amazon.fr/gp/help/customer/display.html?nodeId=G7NECT4B4ZWHQ8WV</string>
<string name="detail_send_confirm_confirm_action">Oui</string>
<string name="detail_send_confirm_cancel_action">Annuler</string>
<string name="search_title">Rechercher sur Biblib</string>
<string name="search_filter_title">Rechercher</string>
<string name="search_filter_param">%1$s : %2$s</string>

View file

@ -48,6 +48,13 @@
<string name="detail_series">Series</string>
<string name="detail_emails_title">Send this eBook to:</string>
<string name="detail_send_confirm_title">Send to your Kindle</string>
<string name="detail_send_confirm_description">Make sure your Kindle has an internet connection and the following email address is correctly set up.</string>
<string name="detail_send_confirm_help">Help me configure my kindle</string>
<string name="detail_send_confirm_help_url">https://www.amazon.co.uk/gp/help/customer/display.html?nodeId=G7NECT4B4ZWHQ8WV</string>
<string name="detail_send_confirm_confirm_action">Oui</string>
<string name="detail_send_confirm_cancel_action">Annuler</string>
<string name="search_title">Search on Biblib</string>
<string name="search_filter_title">Search</string>
<string name="search_filter_param">%1$s: %2$s</string>