瀏覽代碼

Use Compose for reader transition chapter info (#9373)

arkon 1 年之前
父節點
當前提交
0b125b7106

+ 170 - 0
app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt

@@ -0,0 +1,170 @@
+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.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.OfflinePin
+import androidx.compose.material.icons.outlined.Warning
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ProvideTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+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.unit.dp
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.toDomainChapter
+import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
+import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
+import tachiyomi.domain.chapter.service.calculateChapterGap
+import tachiyomi.domain.manga.model.Manga
+import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
+
+@Composable
+fun ChapterTransition(
+    transition: ChapterTransition,
+    downloadManager: DownloadManager,
+    manga: Manga?,
+) {
+    manga ?: return
+
+    val currChapter = transition.from.chapter
+    val currChapterDownloaded = transition.from.pageLoader is DownloadPageLoader
+
+    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) {
+            is ChapterTransition.Prev -> {
+                TransitionText(
+                    topLabel = stringResource(R.string.transition_previous),
+                    topChapter = goingToChapter,
+                    topChapterDownloaded = goingToChapterDownloaded,
+                    bottomLabel = stringResource(R.string.transition_current),
+                    bottomChapter = currChapter,
+                    bottomChapterDownloaded = currChapterDownloaded,
+                    fallbackLabel = stringResource(R.string.transition_no_previous),
+                    chapterGap = calculateChapterGap(currChapter.toDomainChapter(), goingToChapter?.toDomainChapter()),
+                )
+            }
+            is ChapterTransition.Next -> {
+                TransitionText(
+                    topLabel = stringResource(R.string.transition_finished),
+                    topChapter = currChapter,
+                    topChapterDownloaded = currChapterDownloaded,
+                    bottomLabel = stringResource(R.string.transition_next),
+                    bottomChapter = goingToChapter,
+                    bottomChapterDownloaded = goingToChapterDownloaded,
+                    fallbackLabel = stringResource(R.string.transition_no_next),
+                    chapterGap = calculateChapterGap(goingToChapter?.toDomainChapter(), currChapter.toDomainChapter()),
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun TransitionText(
+    topLabel: String,
+    topChapter: Chapter? = null,
+    topChapterDownloaded: Boolean,
+    bottomLabel: String,
+    bottomChapter: Chapter? = null,
+    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,
+        )
+        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(
+            text = if (hasBottomChapter) bottomLabel else fallbackLabel,
+            fontWeight = FontWeight.Bold,
+            textAlign = if (hasBottomChapter) TextAlign.Start else TextAlign.Center,
+        )
+        bottomChapter?.let { ChapterText(chapter = it, downloaded = bottomChapterDownloaded) }
+    }
+}
+
+@Composable
+private fun ColumnScope.ChapterText(
+    chapter: Chapter,
+    downloaded: Boolean,
+) {
+    FlowRow(
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        if (downloaded) {
+            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)
+        }
+    }
+}

+ 14 - 142
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt

@@ -1,30 +1,17 @@
 package eu.kanade.tachiyomi.ui.reader.viewer
 
 import android.content.Context
-import android.text.SpannableStringBuilder
-import android.text.style.ImageSpan
 import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.widget.LinearLayout
-import androidx.core.content.ContextCompat
-import androidx.core.text.bold
-import androidx.core.text.buildSpannedString
-import androidx.core.text.inSpans
-import androidx.core.view.isVisible
-import eu.kanade.tachiyomi.R
+import android.widget.FrameLayout
+import androidx.compose.ui.platform.ComposeView
+import eu.kanade.presentation.reader.ChapterTransition
 import eu.kanade.tachiyomi.data.download.DownloadManager
-import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
-import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
 import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
-import eu.kanade.tachiyomi.util.system.dpToPx
+import eu.kanade.tachiyomi.util.view.setComposeContent
 import tachiyomi.domain.manga.model.Manga
-import kotlin.math.roundToInt
 
 class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-    LinearLayout(context, attrs) {
-
-    private val binding: ReaderTransitionViewBinding =
-        ReaderTransitionViewBinding.inflate(LayoutInflater.from(context), this, true)
+    FrameLayout(context, attrs) {
 
     init {
         layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
@@ -32,133 +19,18 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
 
     fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) {
         manga ?: return
-        when (transition) {
-            is ChapterTransition.Prev -> bindPrevChapterTransition(transition, downloadManager, manga)
-            is ChapterTransition.Next -> bindNextChapterTransition(transition, downloadManager, manga)
-        }
-        missingChapterWarning(transition)
-    }
 
-    /**
-     * Binds a previous chapter transition on this view and subscribes to the page load status.
-     */
-    private fun bindPrevChapterTransition(
-        transition: ChapterTransition,
-        downloadManager: DownloadManager,
-        manga: Manga,
-    ) {
-        val prevChapter = transition.to?.chapter
+        removeAllViews()
 
-        binding.lowerText.isVisible = prevChapter != null
-        if (prevChapter != null) {
-            binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
-            val isPrevDownloaded = downloadManager.isChapterDownloaded(
-                prevChapter.name,
-                prevChapter.scanlator,
-                manga.title,
-                manga.source,
-                skipCache = true,
-            )
-            val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader
-            binding.upperText.text = buildSpannedString {
-                bold { append(context.getString(R.string.transition_previous)) }
-                append("\n${prevChapter.name}")
-                if (!prevChapter.scanlator.isNullOrBlank()) {
-                    append(DOT_SEPARATOR)
-                    append("${prevChapter.scanlator}")
-                }
-                if (isPrevDownloaded) addDLImageSpan()
+        val transitionView = ComposeView(context).apply {
+            setComposeContent {
+                ChapterTransition(
+                    transition = transition,
+                    downloadManager = downloadManager,
+                    manga = manga,
+                )
             }
-            binding.lowerText.text = buildSpannedString {
-                bold { append(context.getString(R.string.transition_current)) }
-                append("\n${transition.from.chapter.name}")
-                if (!transition.from.chapter.scanlator.isNullOrBlank()) {
-                    append(DOT_SEPARATOR)
-                    append("${transition.from.chapter.scanlator}")
-                }
-                if (isCurrentDownloaded) addDLImageSpan()
-            }
-        } else {
-            binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER
-            binding.upperText.text = context.getString(R.string.transition_no_previous)
         }
-    }
-
-    /**
-     * Binds a next chapter transition on this view and subscribes to the load status.
-     */
-    private fun bindNextChapterTransition(
-        transition: ChapterTransition,
-        downloadManager: DownloadManager,
-        manga: Manga,
-    ) {
-        val nextChapter = transition.to?.chapter
-
-        binding.lowerText.isVisible = nextChapter != null
-        if (nextChapter != null) {
-            binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
-            val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader
-            val isNextDownloaded = downloadManager.isChapterDownloaded(
-                nextChapter.name,
-                nextChapter.scanlator,
-                manga.title,
-                manga.source,
-                skipCache = true,
-            )
-            binding.upperText.text = buildSpannedString {
-                bold { append(context.getString(R.string.transition_finished)) }
-                append("\n${transition.from.chapter.name}")
-                if (!transition.from.chapter.scanlator.isNullOrBlank()) {
-                    append(DOT_SEPARATOR)
-                    append("${transition.from.chapter.scanlator}")
-                }
-                if (isCurrentDownloaded) addDLImageSpan()
-            }
-            binding.lowerText.text = buildSpannedString {
-                bold { append(context.getString(R.string.transition_next)) }
-                append("\n${nextChapter.name}")
-                if (!nextChapter.scanlator.isNullOrBlank()) {
-                    append(DOT_SEPARATOR)
-                    append("${nextChapter.scanlator}")
-                }
-                if (isNextDownloaded) addDLImageSpan()
-            }
-        } else {
-            binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER
-            binding.upperText.text = context.getString(R.string.transition_no_next)
-        }
-    }
-
-    private fun SpannableStringBuilder.addDLImageSpan() {
-        val icon = ContextCompat.getDrawable(context, R.drawable.ic_offline_pin_24dp)?.mutate()
-            ?.apply {
-                val size = binding.lowerText.textSize + 4.dpToPx
-                setTint(binding.lowerText.currentTextColor)
-                setBounds(0, 0, size.roundToInt(), size.roundToInt())
-            } ?: return
-        append(" ")
-        inSpans(ImageSpan(icon)) { append("image") }
-    }
-
-    private fun missingChapterWarning(transition: ChapterTransition) {
-        if (transition.to == null) {
-            binding.warning.isVisible = false
-            return
-        }
-
-        val chapterGap = when (transition) {
-            is ChapterTransition.Prev -> calculateChapterGap(transition.from, transition.to)
-            is ChapterTransition.Next -> calculateChapterGap(transition.to, transition.from)
-        }
-
-        if (chapterGap == 0) {
-            binding.warning.isVisible = false
-            return
-        }
-
-        binding.warningText.text = resources.getQuantityString(R.plurals.missing_chapters_warning, chapterGap.toInt(), chapterGap.toInt())
-        binding.warning.isVisible = true
+        addView(transitionView)
     }
 }
-
-private const val DOT_SEPARATOR = " • "

+ 18 - 0
app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt

@@ -25,6 +25,8 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionContext
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
 import androidx.core.view.forEach
 import com.google.android.material.shape.MaterialShapeDrawable
 import eu.kanade.presentation.theme.TachiyomiTheme
@@ -47,6 +49,22 @@ inline fun ComponentActivity.setComposeContent(
     }
 }
 
+fun ComposeView.setComposeContent(
+    content: @Composable () -> Unit,
+) {
+    setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+    setContent {
+        TachiyomiTheme {
+            CompositionLocalProvider(
+                LocalTextStyle provides MaterialTheme.typography.bodySmall,
+                LocalContentColor provides MaterialTheme.colorScheme.onBackground,
+            ) {
+                content()
+            }
+        }
+    }
+}
+
 /**
  * Adds a tooltip shown on long press.
  *

+ 0 - 4
app/src/main/res/layout/compose_controller.xml

@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent" />

+ 0 - 50
app/src/main/res/layout/reader_transition_view.xml

@@ -1,50 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:layout_gravity="center"
-    android:orientation="vertical">
-
-    <TextView
-        android:id="@+id/upper_text"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textAppearance="?attr/textAppearanceTitleMedium"
-        tools:text="Top" />
-
-    <LinearLayout
-        android:id="@+id/warning"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="16dp"
-        android:gravity="center_vertical">
-
-        <ImageView
-            android:layout_width="18dp"
-            android:layout_height="18dp"
-            android:layout_marginEnd="8dp"
-            android:scaleType="fitCenter"
-            app:srcCompat="@drawable/ic_warning_white_24dp"
-            app:tint="?attr/colorError"
-            tools:ignore="ContentDescription" />
-
-        <TextView
-            android:id="@+id/warning_text"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textAppearance="?attr/textAppearanceTitleMedium"
-            tools:text="Warning" />
-
-    </LinearLayout>
-
-    <TextView
-        android:id="@+id/lower_text"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="16dp"
-        android:textAppearance="?attr/textAppearanceTitleMedium"
-        tools:text="Bottom" />
-
-</LinearLayout>