diff --git a/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt b/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt index c8dffc7..d4e32f3 100644 --- a/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt +++ b/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt @@ -17,6 +17,8 @@ import coil.ImageLoader import org.xtimms.tokusho.core.model.ShelfCategory import org.xtimms.tokusho.core.motion.materialSharedAxisXIn 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.history.HistoryView import org.xtimms.tokusho.sections.list.LIST_DESTINATION @@ -154,6 +156,7 @@ fun Navigation( MangaListView( sourceName = "Source", navigateBack = navigateBack, + navigateToDetails = { navController.navigate(DETAILS_DESTINATION) } ) } @@ -169,6 +172,12 @@ fun Navigation( navigateBack = navigateBack, ) } + + composable(DETAILS_DESTINATION) { + DetailsView( + navigateBack = navigateBack, + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/components/DetailsToolbar.kt b/app/src/main/java/org/xtimms/tokusho/core/components/DetailsToolbar.kt new file mode 100644 index 0000000..710dfb4 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/core/components/DetailsToolbar.kt @@ -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()) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/components/MangaCover.kt b/app/src/main/java/org/xtimms/tokusho/core/components/MangaCover.kt new file mode 100644 index 0000000..2df1f62 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/core/components/MangaCover.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/sections/details/DetailsInfoHeader.kt b/app/src/main/java/org/xtimms/tokusho/sections/details/DetailsInfoHeader.kt new file mode 100644 index 0000000..d5bd723 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/sections/details/DetailsInfoHeader.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/sections/details/DetailsView.kt b/app/src/main/java/org/xtimms/tokusho/sections/details/DetailsView.kt new file mode 100644 index 0000000..151222f --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/sections/details/DetailsView.kt @@ -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, + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/sections/details/DetailsViewConstants.kt b/app/src/main/java/org/xtimms/tokusho/sections/details/DetailsViewConstants.kt new file mode 100644 index 0000000..c63972a --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/sections/details/DetailsViewConstants.kt @@ -0,0 +1,9 @@ +package org.xtimms.tokusho.sections.details + +enum class DetailsViewItem { + INFO_BOX, + ACTION_ROW, + DESCRIPTION_WITH_TAG, + CHAPTER_HEADER, + CHAPTER, +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/sections/list/MangaListView.kt b/app/src/main/java/org/xtimms/tokusho/sections/list/MangaListView.kt index 4e11f41..040d276 100644 --- a/app/src/main/java/org/xtimms/tokusho/sections/list/MangaListView.kt +++ b/app/src/main/java/org/xtimms/tokusho/sections/list/MangaListView.kt @@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -15,6 +17,7 @@ const val LIST_DESTINATION = "list" fun MangaListView( sourceName: String, navigateBack: () -> Unit, + navigateToDetails: () -> Unit, ) { val scrollState = rememberScrollState() @@ -29,7 +32,9 @@ fun MangaListView( .padding(padding), horizontalAlignment = Alignment.CenterHorizontally ) { - + Button(onClick = { navigateToDetails() }) { + Text(text = "Click") + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a26e25..bcaa6f9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,4 +59,13 @@ No manga in this category README Check the Gitea repository and the README + Manga cover + Unknown title + Unknown author + Ongoing + Finished + Abandoned + Paused + Upcoming + Unknown \ No newline at end of file