Bläddra i källkod

Chapter transition tweaks (#9470)

* Chapter transition tweaks

* Chapter transition cleanups
Ivan Iskandar 1 år sedan
förälder
incheckning
d36cf5ce15

+ 281 - 75
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,
-                )
+            Spacer(Modifier.height(VerticalSpacerSize))
+        } else {
+            NoChapterNotification(
+                text = fallbackLabel,
+                modifier = Modifier.align(Alignment.CenterHorizontally),
+            )
+        }
 
-                Text(text = pluralStringResource(R.plurals.missing_chapters_warning, count = chapterGap, chapterGap))
+        if (bottomChapter != null) {
+            if (chapterGap > 0) {
+                ChapterGapWarning(
+                    gapCount = chapterGap,
+                    modifier = Modifier.align(Alignment.CenterHorizontally),
+                )
             }
 
-            Spacer(Modifier.height(16.dp))
-        }
+            Spacer(Modifier.height(VerticalSpacerSize))
 
-        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) }
+            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(
+                text = text,
+                style = MaterialTheme.typography.bodyMedium,
+            )
         }
+    }
+}
 
-        Text(chapter.name)
+@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),
+        )
 
-    chapter.scanlator?.let {
-        ProvideTextStyle(
-            MaterialTheme.typography.bodyMedium.copy(
-                color = LocalContentColor.current.copy(alpha = SecondaryItemAlpha),
+        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),
+                    )
+                },
             ),
-        ) {
-            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,
+            )
         }
     }
 }

+ 51 - 15
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
-
-        removeAllViews()
+        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
+        }
+    }
 
-        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,
+    )
 }