Use Compose for reader transition chapter info (#9373)

This commit is contained in:
arkon 2023-04-22 16:33:36 -04:00 committed by GitHub
parent 320587e36e
commit 0b125b7106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 202 additions and 196 deletions

View File

@ -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)
}
}
}

View File

@ -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 transitionView = ComposeView(context).apply {
setComposeContent {
ChapterTransition(
transition = transition,
downloadManager = downloadManager,
manga = manga,
)
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()
}
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)
addView(transitionView)
}
}
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
}
}
private const val DOT_SEPARATOR = ""

View File

@ -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.
*

View File

@ -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" />

View File

@ -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>