Chapter transition tweaks (#9470)

* Chapter transition tweaks

* Chapter transition cleanups
This commit is contained in:
Ivan Iskandar 2023-05-07 21:08:33 +07:00 committed by GitHub
parent 332d9ff61b
commit d36cf5ce15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 339 additions and 97 deletions

View file

@ -2,60 +2,57 @@ package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.OfflinePin
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import tachiyomi.domain.chapter.service.calculateChapterGap
import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
fun ChapterTransition(
transition: ChapterTransition,
downloadManager: DownloadManager,
manga: Manga?,
currChapterDownloaded: Boolean,
goingToChapterDownloaded: Boolean,
) {
manga ?: return
val currChapter = transition.from.chapter
val currChapterDownloaded = transition.from.pageLoader?.isLocal == true
val goingToChapter = transition.to?.chapter
val goingToChapterDownloaded = if (goingToChapter != null) {
downloadManager.isChapterDownloaded(
goingToChapter.name,
goingToChapter.scanlator,
manga.title,
manga.source,
skipCache = true,
)
} else {
false
}
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
when (transition) {
@ -90,30 +87,101 @@ fun ChapterTransition(
@Composable
private fun TransitionText(
topLabel: String,
topChapter: Chapter? = null,
topChapter: Chapter?,
topChapterDownloaded: Boolean,
bottomLabel: String,
bottomChapter: Chapter? = null,
bottomChapter: Chapter?,
bottomChapterDownloaded: Boolean,
fallbackLabel: String,
chapterGap: Int,
) {
val hasTopChapter = topChapter != null
val hasBottomChapter = bottomChapter != null
Column {
Text(
text = if (hasTopChapter) topLabel else fallbackLabel,
fontWeight = FontWeight.Bold,
textAlign = if (hasTopChapter) TextAlign.Start else TextAlign.Center,
Column(
modifier = Modifier
.widthIn(max = 460.dp)
.fillMaxWidth(),
) {
if (topChapter != null) {
ChapterText(
header = topLabel,
name = topChapter.name,
scanlator = topChapter.scanlator,
downloaded = topChapterDownloaded,
)
topChapter?.let { ChapterText(chapter = it, downloaded = topChapterDownloaded) }
Spacer(Modifier.height(16.dp))
Spacer(Modifier.height(VerticalSpacerSize))
} else {
NoChapterNotification(
text = fallbackLabel,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
if (bottomChapter != null) {
if (chapterGap > 0) {
ChapterGapWarning(
gapCount = chapterGap,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
Spacer(Modifier.height(VerticalSpacerSize))
ChapterText(
header = bottomLabel,
name = bottomChapter.name,
scanlator = bottomChapter.scanlator,
downloaded = bottomChapterDownloaded,
)
} else {
NoChapterNotification(
text = fallbackLabel,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}
}
}
@Composable
private fun NoChapterNotification(
text: String,
modifier: Modifier = Modifier,
) {
OutlinedCard(
modifier = modifier,
colors = CardColor,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Info,
tint = MaterialTheme.colorScheme.primary,
contentDescription = null,
)
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
@Composable
private fun ChapterGapWarning(
gapCount: Int,
modifier: Modifier = Modifier,
) {
OutlinedCard(
modifier = modifier,
colors = CardColor,
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
@ -122,48 +190,186 @@ private fun TransitionText(
contentDescription = null,
)
Text(text = pluralStringResource(R.plurals.missing_chapters_warning, count = chapterGap, chapterGap))
}
Spacer(Modifier.height(16.dp))
}
Text(
text = if (hasBottomChapter) bottomLabel else fallbackLabel,
fontWeight = FontWeight.Bold,
textAlign = if (hasBottomChapter) TextAlign.Start else TextAlign.Center,
text = pluralStringResource(R.plurals.missing_chapters_warning, count = gapCount, gapCount),
style = MaterialTheme.typography.bodyMedium,
)
bottomChapter?.let { ChapterText(chapter = it, downloaded = bottomChapterDownloaded) }
}
}
}
@Composable
private fun ColumnScope.ChapterText(
chapter: Chapter,
private fun ChapterHeaderText(
text: String,
modifier: Modifier = Modifier,
) {
Text(
text = text,
modifier = modifier,
style = MaterialTheme.typography.titleMedium,
)
}
@Composable
private fun ChapterText(
header: String,
name: String,
scanlator: String?,
downloaded: Boolean,
) {
FlowRow(
verticalAlignment = Alignment.CenterVertically,
) {
Column {
ChapterHeaderText(
text = header,
modifier = Modifier.padding(bottom = 4.dp),
)
Text(
text = buildAnnotatedString {
if (downloaded) {
appendInlineContent(DownloadedIconContentId)
append(' ')
}
append(name)
},
fontSize = 20.sp,
maxLines = 5,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge,
inlineContent = mapOf(
DownloadedIconContentId to InlineTextContent(
Placeholder(
width = 22.sp,
height = 22.sp,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
),
) {
Icon(
imageVector = Icons.Outlined.OfflinePin,
contentDescription = stringResource(R.string.label_downloaded),
)
Spacer(Modifier.width(8.dp))
}
Text(chapter.name)
}
chapter.scanlator?.let {
ProvideTextStyle(
MaterialTheme.typography.bodyMedium.copy(
color = LocalContentColor.current.copy(alpha = SecondaryItemAlpha),
},
),
) {
Text(it)
)
scanlator?.let {
Text(
text = it,
modifier = Modifier
.secondaryItemAlpha()
.padding(top = 2.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
)
}
}
}
private val CardColor: CardColors
@Composable
get() = CardDefaults.outlinedCardColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurface,
)
private val VerticalSpacerSize = 24.dp
private const val DownloadedIconContentId = "downloaded"
private fun previewChapter(name: String, scanlator: String, chapterNumber: Float) = ChapterImpl().apply {
this.name = name
this.scanlator = scanlator
this.chapter_number = chapterNumber
this.id = 0
this.manga_id = 0
this.url = ""
}
private val FakeChapter = previewChapter(
name = "Vol.1, Ch.1 - Fake Chapter Title",
scanlator = "Scanlator Name",
chapterNumber = 1f,
)
private val FakeGapChapter = previewChapter(
name = "Vol.5, Ch.44 - Fake Gap Chapter Title",
scanlator = "Scanlator Name",
chapterNumber = 44f,
)
private val FakeChapterLongTitle = previewChapter(
name = "Vol.1, Ch.0 - The Mundane Musings of a Metafictional Manga: A Chapter About a Chapter, Featuring" +
" an Absurdly Long Title and a Surprisingly Normal Day in the Lives of Our Heroes, as They Grapple with the " +
"Daily Challenges of Existence, from Paying Rent to Finding Love, All While Navigating the Strange World of " +
"Fictional Realities and Reality-Bending Fiction, Where the Fourth Wall is Always in Danger of Being Broken " +
"and the Line Between Author and Character is Forever Blurred.",
scanlator = "Long Long Funny Scanlator Sniper Group Name Reborn",
chapterNumber = 1f,
)
@ThemePreviews
@Composable
private fun TransitionTextPreview() {
TachiyomiTheme {
Surface(modifier = Modifier.padding(48.dp)) {
ChapterTransition(
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), ReaderChapter(FakeChapter)),
currChapterDownloaded = false,
goingToChapterDownloaded = true,
)
}
}
}
@ThemePreviews
@Composable
private fun TransitionTextLongTitlePreview() {
TachiyomiTheme {
Surface(modifier = Modifier.padding(48.dp)) {
ChapterTransition(
transition = ChapterTransition.Next(ReaderChapter(FakeChapterLongTitle), ReaderChapter(FakeChapter)),
currChapterDownloaded = true,
goingToChapterDownloaded = true,
)
}
}
}
@ThemePreviews
@Composable
private fun TransitionTextWithGapPreview() {
TachiyomiTheme {
Surface(modifier = Modifier.padding(48.dp)) {
ChapterTransition(
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), ReaderChapter(FakeGapChapter)),
currChapterDownloaded = true,
goingToChapterDownloaded = false,
)
}
}
}
@ThemePreviews
@Composable
private fun TransitionTextNoNextPreview() {
TachiyomiTheme {
Surface(modifier = Modifier.padding(48.dp)) {
ChapterTransition(
transition = ChapterTransition.Next(ReaderChapter(FakeChapter), null),
currChapterDownloaded = true,
goingToChapterDownloaded = false,
)
}
}
}
@ThemePreviews
@Composable
private fun TransitionTextNoPreviousPreview() {
TachiyomiTheme {
Surface(modifier = Modifier.padding(48.dp)) {
ChapterTransition(
transition = ChapterTransition.Prev(ReaderChapter(FakeChapter), null),
currChapterDownloaded = true,
goingToChapterDownloaded = false,
)
}
}
}

View file

@ -2,35 +2,71 @@ package eu.kanade.tachiyomi.ui.reader.viewer
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.compose.ui.platform.ComposeView
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.AbstractComposeView
import eu.kanade.presentation.reader.ChapterTransition
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.util.view.setComposeContent
import tachiyomi.domain.manga.model.Manga
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs) {
AbstractComposeView(context, attrs) {
private var data: Data? by mutableStateOf(null)
init {
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
}
fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) {
manga ?: return
removeAllViews()
val transitionView = ComposeView(context).apply {
setComposeContent {
ChapterTransition(
data = if (manga != null) {
Data(
transition = transition,
downloadManager = downloadManager,
manga = manga,
currChapterDownloaded = transition.from.pageLoader?.isLocal == true,
goingToChapterDownloaded = transition.to?.chapter?.let { goingToChapter ->
downloadManager.isChapterDownloaded(
chapterName = goingToChapter.name,
chapterScanlator = goingToChapter.scanlator,
mangaTitle = manga.title,
sourceId = manga.source,
skipCache = true,
)
} ?: false,
)
} else {
null
}
}
@Composable
override fun Content() {
data?.let {
TachiyomiTheme {
CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.bodySmall,
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
) {
ChapterTransition(
transition = it.transition,
currChapterDownloaded = it.currChapterDownloaded,
goingToChapterDownloaded = it.goingToChapterDownloaded,
)
}
}
addView(transitionView)
}
}
private data class Data(
val transition: ChapterTransition,
val currChapterDownloaded: Boolean,
val goingToChapterDownloaded: Boolean,
)
}