Manga details stub
parent
b68b9d0b4b
commit
1c24b7d0b2
@ -0,0 +1,49 @@
|
||||
package org.xtimms.tokusho.core.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DetailsToolbar(
|
||||
title: String,
|
||||
titleAlphaProvider: () -> Float,
|
||||
onBackClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundAlphaProvider: () -> Float = titleAlphaProvider
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalContentColor.current.copy(alpha = titleAlphaProvider()),
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
BackIconButton(
|
||||
onClick = onBackClicked
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme
|
||||
.surfaceColorAtElevation(3.dp)
|
||||
.copy(alpha = backgroundAlphaProvider())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package org.xtimms.tokusho.core.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.semantics.Role
|
||||
|
||||
enum class MangaCover(val ratio: Float) {
|
||||
Square(1f / 1f),
|
||||
Book(10f / 16f),
|
||||
;
|
||||
|
||||
@Composable
|
||||
operator fun invoke(
|
||||
data: Painter,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String = "",
|
||||
shape: Shape = MaterialTheme.shapes.small,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Image(
|
||||
painter = data,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier
|
||||
.aspectRatio(ratio)
|
||||
.clip(shape)
|
||||
.then(
|
||||
if (onClick != null) {
|
||||
Modifier.clickable(
|
||||
role = Role.Button,
|
||||
onClick = onClick,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val CoverPlaceholderColor = Color(0x1F888888)
|
||||
@ -0,0 +1,326 @@
|
||||
package org.xtimms.tokusho.sections.details
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.MenuBook
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
import androidx.compose.material.icons.outlined.Brush
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.DoneAll
|
||||
import androidx.compose.material.icons.outlined.Language
|
||||
import androidx.compose.material.icons.outlined.MenuBook
|
||||
import androidx.compose.material.icons.outlined.Pause
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.outlined.Schedule
|
||||
import androidx.compose.material.icons.outlined.Upcoming
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
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.blur
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.xtimms.tokusho.R
|
||||
import org.xtimms.tokusho.core.components.MangaCover
|
||||
import org.xtimms.tokusho.ui.theme.TokushoTheme
|
||||
import org.xtimms.tokusho.utils.secondaryItemAlpha
|
||||
|
||||
@Composable
|
||||
fun DetailsInfoBox(
|
||||
isTabletUi: Boolean,
|
||||
appBarPadding: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
val backdropGradientColors = listOf(
|
||||
Color.Transparent,
|
||||
MaterialTheme.colorScheme.background,
|
||||
)
|
||||
Image(
|
||||
painterResource(id = R.drawable.ookami),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(colors = backdropGradientColors),
|
||||
)
|
||||
}
|
||||
.blur(8.dp)
|
||||
.alpha(0.2f)
|
||||
)
|
||||
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||
if (!isTabletUi) {
|
||||
MangaAndSourceTitlesSmall(
|
||||
appBarPadding = appBarPadding,
|
||||
onCoverClick = { },
|
||||
title = "Ookami to Koushinryou",
|
||||
author = "Hasekura Isuna",
|
||||
artist = "Koume Keito",
|
||||
state = MangaState.FINISHED
|
||||
)
|
||||
} else {
|
||||
MangaAndSourceTitlesLarge(
|
||||
appBarPadding = appBarPadding,
|
||||
onCoverClick = { },
|
||||
title = "Ookami to Koushinryou",
|
||||
author = "Hasekura Isuna",
|
||||
artist = "Koume Keito",
|
||||
state = MangaState.FINISHED
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaAndSourceTitlesLarge(
|
||||
appBarPadding: Dp,
|
||||
onCoverClick: () -> Unit,
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
state: MangaState?
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier.fillMaxWidth(0.65f),
|
||||
data = painterResource(id = R.drawable.ookami),
|
||||
contentDescription = stringResource(R.string.manga_cover),
|
||||
onClick = onCoverClick,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
DetailsContentInfo(
|
||||
title = title,
|
||||
author = author,
|
||||
artist = artist,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaAndSourceTitlesSmall(
|
||||
appBarPadding: Dp,
|
||||
onCoverClick: () -> Unit,
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
state: MangaState?,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = appBarPadding + 16.dp, end = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
MangaCover.Book(
|
||||
modifier = Modifier
|
||||
.sizeIn(maxWidth = 100.dp)
|
||||
.align(Alignment.Top),
|
||||
data = painterResource(id = R.drawable.ookami),
|
||||
contentDescription = stringResource(R.string.manga_cover),
|
||||
onClick = onCoverClick,
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
DetailsContentInfo(
|
||||
title = title,
|
||||
author = author,
|
||||
artist = artist,
|
||||
)
|
||||
}
|
||||
}
|
||||
Row {
|
||||
DetailsRow(
|
||||
source = "MangaDex",
|
||||
chapters = "22 chapters",
|
||||
state = state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.DetailsContentInfo(
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Text(
|
||||
text = title.ifBlank { stringResource(id = R.string.unknown_title) },
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = textAlign
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Person,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = author?.takeIf { it.isNotBlank() }
|
||||
?: stringResource(id = R.string.unknown_author),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = textAlign
|
||||
)
|
||||
}
|
||||
|
||||
if (!artist.isNullOrBlank() && author != artist) {
|
||||
Row(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Brush,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = artist,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = textAlign,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.DetailsRow(
|
||||
source: String?,
|
||||
chapters: String?,
|
||||
state: MangaState?,
|
||||
textAlign: TextAlign? = LocalTextStyle.current.textAlign,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f).wrapContentSize().secondaryItemAlpha(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (state) {
|
||||
MangaState.ONGOING -> Icons.Outlined.Schedule
|
||||
MangaState.FINISHED -> Icons.Outlined.DoneAll
|
||||
MangaState.ABANDONED -> Icons.Outlined.Close
|
||||
MangaState.PAUSED -> Icons.Outlined.Pause
|
||||
MangaState.UPCOMING -> Icons.Outlined.Upcoming
|
||||
else -> Icons.Outlined.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(24.dp),
|
||||
)
|
||||
ProvideTextStyle(MaterialTheme.typography.bodySmall) {
|
||||
Text(
|
||||
text = when (state) {
|
||||
MangaState.ONGOING -> stringResource(id = R.string.ongoing)
|
||||
MangaState.FINISHED -> stringResource(id = R.string.finished)
|
||||
MangaState.ABANDONED -> stringResource(id = R.string.abandoned)
|
||||
MangaState.PAUSED -> stringResource(id = R.string.paused)
|
||||
MangaState.UPCOMING -> stringResource(id = R.string.upcoming)
|
||||
else -> stringResource(id = R.string.unknown)
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f).wrapContentSize().secondaryItemAlpha(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.MenuBook,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Text(
|
||||
text = chapters?.takeIf { it.isNotBlank() }
|
||||
?: stringResource(id = R.string.unknown),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = textAlign
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f).wrapContentSize().secondaryItemAlpha(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Language,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Text(
|
||||
text = source?.takeIf { it.isNotBlank() }
|
||||
?: stringResource(id = R.string.unknown),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = textAlign
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun DetailsInfoBoxPreview() {
|
||||
TokushoTheme {
|
||||
DetailsInfoBox(
|
||||
isTabletUi = false,
|
||||
appBarPadding = 72.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package org.xtimms.tokusho.sections.details
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import org.xtimms.tokusho.core.components.DetailsToolbar
|
||||
|
||||
const val DETAILS_DESTINATION = "details"
|
||||
|
||||
@Composable
|
||||
fun DetailsView(
|
||||
navigateBack: () -> Unit,
|
||||
) {
|
||||
|
||||
val chapterListState = rememberLazyListState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val isFirstItemVisible by remember {
|
||||
derivedStateOf { chapterListState.firstVisibleItemIndex == 0 }
|
||||
}
|
||||
val isFirstItemScrolled by remember {
|
||||
derivedStateOf { chapterListState.firstVisibleItemScrollOffset > 0 }
|
||||
}
|
||||
val animatedTitleAlpha by animateFloatAsState(
|
||||
if (!isFirstItemVisible) 1f else 0f,
|
||||
label = "Top Bar Title",
|
||||
)
|
||||
val animatedBgAlpha by animateFloatAsState(
|
||||
if (!isFirstItemVisible || isFirstItemScrolled) 1f else 0f,
|
||||
label = "Top Bar Background",
|
||||
)
|
||||
DetailsToolbar(
|
||||
title = "Test",
|
||||
titleAlphaProvider = { animatedTitleAlpha },
|
||||
backgroundAlphaProvider = { animatedBgAlpha },
|
||||
onBackClicked = { navigateBack() }
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
|
||||
},
|
||||
) { contentPadding ->
|
||||
val topPadding = contentPadding.calculateTopPadding()
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
state = chapterListState,
|
||||
contentPadding = PaddingValues(
|
||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||
bottom = contentPadding.calculateBottomPadding(),
|
||||
),
|
||||
) {
|
||||
item(
|
||||
key = DetailsViewItem.INFO_BOX,
|
||||
contentType = DetailsViewItem.INFO_BOX
|
||||
) {
|
||||
DetailsInfoBox(
|
||||
isTabletUi = false,
|
||||
appBarPadding = topPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package org.xtimms.tokusho.sections.details
|
||||
|
||||
enum class DetailsViewItem {
|
||||
INFO_BOX,
|
||||
ACTION_ROW,
|
||||
DESCRIPTION_WITH_TAG,
|
||||
CHAPTER_HEADER,
|
||||
CHAPTER,
|
||||
}
|
||||
Loading…
Reference in New Issue