diff --git a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt index 5d6afd3116..0639e75d77 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt @@ -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,80 +87,289 @@ 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( + modifier = Modifier + .widthIn(max = 460.dp) + .fillMaxWidth(), + ) { + if (topChapter != null) { + ChapterText( + header = topLabel, + name = topChapter.name, + scanlator = topChapter.scanlator, + downloaded = topChapterDownloaded, + ) - Column { - Text( - text = if (hasTopChapter) topLabel else fallbackLabel, - fontWeight = FontWeight.Bold, - textAlign = if (hasTopChapter) TextAlign.Start else TextAlign.Center, - ) - 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)) + Spacer(Modifier.height(VerticalSpacerSize)) + } else { + NoChapterNotification( + text = fallbackLabel, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) } - Text( - text = if (hasBottomChapter) bottomLabel else fallbackLabel, - fontWeight = FontWeight.Bold, - textAlign = if (hasBottomChapter) TextAlign.Start else TextAlign.Center, - ) - bottomChapter?.let { ChapterText(chapter = it, downloaded = bottomChapterDownloaded) } + 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 ColumnScope.ChapterText( - chapter: Chapter, - downloaded: Boolean, +private fun NoChapterNotification( + text: String, + modifier: Modifier = Modifier, ) { - FlowRow( - verticalAlignment = Alignment.CenterVertically, + OutlinedCard( + 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( - imageVector = Icons.Outlined.OfflinePin, - contentDescription = stringResource(R.string.label_downloaded), + imageVector = Icons.Outlined.Info, + tint = MaterialTheme.colorScheme.primary, + contentDescription = null, ) - Spacer(Modifier.width(8.dp)) - } - - Text(chapter.name) - } - - chapter.scanlator?.let { - ProvideTextStyle( - MaterialTheme.typography.bodyMedium.copy( - color = LocalContentColor.current.copy(alpha = SecondaryItemAlpha), - ), - ) { - Text(it) + 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( + 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, + ) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt index 250aaa15ad..295772c43d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt @@ -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 + 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() - - val transitionView = ComposeView(context).apply { - setComposeContent { - ChapterTransition( - transition = transition, - downloadManager = downloadManager, - manga = manga, - ) + @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, + ) }