mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Chapter transition tweaks (#9470)
* Chapter transition tweaks * Chapter transition cleanups
This commit is contained in:
		| @@ -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, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|     ) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user