123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- package eu.kanade.presentation.components
- import androidx.compose.foundation.background
- import androidx.compose.foundation.combinedClickable
- import androidx.compose.foundation.layout.Box
- import androidx.compose.foundation.layout.BoxScope
- import androidx.compose.foundation.layout.Column
- import androidx.compose.foundation.layout.Row
- import androidx.compose.foundation.layout.RowScope
- import androidx.compose.foundation.layout.aspectRatio
- import androidx.compose.foundation.layout.fillMaxHeight
- import androidx.compose.foundation.layout.fillMaxWidth
- import androidx.compose.foundation.layout.height
- import androidx.compose.foundation.layout.padding
- import androidx.compose.foundation.layout.size
- import androidx.compose.foundation.shape.RoundedCornerShape
- import androidx.compose.material.icons.Icons
- import androidx.compose.material.icons.filled.PlayArrow
- import androidx.compose.material3.FilledIconButton
- import androidx.compose.material3.Icon
- import androidx.compose.material3.IconButtonDefaults
- import androidx.compose.material3.LocalContentColor
- import androidx.compose.material3.MaterialTheme
- import androidx.compose.material3.Text
- import androidx.compose.material3.contentColorFor
- import androidx.compose.runtime.Composable
- import androidx.compose.runtime.CompositionLocalProvider
- import androidx.compose.ui.Alignment
- import androidx.compose.ui.Modifier
- import androidx.compose.ui.draw.alpha
- import androidx.compose.ui.draw.clip
- import androidx.compose.ui.graphics.Brush
- import androidx.compose.ui.graphics.Color
- import androidx.compose.ui.graphics.Shadow
- import androidx.compose.ui.graphics.drawscope.ContentDrawScope
- import androidx.compose.ui.node.DrawModifierNode
- import androidx.compose.ui.node.modifierElementOf
- import androidx.compose.ui.text.TextStyle
- import androidx.compose.ui.text.style.TextOverflow
- import androidx.compose.ui.unit.dp
- import androidx.compose.ui.unit.sp
- import eu.kanade.presentation.util.selectedBackground
- object CommonMangaItemDefaults {
- val GridHorizontalSpacer = 4.dp
- val GridVerticalSpacer = 4.dp
- const val BrowseFavoriteCoverAlpha = 0.34f
- }
- private val ContinueReadingButtonSize = 38.dp
- private const val GridSelectedCoverAlpha = 0.76f
- /**
- * Layout of grid list item with title overlaying the cover.
- * Accepts null [title] for a cover-only view.
- */
- @Composable
- fun MangaCompactGridItem(
- isSelected: Boolean = false,
- title: String? = null,
- coverData: eu.kanade.domain.manga.model.MangaCover,
- coverAlpha: Float = 1f,
- coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
- coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
- showContinueReadingButton: Boolean = false,
- onLongClick: () -> Unit,
- onClick: () -> Unit,
- onClickContinueReading: (() -> Unit)? = null,
- ) {
- GridItemSelectable(
- isSelected = isSelected,
- onClick = onClick,
- onLongClick = onLongClick,
- ) {
- MangaGridCover(
- cover = {
- MangaCover.Book(
- modifier = Modifier
- .fillMaxWidth()
- .alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
- data = coverData,
- )
- },
- badgesStart = coverBadgeStart,
- badgesEnd = coverBadgeEnd,
- content = {
- if (title != null) {
- CoverTextOverlay(title = title, showContinueReadingButton)
- }
- },
- continueReadingButton = {
- if (showContinueReadingButton && onClickContinueReading != null) {
- ContinueReadingButton(onClickContinueReading)
- }
- },
- )
- }
- }
- /**
- * Title overlay for [MangaCompactGridItem]
- */
- @Composable
- private fun BoxScope.CoverTextOverlay(
- title: String,
- showContinueReadingButton: Boolean = false,
- ) {
- Box(
- modifier = Modifier
- .clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
- .background(
- Brush.verticalGradient(
- 0f to Color.Transparent,
- 1f to Color(0xAA000000),
- ),
- )
- .fillMaxHeight(0.33f)
- .fillMaxWidth()
- .align(Alignment.BottomCenter),
- )
- val endPadding = if (showContinueReadingButton) ContinueReadingButtonSize else 8.dp
- GridItemTitle(
- modifier = Modifier
- .padding(start = 8.dp, top = 8.dp, end = endPadding, bottom = 8.dp)
- .align(Alignment.BottomStart),
- title = title,
- style = MaterialTheme.typography.titleSmall.copy(
- color = Color.White,
- shadow = Shadow(
- color = Color.Black,
- blurRadius = 4f,
- ),
- ),
- )
- }
- /**
- * Layout of grid list item with title below the cover.
- */
- @Composable
- fun MangaComfortableGridItem(
- isSelected: Boolean = false,
- title: String,
- coverData: eu.kanade.domain.manga.model.MangaCover,
- coverAlpha: Float = 1f,
- coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
- coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
- showContinueReadingButton: Boolean = false,
- onLongClick: () -> Unit,
- onClick: () -> Unit,
- onClickContinueReading: (() -> Unit)? = null,
- ) {
- GridItemSelectable(
- isSelected = isSelected,
- onClick = onClick,
- onLongClick = onLongClick,
- ) {
- Column {
- MangaGridCover(
- cover = {
- MangaCover.Book(
- modifier = Modifier
- .fillMaxWidth()
- .alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
- data = coverData,
- )
- },
- badgesStart = coverBadgeStart,
- badgesEnd = coverBadgeEnd,
- continueReadingButton = {
- if (showContinueReadingButton && onClickContinueReading != null) {
- ContinueReadingButton(onClickContinueReading)
- }
- },
- )
- GridItemTitle(
- modifier = Modifier.padding(4.dp),
- title = title,
- style = MaterialTheme.typography.titleSmall,
- )
- }
- }
- }
- /**
- * Common cover layout to add contents to be drawn on top of the cover.
- */
- @Composable
- private fun MangaGridCover(
- modifier: Modifier = Modifier,
- cover: @Composable BoxScope.() -> Unit = {},
- badgesStart: (@Composable RowScope.() -> Unit)? = null,
- badgesEnd: (@Composable RowScope.() -> Unit)? = null,
- continueReadingButton: (@Composable BoxScope.() -> Unit)? = null,
- content: @Composable (BoxScope.() -> Unit)? = null,
- ) {
- Box(
- modifier = modifier
- .fillMaxWidth()
- .aspectRatio(MangaCover.Book.ratio),
- ) {
- cover()
- content?.invoke(this)
- if (badgesStart != null) {
- BadgeGroup(
- modifier = Modifier
- .padding(4.dp)
- .align(Alignment.TopStart),
- content = badgesStart,
- )
- }
- if (badgesEnd != null) {
- BadgeGroup(
- modifier = Modifier
- .padding(4.dp)
- .align(Alignment.TopEnd),
- content = badgesEnd,
- )
- }
- continueReadingButton?.invoke(this)
- }
- }
- @Composable
- private fun GridItemTitle(
- modifier: Modifier,
- title: String,
- style: TextStyle,
- ) {
- Text(
- modifier = modifier,
- text = title,
- fontSize = 12.sp,
- lineHeight = 18.sp,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- style = style,
- )
- }
- /**
- * Wrapper for grid items to handle selection state, click and long click.
- */
- @Composable
- private fun GridItemSelectable(
- modifier: Modifier = Modifier,
- isSelected: Boolean,
- onClick: () -> Unit,
- onLongClick: () -> Unit,
- content: @Composable () -> Unit,
- ) {
- Box(
- modifier = modifier
- .clip(MaterialTheme.shapes.small)
- .combinedClickable(
- onClick = onClick,
- onLongClick = onLongClick,
- )
- .selectedOutline(isSelected = isSelected, color = MaterialTheme.colorScheme.secondary)
- .padding(4.dp),
- ) {
- val contentColor = if (isSelected) {
- MaterialTheme.colorScheme.onSecondary
- } else {
- LocalContentColor.current
- }
- CompositionLocalProvider(LocalContentColor provides contentColor) {
- content()
- }
- }
- }
- /**
- * @see GridItemSelectable
- */
- private fun Modifier.selectedOutline(
- isSelected: Boolean,
- color: Color,
- ): Modifier {
- class SelectedOutlineNode(var selected: Boolean, var color: Color) : DrawModifierNode, Modifier.Node() {
- override fun ContentDrawScope.draw() {
- if (selected) drawRect(color)
- drawContent()
- }
- }
- return this then modifierElementOf(
- params = isSelected.hashCode() + color.hashCode(),
- create = { SelectedOutlineNode(isSelected, color) },
- update = {
- it.selected = isSelected
- it.color = color
- },
- definitions = {
- name = "selectionOutline"
- properties["isSelected"] = isSelected
- properties["color"] = color
- },
- )
- }
- /**
- * Layout of list item.
- */
- @Composable
- fun MangaListItem(
- isSelected: Boolean = false,
- title: String,
- coverData: eu.kanade.domain.manga.model.MangaCover,
- coverAlpha: Float = 1f,
- badge: @Composable RowScope.() -> Unit,
- showContinueReadingButton: Boolean = false,
- onLongClick: () -> Unit,
- onClick: () -> Unit,
- onClickContinueReading: (() -> Unit)? = null,
- ) {
- Row(
- modifier = Modifier
- .selectedBackground(isSelected)
- .height(56.dp)
- .combinedClickable(
- onClick = onClick,
- onLongClick = onLongClick,
- )
- .padding(horizontal = 16.dp, vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- MangaCover.Square(
- modifier = Modifier
- .fillMaxHeight()
- .alpha(coverAlpha),
- data = coverData,
- )
- Text(
- text = title,
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .weight(1f),
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.bodyMedium,
- )
- BadgeGroup(content = badge)
- if (showContinueReadingButton && onClickContinueReading != null) {
- Box {
- ContinueReadingButton(onClickContinueReading)
- }
- }
- }
- }
- @Composable
- private fun BoxScope.ContinueReadingButton(
- onClickContinueReading: () -> Unit,
- ) {
- FilledIconButton(
- onClick = {
- onClickContinueReading()
- },
- modifier = Modifier
- .size(ContinueReadingButtonSize)
- .padding(3.dp)
- .align(Alignment.BottomEnd),
- shape = MaterialTheme.shapes.small,
- colors = IconButtonDefaults.filledIconButtonColors(
- containerColor = MaterialTheme.colorScheme.primaryContainer,
- contentColor = contentColorFor(MaterialTheme.colorScheme.primaryContainer),
- ),
- ) {
- Icon(
- imageVector = Icons.Filled.PlayArrow,
- contentDescription = "",
- modifier = Modifier
- .size(15.dp),
- )
- }
- }
|