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.Arrangement
import androidx.compose.foundation.layout.Column 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.OfflinePin import androidx.compose.material.icons.outlined.OfflinePin
import androidx.compose.material.icons.outlined.Warning 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.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Surface
import androidx.compose.material3.Text 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
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.style.TextAlign 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.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter 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.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.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import tachiyomi.domain.chapter.service.calculateChapterGap import tachiyomi.domain.chapter.service.calculateChapterGap
import tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.util.ThemePreviews
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable @Composable
fun ChapterTransition( fun ChapterTransition(
transition: ChapterTransition, transition: ChapterTransition,
downloadManager: DownloadManager, currChapterDownloaded: Boolean,
manga: Manga?, goingToChapterDownloaded: Boolean,
) { ) {
manga ?: return
val currChapter = transition.from.chapter val currChapter = transition.from.chapter
val currChapterDownloaded = transition.from.pageLoader?.isLocal == true
val goingToChapter = transition.to?.chapter 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) { ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
when (transition) { when (transition) {
@ -90,80 +87,289 @@ fun ChapterTransition(
@Composable @Composable
private fun TransitionText( private fun TransitionText(
topLabel: String, topLabel: String,
topChapter: Chapter? = null, topChapter: Chapter?,
topChapterDownloaded: Boolean, topChapterDownloaded: Boolean,
bottomLabel: String, bottomLabel: String,
bottomChapter: Chapter? = null, bottomChapter: Chapter?,
bottomChapterDownloaded: Boolean, bottomChapterDownloaded: Boolean,
fallbackLabel: String, fallbackLabel: String,
chapterGap: Int, chapterGap: Int,
) { ) {
val hasTopChapter = topChapter != null Column(
val hasBottomChapter = bottomChapter != null modifier = Modifier
.widthIn(max = 460.dp)
.fillMaxWidth(),
) {
if (topChapter != null) {
ChapterText(
header = topLabel,
name = topChapter.name,
scanlator = topChapter.scanlator,
downloaded = topChapterDownloaded,
)
Column { Spacer(Modifier.height(VerticalSpacerSize))
Text( } else {
text = if (hasTopChapter) topLabel else fallbackLabel, NoChapterNotification(
fontWeight = FontWeight.Bold, text = fallbackLabel,
textAlign = if (hasTopChapter) TextAlign.Start else TextAlign.Center, modifier = Modifier.align(Alignment.CenterHorizontally),
) )
topChapter?.let { ChapterText(chapter = it, downloaded = topChapterDownloaded) }
Spacer(Modifier.height(16.dp))
if (chapterGap > 0) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Warning,
tint = MaterialTheme.colorScheme.error,
contentDescription = null,
)
Text(text = pluralStringResource(R.plurals.missing_chapters_warning, count = chapterGap, chapterGap))
}
Spacer(Modifier.height(16.dp))
} }
Text( if (bottomChapter != null) {
text = if (hasBottomChapter) bottomLabel else fallbackLabel, if (chapterGap > 0) {
fontWeight = FontWeight.Bold, ChapterGapWarning(
textAlign = if (hasBottomChapter) TextAlign.Start else TextAlign.Center, gapCount = chapterGap,
) modifier = Modifier.align(Alignment.CenterHorizontally),
bottomChapter?.let { ChapterText(chapter = it, downloaded = bottomChapterDownloaded) } )
}
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 @Composable
private fun ColumnScope.ChapterText( private fun NoChapterNotification(
chapter: Chapter, text: String,
downloaded: Boolean, modifier: Modifier = Modifier,
) { ) {
FlowRow( OutlinedCard(
verticalAlignment = Alignment.CenterVertically, modifier = modifier,
colors = CardColor,
) { ) {
if (downloaded) { Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon( Icon(
imageVector = Icons.Outlined.OfflinePin, imageVector = Icons.Outlined.Info,
contentDescription = stringResource(R.string.label_downloaded), tint = MaterialTheme.colorScheme.primary,
contentDescription = null,
) )
Spacer(Modifier.width(8.dp)) Text(
} text = text,
style = MaterialTheme.typography.bodyMedium,
Text(chapter.name) )
} }
}
chapter.scanlator?.let { }
ProvideTextStyle(
MaterialTheme.typography.bodyMedium.copy( @Composable
color = LocalContentColor.current.copy(alpha = SecondaryItemAlpha), private fun ChapterGapWarning(
), gapCount: Int,
) { modifier: Modifier = Modifier,
Text(it) ) {
OutlinedCard(
modifier = modifier,
colors = CardColor,
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Warning,
tint = MaterialTheme.colorScheme.error,
contentDescription = null,
)
Text(
text = pluralStringResource(R.plurals.missing_chapters_warning, count = gapCount, gapCount),
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
@Composable
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,
) {
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),
)
},
),
)
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.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.FrameLayout import androidx.compose.material3.LocalContentColor
import androidx.compose.ui.platform.ComposeView 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.reader.ChapterTransition
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.util.view.setComposeContent
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs) { AbstractComposeView(context, attrs) {
private var data: Data? by mutableStateOf(null)
init { init {
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
} }
fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) { fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) {
manga ?: return data = if (manga != null) {
Data(
transition = transition,
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
}
}
removeAllViews() @Composable
override fun Content() {
val transitionView = ComposeView(context).apply { data?.let {
setComposeContent { TachiyomiTheme {
ChapterTransition( CompositionLocalProvider(
transition = transition, LocalTextStyle provides MaterialTheme.typography.bodySmall,
downloadManager = downloadManager, LocalContentColor provides MaterialTheme.colorScheme.onBackground,
manga = manga, ) {
) 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,
)
} }