Detail Screen
This commit is contained in:
parent
e6d7694f67
commit
c855e97c34
15 changed files with 475 additions and 183 deletions
|
|
@ -1,11 +1,10 @@
|
||||||
package com.pixelized.biblib.module
|
package com.pixelized.biblib.module
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.pixelized.biblib.database.BibLibDatabase
|
import com.pixelized.biblib.database.BibLibDatabase
|
||||||
import com.pixelized.biblib.utils.BitmapCache
|
import com.pixelized.biblib.utils.CoverCache
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -34,8 +33,8 @@ class PersistenceModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideBitmapCache(
|
fun provideBitmapCache(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
): BitmapCache {
|
): CoverCache {
|
||||||
return BitmapCache(context)
|
return CoverCache(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
99
app/src/main/java/com/pixelized/biblib/ui/composable/Text.kt
Normal file
99
app/src/main/java/com/pixelized/biblib/ui/composable/Text.kt
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package com.pixelized.biblib.ui.composable
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.StyleSpan
|
||||||
|
import android.text.style.UnderlineSpan
|
||||||
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
|
import androidx.compose.material.LocalTextStyle
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.*
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SpannedText(
|
||||||
|
text: Spanned,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = Color.Unspecified,
|
||||||
|
fontSize: TextUnit = TextUnit.Unspecified,
|
||||||
|
fontStyle: FontStyle? = null,
|
||||||
|
fontWeight: FontWeight? = null,
|
||||||
|
fontFamily: FontFamily? = null,
|
||||||
|
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||||
|
textDecoration: TextDecoration? = null,
|
||||||
|
textAlign: TextAlign? = null,
|
||||||
|
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||||
|
overflow: TextOverflow = TextOverflow.Clip,
|
||||||
|
softWrap: Boolean = true,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
inlineContent: Map<String, InlineTextContent> = mapOf(),
|
||||||
|
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||||
|
style: TextStyle = LocalTextStyle.current
|
||||||
|
) {
|
||||||
|
androidx.compose.material.Text(
|
||||||
|
text = text.toAnnotatedString(),
|
||||||
|
modifier = modifier,
|
||||||
|
color = color,
|
||||||
|
fontSize = fontSize,
|
||||||
|
fontStyle = fontStyle,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
fontFamily = fontFamily,
|
||||||
|
letterSpacing = letterSpacing,
|
||||||
|
textDecoration = textDecoration,
|
||||||
|
textAlign = textAlign,
|
||||||
|
lineHeight = lineHeight,
|
||||||
|
overflow = overflow,
|
||||||
|
softWrap = softWrap,
|
||||||
|
maxLines = maxLines,
|
||||||
|
inlineContent = inlineContent,
|
||||||
|
onTextLayout = onTextLayout,
|
||||||
|
style = style,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
|
||||||
|
val spanned = this@toAnnotatedString
|
||||||
|
append(spanned.toString())
|
||||||
|
getSpans(0, spanned.length, Any::class.java).forEach { span ->
|
||||||
|
val start = getSpanStart(span)
|
||||||
|
val end = getSpanEnd(span)
|
||||||
|
when (span) {
|
||||||
|
is StyleSpan -> when (span.style) {
|
||||||
|
Typeface.BOLD -> addStyle(
|
||||||
|
style = SpanStyle(fontWeight = FontWeight.Bold),
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
Typeface.ITALIC -> addStyle(
|
||||||
|
style = SpanStyle(fontStyle = FontStyle.Italic),
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
Typeface.BOLD_ITALIC -> addStyle(
|
||||||
|
style = SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic),
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is UnderlineSpan -> addStyle(
|
||||||
|
style = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
is ForegroundColorSpan -> addStyle(
|
||||||
|
style = SpanStyle(color = Color(span.foregroundColor)),
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.Saver
|
import androidx.compose.runtime.saveable.Saver
|
||||||
import androidx.compose.runtime.saveable.SaverScope
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
|
|
@ -22,12 +23,14 @@ import androidx.compose.ui.unit.dp
|
||||||
fun AnimatedDelayerScope.AnimatedOffset(
|
fun AnimatedDelayerScope.AnimatedOffset(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
transitionLabel: String = "AnimatedOffset",
|
transitionLabel: String = "AnimatedOffset",
|
||||||
|
contentAlignment: Alignment = Alignment.TopStart,
|
||||||
content: @Composable BoxScope.() -> Unit,
|
content: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
AnimatedOffset(
|
AnimatedOffset(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
transitionLabel = transitionLabel,
|
transitionLabel = transitionLabel,
|
||||||
delay = delay++,
|
delay = delay++,
|
||||||
|
contentAlignment = contentAlignment,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +40,7 @@ fun AnimatedOffset(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
transitionLabel: String = "AnimatedOffset",
|
transitionLabel: String = "AnimatedOffset",
|
||||||
delay: Delay = Delay(),
|
delay: Delay = Delay(),
|
||||||
|
contentAlignment: Alignment = Alignment.TopStart,
|
||||||
content: @Composable BoxScope.() -> Unit,
|
content: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val displayed = rememberSavableMutableTransitionState(
|
val displayed = rememberSavableMutableTransitionState(
|
||||||
|
|
@ -48,6 +52,7 @@ fun AnimatedOffset(
|
||||||
transitionLabel = transitionLabel,
|
transitionLabel = transitionLabel,
|
||||||
displayed = displayed,
|
displayed = displayed,
|
||||||
delay = delay,
|
delay = delay,
|
||||||
|
contentAlignment = contentAlignment,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +63,7 @@ fun AnimatedOffset(
|
||||||
displayed: MutableTransitionState<Boolean>,
|
displayed: MutableTransitionState<Boolean>,
|
||||||
transitionLabel: String = "AnimatedOffset",
|
transitionLabel: String = "AnimatedOffset",
|
||||||
delay: Delay = Delay(),
|
delay: Delay = Delay(),
|
||||||
|
contentAlignment: Alignment = Alignment.TopStart,
|
||||||
content: @Composable BoxScope.() -> Unit,
|
content: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val transition: TransitionData = updateTransition(
|
val transition: TransitionData = updateTransition(
|
||||||
|
|
@ -69,6 +75,7 @@ fun AnimatedOffset(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.offset(y = transition.offset)
|
.offset(y = transition.offset)
|
||||||
.graphicsLayer(alpha = transition.alpha, clip = false),
|
.graphicsLayer(alpha = transition.alpha, clip = false),
|
||||||
|
contentAlignment = contentAlignment,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,41 @@
|
||||||
package com.pixelized.biblib.ui.screen.detail
|
package com.pixelized.biblib.ui.screen.detail
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.pixelized.biblib.R
|
||||||
import com.pixelized.biblib.model.book.Book
|
import com.pixelized.biblib.model.book.Book
|
||||||
import com.pixelized.biblib.network.client.IBibLibClient
|
import com.pixelized.biblib.network.client.IBibLibClient
|
||||||
import com.pixelized.biblib.network.factory.BookFactory
|
import com.pixelized.biblib.network.factory.BookFactory
|
||||||
import com.pixelized.biblib.ui.composable.StateUio
|
import com.pixelized.biblib.ui.composable.StateUio
|
||||||
import com.pixelized.biblib.ui.navigation.screen.Screen
|
import com.pixelized.biblib.ui.navigation.screen.Screen
|
||||||
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
|
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
|
||||||
|
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
|
||||||
|
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
|
||||||
|
import com.pixelized.biblib.utils.CoverCache
|
||||||
import com.pixelized.biblib.utils.extention.capitalize
|
import com.pixelized.biblib.utils.extention.capitalize
|
||||||
|
import com.pixelized.biblib.utils.extention.context
|
||||||
import com.pixelized.biblib.utils.extention.shortDate
|
import com.pixelized.biblib.utils.extention.shortDate
|
||||||
|
import com.pixelized.biblib.utils.painterResource
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.URL
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class BookDetailViewModel @Inject constructor(
|
class BookDetailViewModel @Inject constructor(
|
||||||
|
application: Application,
|
||||||
|
coverCache: CoverCache,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
private val client: IBibLibClient,
|
private val client: IBibLibClient,
|
||||||
) : ViewModel() {
|
) : ACoverViewModel(application, coverCache) {
|
||||||
|
|
||||||
private val _state = mutableStateOf<StateUio<BookUio>>(StateUio.Progress())
|
private val _state = mutableStateOf<StateUio<BookUio>>(StateUio.Progress())
|
||||||
val state: State<StateUio<BookUio>> get() = _state
|
val state: State<StateUio<BookUio>> get() = _state
|
||||||
|
|
@ -53,16 +64,34 @@ class BookDetailViewModel @Inject constructor(
|
||||||
return book.toUio()
|
return book.toUio()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Book.toUio() = BookUio(
|
private fun Book.toUio() : BookUio {
|
||||||
id = id,
|
val thumbnailCover by cover(
|
||||||
title = title,
|
placeHolder = CoverUio(
|
||||||
author = author.joinToString { it.name },
|
type = CoverUio.Type.PLACE_HOLDER,
|
||||||
rating = rating?.toFloat() ?: 0.0f,
|
contentScale = ContentScale.FillBounds,
|
||||||
language = language?.displayLanguage?.capitalize() ?: "",
|
painter = painterResource(context, R.drawable.ic_baseline_auto_stories_24),
|
||||||
date = releaseDate.shortDate(),
|
),
|
||||||
series = series?.name,
|
type = CoverUio.Type.THUMBNAIL,
|
||||||
description = synopsis ?: "",
|
contentScale = ContentScale.FillHeight,
|
||||||
)
|
url = URL("${IBibLibClient.THUMBNAIL_URL}/$id.jpg"),
|
||||||
|
)
|
||||||
|
return BookUio(
|
||||||
|
id = id,
|
||||||
|
title = title,
|
||||||
|
author = author.joinToString { it.name },
|
||||||
|
rating = rating?.toFloat() ?: 0.0f,
|
||||||
|
language = language?.displayLanguage?.capitalize() ?: "",
|
||||||
|
date = releaseDate.shortDate(),
|
||||||
|
series = series?.name,
|
||||||
|
description = synopsis ?: "",
|
||||||
|
cover = cover(
|
||||||
|
placeHolder = thumbnailCover,
|
||||||
|
type = CoverUio.Type.DETAIL,
|
||||||
|
contentScale = ContentScale.FillHeight,
|
||||||
|
url = URL("${IBibLibClient.COVER_URL}/$id.jpg"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val SavedStateHandle.bookId: Int
|
private val SavedStateHandle.bookId: Int
|
||||||
get() = get<Int>(Screen.BookDetail.ARG_BOOK_ID) ?: error("")
|
get() = get<Int>(Screen.BookDetail.ARG_BOOK_ID) ?: error("")
|
||||||
|
|
|
||||||
|
|
@ -2,94 +2,175 @@ package com.pixelized.biblib.ui.screen.detail
|
||||||
|
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
import android.widget.TextView
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
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.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
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.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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.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.viewinterop.AndroidView
|
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.google.accompanist.insets.systemBarsPadding
|
|
||||||
import com.pixelized.biblib.R
|
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.AnimatedDelayer
|
||||||
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
|
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
|
||||||
import com.pixelized.biblib.ui.composable.animation.Delay
|
import com.pixelized.biblib.ui.composable.animation.Delay
|
||||||
|
import com.pixelized.biblib.ui.navigation.screen.LocalScreenNavHostController
|
||||||
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
|
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
|
||||||
|
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
|
||||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||||
|
import com.pixelized.biblib.utils.extention.bibLib
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DetailScreen(
|
fun DetailScreen(
|
||||||
viewModel: BookDetailViewModel = hiltViewModel()
|
viewModel: BookDetailViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
|
val screenNavHostController = LocalScreenNavHostController.current
|
||||||
Box {
|
Box {
|
||||||
val book by viewModel.book
|
val book by viewModel.book
|
||||||
book?.let {
|
book?.let {
|
||||||
DetailScreenContent(
|
DetailScreenContent(
|
||||||
book = it,
|
book = it,
|
||||||
onSendClick = {},
|
onClose = {
|
||||||
|
screenNavHostController.popBackStack()
|
||||||
|
},
|
||||||
|
onMobi = {},
|
||||||
|
onEpub = {},
|
||||||
|
onSend = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun DetailScreenContent(
|
private fun DetailScreenContent(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
book: BookUio,
|
book: BookUio,
|
||||||
onSendClick: () -> Unit,
|
onClose: () -> Unit,
|
||||||
|
onMobi: () -> Unit,
|
||||||
|
onEpub: () -> Unit,
|
||||||
|
onSend: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AnimatedDelayer(delay = Delay(300)) {
|
AnimatedDelayer(delay = Delay(300)) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.systemBarsPadding()
|
.systemBarsPadding()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = MaterialTheme.bibLib.dimen.medium)
|
||||||
|
.then(modifier)
|
||||||
) {
|
) {
|
||||||
AnimatedOffset(
|
AnimatedOffset(
|
||||||
modifier = Modifier
|
modifier = Modifier.align(Alignment.End),
|
||||||
.padding(16.dp)
|
|
||||||
.align(Alignment.CenterHorizontally),
|
|
||||||
) {
|
) {
|
||||||
Box(
|
IconButton(onClick = onClose) {
|
||||||
modifier = Modifier
|
Icon(
|
||||||
.size(200.dp, 320.dp)
|
imageVector = Icons.Default.Close,
|
||||||
.background(MaterialTheme.colors.surface),
|
tint = MaterialTheme.colors.onSurface,
|
||||||
) {
|
contentDescription = null
|
||||||
Box(contentAlignment = Alignment.Center) {
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedOffset(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(MaterialTheme.bibLib.dimen.detail.cover),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
val cover by book.cover
|
||||||
|
when (cover.type) {
|
||||||
|
CoverUio.Type.PLACE_HOLDER -> {
|
||||||
Image(
|
Image(
|
||||||
modifier = Modifier.size(64.dp),
|
modifier = Modifier.size(MaterialTheme.bibLib.dimen.detail.placeHolder),
|
||||||
painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24),
|
painter = cover.painter,
|
||||||
|
contentScale = cover.contentScale,
|
||||||
|
colorFilter = cover.colorFilter,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CoverUio.Type.THUMBNAIL -> {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
painter = cover.painter,
|
||||||
|
contentScale = cover.contentScale,
|
||||||
|
colorFilter = cover.colorFilter,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CoverUio.Type.DETAIL -> {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
painter = cover.painter,
|
||||||
|
contentScale = cover.contentScale,
|
||||||
|
colorFilter = cover.colorFilter,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedOffset(
|
Row(modifier = Modifier.padding(vertical = MaterialTheme.bibLib.dimen.medium)) {
|
||||||
modifier = Modifier
|
AnimatedOffset(
|
||||||
.padding(16.dp)
|
modifier = Modifier.weight(1f),
|
||||||
.align(Alignment.CenterHorizontally),
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = onSendClick,
|
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Default.Send, contentDescription = "")
|
Button(
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
modifier = Modifier.fillMaxSize(),
|
||||||
Text(text = stringResource(id = R.string.action_send))
|
onClick = onMobi,
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Default.Download, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.extraSmall))
|
||||||
|
Text(text = stringResource(id = R.string.action_mobi))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.padding(all = MaterialTheme.bibLib.dimen.small))
|
||||||
|
|
||||||
|
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.extraSmall))
|
||||||
|
Text(text = stringResource(id = R.string.action_epub))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.padding(all = MaterialTheme.bibLib.dimen.small))
|
||||||
|
|
||||||
|
AnimatedOffset(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
onClick = onSend,
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Default.Send, contentDescription = "")
|
||||||
|
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.extraSmall))
|
||||||
|
Text(text = stringResource(id = R.string.action_send))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +181,7 @@ private fun DetailScreenContent(
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
style = MaterialTheme.typography.h5,
|
style = MaterialTheme.typography.h5,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
text = book.title,
|
text = book.title,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +194,7 @@ private fun DetailScreenContent(
|
||||||
Text(
|
Text(
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
style = MaterialTheme.typography.h6,
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
text = book.author,
|
text = book.author,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -151,13 +234,20 @@ private fun DetailScreenContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedOffset {
|
AnimatedOffset {
|
||||||
HtmlText(
|
SpannedText(
|
||||||
html = book.description,
|
modifier = Modifier.padding(bottom = 16.dp),
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
style = MaterialTheme.typography.body1,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
text = HtmlCompat.fromHtml(
|
||||||
|
book.description,
|
||||||
|
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||||
|
).toSpannable(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackHandler(onBack = onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -172,35 +262,50 @@ private fun TitleLabel(
|
||||||
Text(
|
Text(
|
||||||
style = MaterialTheme.typography.body2,
|
style = MaterialTheme.typography.body2,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
text = title,
|
text = title,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
style = MaterialTheme.typography.body1,
|
style = MaterialTheme.typography.body1,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
text = label,
|
text = label,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun HtmlText(
|
|
||||||
html: String,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
AndroidView(
|
|
||||||
modifier = modifier,
|
|
||||||
factory = { context -> TextView(context) },
|
|
||||||
update = { it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
|
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
|
||||||
private fun DetailScreenContentPreview() {
|
private fun DetailScreenContentPreview() {
|
||||||
|
val painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24)
|
||||||
|
val cover = remember {
|
||||||
|
mutableStateOf(
|
||||||
|
CoverUio(
|
||||||
|
type = CoverUio.Type.PLACE_HOLDER,
|
||||||
|
contentScale = ContentScale.FillBounds,
|
||||||
|
painter = painter,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val book = BookUio(
|
||||||
|
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 = cover,
|
||||||
|
)
|
||||||
BibLibTheme {
|
BibLibTheme {
|
||||||
// DetailScreenContent(
|
DetailScreenContent(
|
||||||
//
|
book = book,
|
||||||
// )
|
onClose = {},
|
||||||
|
onMobi = {},
|
||||||
|
onEpub = {},
|
||||||
|
onSend = {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,7 +11,6 @@ import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
|
@ -101,6 +100,7 @@ private fun Cover(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
alignment = Alignment.Center,
|
alignment = Alignment.Center,
|
||||||
contentScale = cover.contentScale,
|
contentScale = cover.contentScale,
|
||||||
|
colorFilter = cover.colorFilter,
|
||||||
painter = cover.painter,
|
painter = cover.painter,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
|
|
@ -122,8 +122,9 @@ private fun BookThumbnailPreview() {
|
||||||
cover = remember {
|
cover = remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
CoverUio(
|
CoverUio(
|
||||||
painter = painter,
|
type = CoverUio.Type.PLACE_HOLDER,
|
||||||
contentScale = ContentScale.None,
|
contentScale = ContentScale.None,
|
||||||
|
painter = painter,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ private fun LazyBookThumbnailColumnPreview() {
|
||||||
@Composable
|
@Composable
|
||||||
private fun previewResources(): LazyPagingItems<BookThumbnailUio> {
|
private fun previewResources(): LazyPagingItems<BookThumbnailUio> {
|
||||||
val cover = CoverUio(
|
val cover = CoverUio(
|
||||||
|
type = CoverUio.Type.PLACE_HOLDER,
|
||||||
painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24),
|
painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24),
|
||||||
contentScale = ContentScale.None,
|
contentScale = ContentScale.None,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.pixelized.biblib.ui.screen.home.common.uio
|
package com.pixelized.biblib.ui.screen.home.common.uio
|
||||||
|
|
||||||
|
import androidx.compose.runtime.State
|
||||||
import com.pixelized.biblib.network.client.IBibLibClient.Companion.COVER_URL
|
import com.pixelized.biblib.network.client.IBibLibClient.Companion.COVER_URL
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
|
|
@ -12,6 +13,5 @@ data class BookUio(
|
||||||
val date: String?,
|
val date: String?,
|
||||||
val series: String?,
|
val series: String?,
|
||||||
val description: String,
|
val description: String,
|
||||||
) {
|
val cover: State<CoverUio>,
|
||||||
val cover: URL = URL("${COVER_URL}/$id.jpg")
|
)
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,21 @@ package com.pixelized.biblib.ui.screen.home.common.uio
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@Immutable
|
@Immutable
|
||||||
data class CoverUio(
|
data class CoverUio(
|
||||||
|
val type: Type,
|
||||||
val contentScale: ContentScale = ContentScale.FillBounds,
|
val contentScale: ContentScale = ContentScale.FillBounds,
|
||||||
|
val colorFilter: ColorFilter? = null,
|
||||||
val painter: Painter,
|
val painter: Painter,
|
||||||
)
|
) {
|
||||||
|
enum class Type {
|
||||||
|
PLACE_HOLDER,
|
||||||
|
THUMBNAIL,
|
||||||
|
DETAIL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,35 @@
|
||||||
package com.pixelized.biblib.ui.screen.home.common.viewModel
|
package com.pixelized.biblib.ui.screen.home.common.viewModel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.pixelized.biblib.R
|
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
|
||||||
import com.pixelized.biblib.utils.BitmapCache
|
import com.pixelized.biblib.utils.CoverCache
|
||||||
import com.pixelized.biblib.utils.extention.context
|
|
||||||
import com.pixelized.biblib.utils.painterResource
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
|
|
||||||
abstract class ACoverViewModel(
|
abstract class ACoverViewModel(
|
||||||
application: Application,
|
application: Application,
|
||||||
private val cache: BitmapCache,
|
private val cache: CoverCache,
|
||||||
) : AndroidViewModel(application) {
|
) : AndroidViewModel(application) {
|
||||||
|
|
||||||
fun cover(
|
fun cover(
|
||||||
cache: BitmapCache = this.cache,
|
cache: CoverCache = this.cache,
|
||||||
coroutineScope: CoroutineScope = viewModelScope,
|
coroutineScope: CoroutineScope = viewModelScope,
|
||||||
placeHolder: Painter = painterResource(context, R.drawable.ic_baseline_auto_stories_24),
|
placeHolder: CoverUio,
|
||||||
|
type: CoverUio.Type,
|
||||||
|
contentScale: ContentScale = ContentScale.FillBounds,
|
||||||
|
tint: ColorFilter? = null,
|
||||||
url: URL,
|
url: URL,
|
||||||
) = cache.download(
|
) = cache.cover(
|
||||||
placeHolder = placeHolder,
|
placeHolder = placeHolder,
|
||||||
coroutineScope = coroutineScope,
|
coroutineScope = coroutineScope,
|
||||||
|
type = type,
|
||||||
|
contentScale = contentScale,
|
||||||
|
tint = tint,
|
||||||
url = url,
|
url = url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,21 @@ package com.pixelized.biblib.ui.screen.home.page.books
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import com.pixelized.biblib.R
|
||||||
import com.pixelized.biblib.model.book.Book
|
import com.pixelized.biblib.model.book.Book
|
||||||
import com.pixelized.biblib.network.client.IBibLibClient
|
import com.pixelized.biblib.network.client.IBibLibClient
|
||||||
import com.pixelized.biblib.repository.book.IBookRepository
|
import com.pixelized.biblib.repository.book.IBookRepository
|
||||||
import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio
|
import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio
|
||||||
|
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
|
||||||
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
|
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
|
||||||
import com.pixelized.biblib.utils.BitmapCache
|
import com.pixelized.biblib.utils.CoverCache
|
||||||
|
import com.pixelized.biblib.utils.extention.context
|
||||||
import com.pixelized.biblib.utils.extention.longDate
|
import com.pixelized.biblib.utils.extention.longDate
|
||||||
|
import com.pixelized.biblib.utils.painterResource
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
@ -22,7 +27,7 @@ import javax.inject.Inject
|
||||||
class BooksViewModel @Inject constructor(
|
class BooksViewModel @Inject constructor(
|
||||||
application: Application,
|
application: Application,
|
||||||
bookRepository: IBookRepository,
|
bookRepository: IBookRepository,
|
||||||
cache: BitmapCache,
|
cache: CoverCache,
|
||||||
) : ACoverViewModel(application, cache) {
|
) : ACoverViewModel(application, cache) {
|
||||||
|
|
||||||
private val booksSource = Pager(
|
private val booksSource = Pager(
|
||||||
|
|
@ -41,6 +46,14 @@ class BooksViewModel @Inject constructor(
|
||||||
author = author.joinToString { it.name },
|
author = author.joinToString { it.name },
|
||||||
date = releaseDate.longDate(),
|
date = releaseDate.longDate(),
|
||||||
isNew = isNew,
|
isNew = isNew,
|
||||||
cover = cover(url = URL("${IBibLibClient.THUMBNAIL_URL}/$id.jpg"))
|
cover = cover(
|
||||||
|
placeHolder = CoverUio(
|
||||||
|
type = CoverUio.Type.PLACE_HOLDER,
|
||||||
|
contentScale = ContentScale.None,
|
||||||
|
painter = painterResource(context, R.drawable.ic_baseline_auto_stories_24),
|
||||||
|
),
|
||||||
|
type = CoverUio.Type.THUMBNAIL,
|
||||||
|
url = URL("${IBibLibClient.THUMBNAIL_URL}/$id.jpg"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2,17 +2,22 @@ package com.pixelized.biblib.ui.screen.home.page.news
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import com.pixelized.biblib.R
|
||||||
import com.pixelized.biblib.model.book.Book
|
import com.pixelized.biblib.model.book.Book
|
||||||
import com.pixelized.biblib.network.client.IBibLibClient
|
import com.pixelized.biblib.network.client.IBibLibClient
|
||||||
import com.pixelized.biblib.repository.book.IBookRepository
|
import com.pixelized.biblib.repository.book.IBookRepository
|
||||||
import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio
|
import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio
|
||||||
|
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
|
||||||
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
|
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
|
||||||
import com.pixelized.biblib.utils.BitmapCache
|
import com.pixelized.biblib.utils.CoverCache
|
||||||
|
import com.pixelized.biblib.utils.extention.context
|
||||||
import com.pixelized.biblib.utils.extention.longDate
|
import com.pixelized.biblib.utils.extention.longDate
|
||||||
|
import com.pixelized.biblib.utils.painterResource
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -24,7 +29,7 @@ import javax.inject.Inject
|
||||||
class NewsBookViewModel @Inject constructor(
|
class NewsBookViewModel @Inject constructor(
|
||||||
application: Application,
|
application: Application,
|
||||||
bookRepository: IBookRepository,
|
bookRepository: IBookRepository,
|
||||||
cache: BitmapCache,
|
cache: CoverCache,
|
||||||
) : ACoverViewModel(application, cache) {
|
) : ACoverViewModel(application, cache) {
|
||||||
|
|
||||||
private val newsSource: Flow<PagingData<BookThumbnailUio>> = Pager(
|
private val newsSource: Flow<PagingData<BookThumbnailUio>> = Pager(
|
||||||
|
|
@ -43,6 +48,14 @@ class NewsBookViewModel @Inject constructor(
|
||||||
author = author.joinToString { it.name },
|
author = author.joinToString { it.name },
|
||||||
date = releaseDate.longDate(),
|
date = releaseDate.longDate(),
|
||||||
isNew = isNew,
|
isNew = isNew,
|
||||||
cover = cover(url = URL("${IBibLibClient.THUMBNAIL_URL}/$id.jpg"))
|
cover = cover(
|
||||||
|
placeHolder = CoverUio(
|
||||||
|
type = CoverUio.Type.PLACE_HOLDER,
|
||||||
|
contentScale = ContentScale.None,
|
||||||
|
painter = painterResource(context, R.drawable.ic_baseline_auto_stories_24),
|
||||||
|
),
|
||||||
|
type = CoverUio.Type.THUMBNAIL,
|
||||||
|
url = URL("${IBibLibClient.THUMBNAIL_URL}/$id.jpg"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -17,8 +17,8 @@ data class BibLibDimen(
|
||||||
val extraLarge: Dp = 64.dp,
|
val extraLarge: Dp = 64.dp,
|
||||||
val dialog: Dialog = Dialog(),
|
val dialog: Dialog = Dialog(),
|
||||||
val thumbnail: BookThumbnail = BookThumbnail(),
|
val thumbnail: BookThumbnail = BookThumbnail(),
|
||||||
|
val detail: BookDetail = BookDetail(),
|
||||||
) {
|
) {
|
||||||
@Stable
|
@Stable
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Dialog(
|
data class Dialog(
|
||||||
|
|
@ -32,7 +32,14 @@ data class BibLibDimen(
|
||||||
data class BookThumbnail(
|
data class BookThumbnail(
|
||||||
val padding: Dp = 16.dp,
|
val padding: Dp = 16.dp,
|
||||||
val arrangement: Dp = 8.dp,
|
val arrangement: Dp = 8.dp,
|
||||||
val cover: DpSize = DpSize(60.dp, 96.dp),
|
val cover: DpSize = DpSize(64.dp, 102.dp), // ratio 1.6
|
||||||
val corner: Dp = 8.dp,
|
val corner: Dp = 8.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Immutable
|
||||||
|
data class BookDetail(
|
||||||
|
val placeHolder: Dp = 64.dp,
|
||||||
|
val cover: Dp = 384.dp,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
package com.pixelized.biblib.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.URL
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class BitmapCache @Inject constructor(context: Context) {
|
|
||||||
private var cache: File? = context.cacheDir
|
|
||||||
|
|
||||||
fun writeToDisk(url: URL, bitmap: Bitmap) {
|
|
||||||
val file = file(url)
|
|
||||||
try {
|
|
||||||
file.mkdirs()
|
|
||||||
file.delete()
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("BitmapCache", "bitmap?.compress() FAILED !", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readFromDisk(url: URL): Bitmap? {
|
|
||||||
val file = file(url)
|
|
||||||
return try {
|
|
||||||
BitmapFactory.decodeStream(file.inputStream())
|
|
||||||
} catch (e: IOException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun exist(url: URL): Boolean {
|
|
||||||
val file = file(url)
|
|
||||||
return file.exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun download(url: URL): Bitmap? {
|
|
||||||
Log.v("BitmapCache", "download: $url")
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
BitmapFactory.decodeStream(url.openStream())
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e("BitmapCache", "BitmapFactory.decodeStream(URL) FAILED !", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun download(
|
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
placeHolder: Painter,
|
|
||||||
url: URL,
|
|
||||||
): State<CoverUio> {
|
|
||||||
if (exist(url)) {
|
|
||||||
val bitmap = readFromDisk(url) ?: throw RuntimeException("")
|
|
||||||
val resource = BitmapPainter(bitmap.asImageBitmap())
|
|
||||||
return mutableStateOf(CoverUio(painter = resource))
|
|
||||||
} else {
|
|
||||||
val state = mutableStateOf(
|
|
||||||
CoverUio(
|
|
||||||
contentScale = ContentScale.None,
|
|
||||||
painter = placeHolder
|
|
||||||
)
|
|
||||||
)
|
|
||||||
coroutineScope.launch {
|
|
||||||
val resource = readFromDisk(url)?.let { BitmapPainter(it.asImageBitmap()) }
|
|
||||||
if (resource != null) {
|
|
||||||
state.value = CoverUio(painter = resource)
|
|
||||||
} else {
|
|
||||||
val downloaded = download(url)
|
|
||||||
if (downloaded != null) {
|
|
||||||
writeToDisk(url, downloaded)
|
|
||||||
state.value = CoverUio(painter = BitmapPainter(downloaded.asImageBitmap()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun file(url: URL): File = File(cache?.absolutePath + url.file)
|
|
||||||
}
|
|
||||||
101
app/src/main/java/com/pixelized/biblib/utils/CoverCache.kt
Normal file
101
app/src/main/java/com/pixelized/biblib/utils/CoverCache.kt
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
package com.pixelized.biblib.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.URL
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CoverCache @Inject constructor(context: Context) {
|
||||||
|
private var cache: File? = context.cacheDir
|
||||||
|
|
||||||
|
fun cover(
|
||||||
|
coroutineScope: CoroutineScope,
|
||||||
|
placeHolder: CoverUio,
|
||||||
|
url: URL,
|
||||||
|
type: CoverUio.Type,
|
||||||
|
contentScale: ContentScale,
|
||||||
|
tint: ColorFilter?,
|
||||||
|
): State<CoverUio> {
|
||||||
|
// read the cache a convert it to a UIO.
|
||||||
|
val cache = readFromDisk(url)?.let {
|
||||||
|
CoverUio(
|
||||||
|
type = type,
|
||||||
|
contentScale = contentScale,
|
||||||
|
colorFilter = tint,
|
||||||
|
painter = BitmapPainter(it.asImageBitmap()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// publish the cache and stop there, or publish the placeHolder and download the proper file.
|
||||||
|
return mutableStateOf(cache ?: placeHolder).also {
|
||||||
|
if (cache == null) {
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
it.download(
|
||||||
|
type = type,
|
||||||
|
contentScale = contentScale,
|
||||||
|
tint = tint,
|
||||||
|
url = url,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableState<CoverUio>.download(
|
||||||
|
type: CoverUio.Type,
|
||||||
|
contentScale: ContentScale,
|
||||||
|
tint: ColorFilter?,
|
||||||
|
url: URL,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val bitmap = BitmapFactory.decodeStream(url.openStream())
|
||||||
|
val painter = BitmapPainter(bitmap.asImageBitmap())
|
||||||
|
writeToDisk(url, bitmap)
|
||||||
|
value = CoverUio(
|
||||||
|
type = type,
|
||||||
|
contentScale = contentScale,
|
||||||
|
colorFilter = tint,
|
||||||
|
painter = painter,
|
||||||
|
)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.w("CoverCache", "Fail to download: {$url}", exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun file(url: URL): File = File(cache?.absolutePath + url.file)
|
||||||
|
|
||||||
|
private fun writeToDisk(url: URL, bitmap: Bitmap) {
|
||||||
|
val file = file(url)
|
||||||
|
try {
|
||||||
|
file.mkdirs()
|
||||||
|
file.delete()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("BitmapCache", "bitmap?.compress() FAILED !", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readFromDisk(url: URL): Bitmap? {
|
||||||
|
val file = file(url)
|
||||||
|
return try {
|
||||||
|
if (file.exists()) BitmapFactory.decodeStream(file.inputStream()) else null
|
||||||
|
} catch (e: IOException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue