Manga details stub

master
Zakhar Timoshenko 2 years ago
parent b68b9d0b4b
commit 1c24b7d0b2
Signed by: Xtimms
SSH Key Fingerprint: SHA256:wH6spYepK/A5erBh7ZyAnr1ru9H4eaMVBEuiw6DSpxI

@ -17,6 +17,8 @@ import coil.ImageLoader
import org.xtimms.tokusho.core.model.ShelfCategory import org.xtimms.tokusho.core.model.ShelfCategory
import org.xtimms.tokusho.core.motion.materialSharedAxisXIn import org.xtimms.tokusho.core.motion.materialSharedAxisXIn
import org.xtimms.tokusho.core.motion.materialSharedAxisXOut import org.xtimms.tokusho.core.motion.materialSharedAxisXOut
import org.xtimms.tokusho.sections.details.DETAILS_DESTINATION
import org.xtimms.tokusho.sections.details.DetailsView
import org.xtimms.tokusho.sections.explore.ExploreView import org.xtimms.tokusho.sections.explore.ExploreView
import org.xtimms.tokusho.sections.history.HistoryView import org.xtimms.tokusho.sections.history.HistoryView
import org.xtimms.tokusho.sections.list.LIST_DESTINATION import org.xtimms.tokusho.sections.list.LIST_DESTINATION
@ -154,6 +156,7 @@ fun Navigation(
MangaListView( MangaListView(
sourceName = "Source", sourceName = "Source",
navigateBack = navigateBack, navigateBack = navigateBack,
navigateToDetails = { navController.navigate(DETAILS_DESTINATION) }
) )
} }
@ -169,6 +172,12 @@ fun Navigation(
navigateBack = navigateBack, navigateBack = navigateBack,
) )
} }
composable(DETAILS_DESTINATION) {
DetailsView(
navigateBack = navigateBack,
)
}
} }
} }

@ -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,
}

@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -15,6 +17,7 @@ const val LIST_DESTINATION = "list"
fun MangaListView( fun MangaListView(
sourceName: String, sourceName: String,
navigateBack: () -> Unit, navigateBack: () -> Unit,
navigateToDetails: () -> Unit,
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@ -29,7 +32,9 @@ fun MangaListView(
.padding(padding), .padding(padding),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Button(onClick = { navigateToDetails() }) {
Text(text = "Click")
}
} }
} }

@ -59,4 +59,13 @@
<string name="information_no_manga_category">No manga in this category</string> <string name="information_no_manga_category">No manga in this category</string>
<string name="readme">README</string> <string name="readme">README</string>
<string name="readme_desc">Check the Gitea repository and the README</string> <string name="readme_desc">Check the Gitea repository and the README</string>
<string name="manga_cover">Manga cover</string>
<string name="unknown_title">Unknown title</string>
<string name="unknown_author">Unknown author</string>
<string name="ongoing">Ongoing</string>
<string name="finished">Finished</string>
<string name="abandoned">Abandoned</string>
<string name="paused">Paused</string>
<string name="upcoming">Upcoming</string>
<string name="unknown">Unknown</string>
</resources> </resources>
Loading…
Cancel
Save