mihon/app/src/main/java/eu/kanade/presentation/components/CommonMangaItem.kt

318 lines
9.1 KiB
Kotlin
Raw Normal View History

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.shape.RoundedCornerShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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 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,
onLongClick: () -> Unit,
onClick: () -> Unit,
) {
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)
}
},
)
}
}
/**
* Title overlay for [MangaCompactGridItem]
*/
@Composable
private fun BoxScope.CoverTextOverlay(title: String) {
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),
)
GridItemTitle(
modifier = Modifier
.padding(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,
onLongClick: () -> Unit,
onClick: () -> Unit,
) {
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,
)
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,
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,
)
}
}
}
@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,
onLongClick: () -> Unit,
onClick: () -> Unit,
) {
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)
}
}