Simplify the detail page. & small code clean up

This commit is contained in:
Thomas Andres Gomez 2023-03-31 15:37:32 +02:00
parent 100f8d32bd
commit 9c8d05e578
9 changed files with 428 additions and 356 deletions

View file

@ -86,9 +86,6 @@ android {
} }
dependencies { dependencies {
// Kotlin core
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.10"
// Android core // Android core
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.7.0-alpha02' implementation 'androidx.appcompat:appcompat:1.7.0-alpha02'

View file

@ -4,6 +4,7 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@Composable @Composable
fun HandleBottomSheetData( fun HandleBottomSheetData(
@ -34,12 +35,22 @@ fun HandleBottomSheetDismiss(
val currentOnDismiss by rememberUpdatedState( val currentOnDismiss by rememberUpdatedState(
newValue = onDismiss, newValue = onDismiss,
) )
val haveBeenDismissed by remember(bottomSheetState) { // Showing a bottom sheet produce a :
derivedStateOf { // - currentValue == Hidden targetValue == Hidden
bottomSheetState.currentValue != ModalBottomSheetValue.Hidden && bottomSheetState.targetValue == ModalBottomSheetValue.Hidden // before a :
// - currentValue == Hidden targetValue == Expanded
// We need to keep track of this behavior to avoid closing the bottom sheet immediately.
var skipLaunchEffect by rememberSaveable {
mutableStateOf(true)
}
LaunchedEffect(bottomSheetState.currentValue) {
if (skipLaunchEffect.not()) {
if (bottomSheetState.currentValue == ModalBottomSheetValue.Hidden && bottomSheetState.targetValue == ModalBottomSheetValue.Hidden) {
currentOnDismiss()
skipLaunchEffect = true
}
} else {
skipLaunchEffect = false
} }
} }
if (haveBeenDismissed) {
currentOnDismiss()
}
} }

View file

@ -11,7 +11,6 @@ import com.pixelized.biblib.model.book.Book
import com.pixelized.biblib.model.search.FilterType import com.pixelized.biblib.model.search.FilterType
import com.pixelized.biblib.model.search.SortType import com.pixelized.biblib.model.search.SortType
import com.pixelized.biblib.model.search.SortValue import com.pixelized.biblib.model.search.SortValue
import com.pixelized.biblib.repository.book.BookRepository
import com.pixelized.biblib.repository.book.IBookRepository import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.repository.search.ISearchRepository import com.pixelized.biblib.repository.search.ISearchRepository
import com.pixelized.biblib.ui.screen.home.common.item.LargeBookThumbnailUio import com.pixelized.biblib.ui.screen.home.common.item.LargeBookThumbnailUio
@ -105,142 +104,102 @@ class BookSearchViewModel @Inject constructor(
} }
} }
fun sort(sortBy: Map<SortType, SortValue?>) { fun source(clean: Boolean = false, block: SortAndFilterScope.() -> Unit) {
this.sortBy = sortBy val scope = if (clean) {
SortAndFilterScope(
sortBy = mapOf(),
isNew = null,
title = null,
author = null,
authorId = null,
series = null,
seriesId = null,
genre = null,
genreId = null,
language = null,
languageId = null,
)
} else {
SortAndFilterScope(
sortBy = sortBy,
isNew = filterByNew,
title = search.value,
author = authorFilter.value.label,
authorId = filterByAuthorId,
series = seriesFilter.value.label,
seriesId = filterBySeriesId,
genre = genreFilter.value.label,
genreId = filterByGenreId,
language = languageFilter.value.label,
languageId = filterByLanguageId,
)
}
scope.block()
// filter the search
if (_search.value != scope.title) {
_search.value = scope.title
}
// clean the new filter
if (filterByNew != scope.isNew) {
filterByNew = scope.isNew
newFilter.value = newFilter.value.copy(
selected = scope.isNew == true,
closable = scope.isNew == true,
)
}
// clean the author filter
if (filterByAuthorId != scope.authorId) {
filterByAuthorId = scope.authorId
authorFilter.value = authorFilter.value.copy(
selected = scope.authorId != null,
label = scope.author,
closable = scope.authorId != null,
openable = scope.authorId == null,
)
}
// clean the author filter
if (filterBySeriesId != scope.seriesId) {
filterBySeriesId = scope.seriesId
seriesFilter.value = seriesFilter.value.copy(
selected = scope.seriesId != null,
label = scope.series,
closable = scope.seriesId != null,
openable = scope.seriesId == null,
)
}
// clean the author filter
if (filterByGenreId != scope.genreId) {
filterByGenreId = scope.genreId
genreFilter.value = genreFilter.value.copy(
selected = scope.genreId != null,
label = scope.genre,
closable = scope.genreId != null,
openable = scope.genreId == null,
)
}
// clean the author filter
if (filterByLanguageId != scope.languageId) {
filterByLanguageId = scope.languageId
languageFilter.value = languageFilter.value.copy(
selected = scope.languageId != null,
label = scope.language,
closable = scope.languageId != null,
openable = scope.languageId == null,
)
}
// apply the sorting
if (sortBy != scope.sortBy) {
sortBy = scope.sortBy
}
// invalidate the source
searchSource?.invalidate() searchSource?.invalidate()
} }
fun cleanFilterThen(block: BookSearchViewModel.() -> Unit) { fun filterByTitle(criteria: String?) {
// filter the search
_search.value = null
// clean the new filter
filterByNew = null
newFilter.value = newFilter.value.copy(selected = false)
// clean the author filter
filterByAuthorId = null
authorFilter.value = authorFilter.value.copy(
selected = false, label = null, closable = false, openable = true
)
// clean the author filter
filterBySeriesId = null
seriesFilter.value = seriesFilter.value.copy(
selected = false, label = null, closable = false, openable = true
)
// clean the author filter
filterByGenreId = null
genreFilter.value = genreFilter.value.copy(
selected = false, label = null, closable = false, openable = true
)
// clean the author filter
filterByLanguageId = null
languageFilter.value = languageFilter.value.copy(
selected = false, label = null, closable = false, openable = true
)
// call the filter
this.block()
}
fun filterSearch(criteria: String?) {
_search.value = criteria _search.value = criteria
searchSource?.invalidate() searchSource?.invalidate()
} }
fun filterByNew(criteria: Boolean) {
if (filterByNew != criteria) {
filterByNew = if (criteria) true else null
newFilter.value = newFilter.value.copy(selected = criteria)
searchSource?.invalidate()
}
}
fun filterByAuthor(label: String?, criteria: Int?) {
if (filterByAuthorId != criteria) {
filterByAuthorId = criteria
authorFilter.value = if (criteria != null) {
authorFilter.value.copy(
selected = true,
label = label,
closable = true,
openable = false
)
} else {
authorFilter.value.copy(
selected = false,
label = null,
closable = false,
openable = true
)
}
searchSource?.invalidate()
}
}
fun filterBySeries(label: String?, criteria: Int?) {
if (filterBySeriesId != criteria) {
filterBySeriesId = criteria
seriesFilter.value = if (criteria != null) {
seriesFilter.value.copy(
selected = true,
label = label,
closable = true,
openable = false
)
} else {
seriesFilter.value.copy(
selected = false,
label = null,
closable = false,
openable = true
)
}
searchSource?.invalidate()
}
}
fun filterByGenre(label: String?, criteria: Int?) {
if (filterByGenreId != criteria) {
filterByGenreId = criteria
genreFilter.value = if (criteria != null) {
genreFilter.value.copy(
selected = true,
label = label,
closable = true,
openable = false
)
} else {
genreFilter.value.copy(
selected = false,
label = null,
closable = false,
openable = true
)
}
searchSource?.invalidate()
}
}
fun filterByLanguage(label: String?, criteria: Int?) {
if (filterByLanguageId != criteria) {
filterByLanguageId = criteria
languageFilter.value = if (criteria != null) {
languageFilter.value.copy(
selected = true,
label = label,
closable = true,
openable = false
)
} else {
languageFilter.value.copy(
selected = false,
label = null,
closable = false,
openable = true
)
}
searchSource?.invalidate()
}
}
private fun buildBookSource(): PagingSource<Int, Book> { private fun buildBookSource(): PagingSource<Int, Book> {
return BookSearchSource( return BookSearchSource(
searchRepository = searchRepository, searchRepository = searchRepository,
@ -312,6 +271,79 @@ class BookSearchViewModel @Inject constructor(
) )
) )
class SortAndFilterScope(
sortBy: Map<SortType, SortValue?>,
isNew: Boolean?,
title: String?,
author: String?,
authorId: Int?,
series: String?,
seriesId: Int?,
genre: String?,
genreId: Int?,
language: String?,
languageId: Int?,
) {
var sortBy: Map<SortType, SortValue?> = sortBy
private set
var isNew: Boolean? = isNew
private set
var title: String? = title
private set
var author: String? = author
private set
var authorId: Int? = authorId
private set
var series: String? = series
private set
var seriesId: Int? = seriesId
private set
var genre: String? = genre
private set
var genreId: Int? = genreId
private set
var language: String? = language
private set
var languageId: Int? = languageId
private set
fun sortBy(type: SortType, value: SortValue?) {
this.sortBy = mapOf(type to value)
}
fun sortBy(sortBy: Map<SortType, SortValue?>) {
this.sortBy = sortBy
}
fun filterByNew(isNew: Boolean) {
this.isNew = if (isNew) true else null
}
fun filterByTitle(title: String?) {
this.title = title
}
fun filterByAuthor(author: String?, authorId: Int?) {
this.author = author
this.authorId = authorId
}
fun filterBySeries(series: String?, seriesId: Int?) {
this.series = series
this.seriesId = seriesId
}
fun filterByGenre(genre: String?, genreId: Int?) {
this.genre = genre
this.genreId = genreId
}
fun filterByLanguage(language: String?, languageId: Int?) {
this.language = language
this.languageId = languageId
}
}
companion object { companion object {
private const val SEARCH_PAGE_SIZE = 20 private const val SEARCH_PAGE_SIZE = 20
} }

View file

@ -41,6 +41,8 @@ import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.model.search.FilterType import com.pixelized.biblib.model.search.FilterType
import com.pixelized.biblib.model.search.SortType
import com.pixelized.biblib.model.search.SortValue
import com.pixelized.biblib.ui.LocalSnackHostState import com.pixelized.biblib.ui.LocalSnackHostState
import com.pixelized.biblib.ui.composable.scaffold.* import com.pixelized.biblib.ui.composable.scaffold.*
import com.pixelized.biblib.ui.navigation.LocalScreenNavHostController import com.pixelized.biblib.ui.navigation.LocalScreenNavHostController
@ -104,14 +106,16 @@ fun HomeScreen(
DetailBottomSheet( DetailBottomSheet(
bottomDetailState = detailState, bottomDetailState = detailState,
onAuthor = { label: String?, id: Int? -> onAuthor = { label: String?, id: Int? ->
bookViewModel.cleanFilterThen { bookViewModel.source(clean = true) {
filterByAuthor(label = label, criteria = id) filterByAuthor(author = label, authorId = id)
sortBy(type = SortType.Title, value = SortValue.ASC)
} }
scope.launch { detailState.collapse() } scope.launch { detailState.collapse() }
}, },
onSeries = { label: String?, id: Int? -> onSeries = { label: String?, id: Int? ->
bookViewModel.cleanFilterThen { bookViewModel.source(clean = true) {
filterBySeries(label = label, criteria = id) filterBySeries(series = label, seriesId = id)
sortBy(type = SortType.Series, value = SortValue.ASC)
} }
scope.launch { detailState.collapse() } scope.launch { detailState.collapse() }
}, },
@ -123,7 +127,7 @@ fun HomeScreen(
avatar = profileViewModel.avatar, avatar = profileViewModel.avatar,
search = bookViewModel.search, search = bookViewModel.search,
onSearch = { onSearch = {
bookViewModel.filterSearch(criteria = it) bookViewModel.filterByTitle(criteria = it)
}, },
onAvatarTap = { onAvatarTap = {
navigation.navigateToProfile() navigation.navigateToProfile()
@ -149,7 +153,9 @@ fun HomeScreen(
scope.launch { scope.launch {
val result = sortingState.show(sortBy = bookViewModel.sortBy) val result = sortingState.show(sortBy = bookViewModel.sortBy)
if (result is SortBottomSheetResult.ActionPerformed) { if (result is SortBottomSheetResult.ActionPerformed) {
bookViewModel.sort(sortBy = result.sortBy) bookViewModel.source {
sortBy(sortBy = result.sortBy)
}
} }
} }
}, },
@ -182,6 +188,12 @@ fun HomeScreen(
} }
} }
CollapseKeyboardOnScrollHandler(
largeGridState = largeGridState,
smallListState = smallListState,
microListState = microListState
)
LaunchedEffect(key1 = "HomeScreenLaunchedEffect") { LaunchedEffect(key1 = "HomeScreenLaunchedEffect") {
// update // update
launch { launch {
@ -212,53 +224,58 @@ private fun rememberOnFilter(
return remember { return remember {
{ {
if (it.id == FilterType.New) { if (it.id == FilterType.New) {
filterViewModel.filterByNew(criteria = it.selected.not()) filterViewModel.source {
filterByNew(isNew = it.selected.not())
}
} else { } else {
if (it.selected.not()) { if (it.selected.not()) {
scope.launch { scope.launch {
val result = filterState.show(type = it.id) val result = filterState.show(type = it.id)
if (result is FilterBottomSheetResult.ActionPerformed) { if (result is FilterBottomSheetResult.ActionPerformed) {
when (it.id) { filterViewModel.source {
FilterType.Author -> filterViewModel.filterByAuthor( when (it.id) {
label = result.filter.filterLabel, FilterType.Author -> filterByAuthor(
criteria = result.filter.filterId, author = result.filter.filterLabel,
) authorId = result.filter.filterId,
FilterType.Series -> filterViewModel.filterBySeries( )
label = result.filter.filterLabel, FilterType.Series -> filterBySeries(
criteria = result.filter.filterId, series = result.filter.filterLabel,
) seriesId = result.filter.filterId,
FilterType.Genre -> filterViewModel.filterByGenre( )
label = result.filter.filterLabel, FilterType.Genre -> filterByGenre(
criteria = result.filter.filterId, genre = result.filter.filterLabel,
) genreId = result.filter.filterId,
FilterType.Language -> filterViewModel.filterByLanguage( )
label = result.filter.filterLabel, FilterType.Language -> filterByLanguage(
criteria = result.filter.filterId, language = result.filter.filterLabel,
) languageId = result.filter.filterId,
else -> Unit )
else -> Unit
}
} }
} }
} }
} else { } else {
when (it.id) { filterViewModel.source {
FilterType.Author -> filterViewModel.filterByAuthor( when (it.id) {
label = null, FilterType.Author -> filterByAuthor(
criteria = null, author = null,
) authorId = null,
FilterType.Series -> filterViewModel.filterBySeries( )
label = null, FilterType.Series -> filterBySeries(
criteria = null, series = null,
) seriesId = null,
FilterType.Genre -> filterViewModel.filterByGenre( )
label = null, FilterType.Genre -> filterByGenre(
criteria = null, genre = null,
) genreId = null,
FilterType.Language -> filterViewModel.filterByLanguage( )
label = null, FilterType.Language -> filterByLanguage(
criteria = null, language = null,
) languageId = null,
else -> Unit )
else -> Unit
}
} }
} }
} }
@ -569,6 +586,26 @@ private fun HomeToolbar(
} }
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun CollapseKeyboardOnScrollHandler(
largeGridState: LazyGridState,
smallListState: LazyListState,
microListState: LazyListState,
) {
val keyboard = LocalSoftwareKeyboardController.current
val focus = LocalFocusManager.current
if (
largeGridState.isScrollInProgress
|| smallListState.isScrollInProgress
|| microListState.isScrollInProgress
) {
focus.clearFocus()
keyboard?.hide()
}
}
@Composable @Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -91,24 +91,21 @@ fun DetailScreen(
content = { content = {
if (detailState.bottomSheetState.isVisible) { if (detailState.bottomSheetState.isVisible) {
DetailScreenContent( DetailScreenContent(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(all = 16.dp)
.systemBarsPadding(),
book = detail, book = detail,
onAuthor = { onAuthor(detail.author, detail.authorId) }, onAuthor = { onAuthor(it.name, it.id) },
onSeries = { onSeries(detail.series, detail.seriesId) }, onSeries = { onSeries(it.name, it.id) },
onMobi = { }, onMobi = { },
onEpub = { }, onEpub = { },
onSend = { onSend = {
send( scope.launch {
context = context, send(
profileViewModel = profileViewModel, context = context,
snackBarHost = snackBarHost, profileViewModel = profileViewModel,
emailSheetState = emailSheetState, snackBarHost = snackBarHost,
user = profileViewModel.user emailSheetState = emailSheetState,
) user = profileViewModel.user
)
}
} }
) )
DetailScreenSendLoader( DetailScreenSendLoader(

View file

@ -11,19 +11,17 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
@ -36,7 +34,6 @@ import com.pixelized.biblib.utils.extention.bibLib
import com.pixelized.biblib.utils.extention.placeholder import com.pixelized.biblib.utils.extention.placeholder
import com.skydoves.landscapist.CircularReveal import com.skydoves.landscapist.CircularReveal
import com.skydoves.landscapist.glide.GlideImage import com.skydoves.landscapist.glide.GlideImage
import kotlinx.coroutines.launch
import java.io.Serializable import java.io.Serializable
@Stable @Stable
@ -45,25 +42,40 @@ data class BookDetailUio(
val id: Int, val id: Int,
val title: String, val title: String,
val authorId: Int?, val authorId: Int?,
val author: String, val authors: List<AuthorUio>,
val rating: Float, val rating: Float,
val language: String, val language: String,
val date: String?, val date: String?,
val seriesId: Int?, val seriesId: Int?,
val series: String?, val series: SeriesUio?,
val description: String, val description: String,
val cover: String, val cover: String,
val placeHolder: Boolean, val placeHolder: Boolean,
) : Serializable { ) : Serializable {
@Stable
@Immutable
data class AuthorUio(
val id: Int,
val name: String,
)
@Stable
@Immutable
data class SeriesUio(
val id: Int,
val name: String,
val label: String,
)
companion object { companion object {
fun preview() = BookDetailUio( fun preview() = BookDetailUio(
id = 90, id = 90,
title = "Foundation", title = "Foundation",
authorId = null, authorId = null,
author = "Asimov", authors = listOf(AuthorUio(id = 0, name = "Asimov")),
date = "1951", date = "1951",
seriesId = null, seriesId = null,
series = "Foundation - 1", series = SeriesUio(id = 0, "Foundation", label = "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...", 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, rating = 4.5f,
language = "Français", language = "Français",
@ -77,162 +89,146 @@ data class BookDetailUio(
fun DetailScreenContent( fun DetailScreenContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
book: BookDetailUio, book: BookDetailUio,
onAuthor: () -> Unit, onAuthor: (BookDetailUio.AuthorUio) -> Unit,
onSeries: () -> Unit, onSeries: (BookDetailUio.SeriesUio) -> Unit,
onMobi: () -> Unit, onMobi: () -> Unit,
onEpub: () -> Unit, onEpub: () -> Unit,
onSend: suspend () -> Unit, onSend: () -> Unit,
) { ) {
val scope = rememberCoroutineScope() val density = LocalDensity.current
val fabHeight = remember { mutableStateOf(0.dp) }
AnimatedDelayer( AnimatedDelayer(
targetState = book.id, targetState = book.id,
) { ) {
Column( Box {
modifier = modifier, Column(
) {
GlideImage(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxSize()
.padding(vertical = 16.dp) .verticalScroll(rememberScrollState())
.height(MaterialTheme.bibLib.dimen.detail.cover), .padding(all = 16.dp)
previewPlaceholder = R.drawable.ic_fondatoin_cover, .systemBarsPadding(),
circularReveal = CircularReveal(duration = 1000), verticalArrangement = Arrangement.spacedBy(16.dp)
contentScale = ContentScale.FillHeight,
imageModel = book.cover,
)
Row(modifier = Modifier.padding(vertical = 16.dp)) {
AnimatedOffset(
modifier = Modifier.weight(1f),
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onMobi,
) {
Icon(imageVector = Icons.Default.Download, contentDescription = null)
Spacer(modifier = Modifier.width(4.dp))
Text(text = stringResource(id = R.string.action_mobi))
}
}
Spacer(modifier = Modifier.padding(all = 4.dp))
AnimatedOffset(
modifier = Modifier.weight(1f),
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onEpub,
) {
Icon(imageVector = Icons.Default.Download, contentDescription = null)
Spacer(modifier = Modifier.width(4.dp))
Text(text = stringResource(id = R.string.action_epub))
}
}
Spacer(modifier = Modifier.padding(all = 4.dp))
AnimatedOffset(
modifier = Modifier.weight(1f),
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { scope.launch { onSend() } },
) {
Icon(imageVector = Icons.Default.Send, contentDescription = "")
Spacer(modifier = Modifier.width(4.dp))
Text(text = stringResource(id = R.string.action_send))
}
}
}
AnimatedOffset(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(bottom = 4.dp),
) { ) {
Text( GlideImage(
style = MaterialTheme.typography.h5,
color = MaterialTheme.colors.onSurface,
text = book.title,
)
}
AnimatedOffset(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(bottom = 16.dp),
) {
Text(
modifier = Modifier.clickable(onClick = onAuthor),
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onSurface,
text = book.author,
)
}
Row(
modifier = Modifier.padding(bottom = 8.dp),
) {
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 = 16.dp),
) {
TitleLabel(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onSeries), .padding(vertical = 16.dp)
title = stringResource(id = R.string.detail_series), .height(MaterialTheme.bibLib.dimen.detail.cover),
label = book.series ?: "-", previewPlaceholder = R.drawable.ic_fondatoin_cover,
circularReveal = CircularReveal(duration = 1000),
contentScale = ContentScale.FillHeight,
imageModel = book.cover,
) )
AnimatedOffset {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
style = MaterialTheme.typography.h5,
color = MaterialTheme.colors.onSurface,
text = book.title,
)
book.series?.let { series ->
Text(
modifier = Modifier.clickable(onClick = { onSeries(series) }),
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface,
text = series.label,
)
}
}
}
AnimatedOffset {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
book.authors.forEach { author ->
Text(
modifier = Modifier.clickable(onClick = { onAuthor(author) }),
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onSurface,
text = author.name,
)
}
}
}
Row {
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 {
SpannedText(
modifier = Modifier
.fillMaxWidth()
.placeholder(
shape = MaterialTheme.bibLib.shapes.base.medium,
visible = { book.placeHolder },
),
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface,
text = HtmlCompat.fromHtml(
book.description,
HtmlCompat.FROM_HTML_MODE_COMPACT,
).toSpannable(),
)
}
Spacer(height = fabHeight)
} }
AnimatedOffset { AnimatedOffset(
SpannedText( modifier = Modifier
modifier = Modifier .align(Alignment.BottomEnd)
.fillMaxWidth() .padding(all = 16.dp)
.placeholder( .systemBarsPadding()
shape = MaterialTheme.bibLib.shapes.base.medium, .onSizeChanged { fabHeight.value = with(density) { it.height.toDp() } },
visible = { book.placeHolder }, ) {
), Button(
style = MaterialTheme.typography.body1, onClick = onSend,
color = MaterialTheme.colors.onSurface, ) {
text = HtmlCompat.fromHtml( Icon(imageVector = Icons.Default.Send, contentDescription = "")
book.description, Spacer(modifier = Modifier.width(4.dp))
HtmlCompat.FROM_HTML_MODE_COMPACT, Text(text = stringResource(id = R.string.action_send))
).toSpannable(), }
)
} }
} }
} }
} }
@Composable
private fun Spacer(height: State<Dp>) {
Spacer(modifier = Modifier.height(height = height.value))
}
@Composable @Composable
private fun TitleLabel( private fun TitleLabel(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -245,13 +241,12 @@ private fun TitleLabel(
) { ) {
Text( Text(
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface, color = MaterialTheme.colors.onSurface,
text = title, text = title,
) )
Text( Text(
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center, fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface, color = MaterialTheme.colors.onSurface,
text = label, text = label,
) )

View file

@ -24,7 +24,7 @@ fun Book.toMicroThumbnailUio(
cover = "${coverBaseUrl}/$id.jpg", cover = "${coverBaseUrl}/$id.jpg",
title = title, title = title,
author = author.joinToString { it.name }, author = author.joinToString { it.name },
series = toLabel(series) ?: "", series = series?.let { toLabel(it) } ?: "",
isNew = isNew, isNew = isNew,
) )
@ -55,21 +55,26 @@ fun Book.toDetailUio(
id = id, id = id,
title = title, title = title,
authorId = author.firstOrNull()?.id, authorId = author.firstOrNull()?.id,
author = author.joinToString { it.name }, authors = author.map { BookDetailUio.AuthorUio(id = it.id, it.name) },
rating = rating?.toFloat() ?: 0.0f, rating = rating?.toFloat() ?: 0.0f,
language = language?.displayLanguage?.capitalize() ?: "", language = language?.displayLanguage?.capitalize() ?: "",
date = releaseDate.shortDate(), date = releaseDate.shortDate(),
seriesId = series?.id, seriesId = series?.id,
series = toLabel(series), series = series?.let {
BookDetailUio.SeriesUio(
id = it.id,
name = it.name,
label = toLabel(series)
)
},
description = synopsis ?: "", description = synopsis ?: "",
cover = "${coverBaseUrl}/$id.jpg", cover = "${coverBaseUrl}/$id.jpg",
placeHolder = placeHolder, placeHolder = placeHolder,
) )
fun Book.toLabel(series: Series?): String? { fun Book.toLabel(series: Series): String {
return when { return when {
series?.name != null && seriesIndex != null -> "${series.name} - $seriesIndex" seriesIndex != null -> "${series.name} - $seriesIndex"
series?.name != null -> series.name else -> series.name
else -> null
} }
} }

View file

@ -46,7 +46,6 @@
<string name="detail_language">Langue</string> <string name="detail_language">Langue</string>
<string name="detail_release">Publication</string> <string name="detail_release">Publication</string>
<string name="detail_genre">Genre</string> <string name="detail_genre">Genre</string>
<string name="detail_series">Séries</string>
<string name="detail_option_mail">Envoyer cet eBook à :</string> <string name="detail_option_mail">Envoyer cet eBook à :</string>
<string name="detail_option_format">Format de l\'eBook :</string> <string name="detail_option_format">Format de l\'eBook :</string>

View file

@ -61,7 +61,6 @@
<string name="detail_language">Language</string> <string name="detail_language">Language</string>
<string name="detail_release">Release</string> <string name="detail_release">Release</string>
<string name="detail_genre">Genre</string> <string name="detail_genre">Genre</string>
<string name="detail_series">Series</string>
<string name="detail_option_mail">Send this eBook to:</string> <string name="detail_option_mail">Send this eBook to:</string>
<string name="detail_option_format">eBook format:</string> <string name="detail_option_format">eBook format:</string>