mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-30 20:46:27 -07:00
Share the reader's page chrome and page logic between viewers (#4193)
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package eu.kanade.presentation.reader
|
||||
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
/**
|
||||
* A page that failed to load or decode: the message, Retry, and Open in WebView when [onOpenInWebView] is given.
|
||||
*
|
||||
* [onButtonPressChange] tells a viewer that reads every touch for its own taps when a button is held.
|
||||
*/
|
||||
@Composable
|
||||
fun ReaderError(
|
||||
message: String?,
|
||||
onRetry: () -> Unit,
|
||||
onOpenInWebView: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
onButtonPressChange: ((pressed: Boolean) -> Unit)? = null,
|
||||
) {
|
||||
val buttonModifier = if (onButtonPressChange != null) Modifier.reportPress(onButtonPressChange) else Modifier
|
||||
Column(
|
||||
modifier = modifier.padding(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
|
||||
) {
|
||||
Text(
|
||||
text = message ?: stringResource(MR.strings.decode_image_error),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Button(onClick = onRetry, modifier = buttonModifier) {
|
||||
Text(stringResource(MR.strings.action_retry))
|
||||
}
|
||||
if (onOpenInWebView != null) {
|
||||
Button(onClick = onOpenInWebView, modifier = buttonModifier) {
|
||||
Text(stringResource(MR.strings.action_open_in_web_view))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Modifier.reportPress(onPressChange: (Boolean) -> Unit) = pointerInput(onPressChange) {
|
||||
awaitEachGesture {
|
||||
awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
|
||||
onPressChange(true)
|
||||
waitForUpOrCancellation(pass = PointerEventPass.Initial)
|
||||
onPressChange(false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
/**
|
||||
* How a viewer shows a wide page.
|
||||
*/
|
||||
interface DualPageSettings {
|
||||
val dualPageSplit: Boolean
|
||||
val dualPageInvert: Boolean
|
||||
val dualPageRotateToFit: Boolean
|
||||
val dualPageRotateToFitInvert: Boolean
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
|
||||
/**
|
||||
* The image of [page] as a pager shows it: a wide page rotated to fit or split in two, the half on the reading
|
||||
* side first, so [isLeftToRight] tells which. [onPageSplit] is told of the [InsertPage] for the second half.
|
||||
*/
|
||||
fun DualPageSettings.processPagedImage(
|
||||
page: ReaderPage,
|
||||
imageSource: BufferedSource,
|
||||
isLeftToRight: Boolean,
|
||||
onPageSplit: (InsertPage) -> Unit,
|
||||
): BufferedSource {
|
||||
if (dualPageRotateToFit) {
|
||||
return rotateDualPage(imageSource)
|
||||
}
|
||||
|
||||
if (!dualPageSplit) {
|
||||
return imageSource
|
||||
}
|
||||
|
||||
if (page is InsertPage) {
|
||||
return splitInHalf(imageSource, page, isLeftToRight)
|
||||
}
|
||||
|
||||
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||
if (!isDoublePage) {
|
||||
return imageSource
|
||||
}
|
||||
|
||||
onPageSplit(InsertPage(page))
|
||||
|
||||
return splitInHalf(imageSource, page, isLeftToRight)
|
||||
}
|
||||
|
||||
/**
|
||||
* The image of a page as a strip shows it: a wide page rotated to fit, or split with its halves one above the other.
|
||||
*/
|
||||
fun DualPageSettings.processStripImage(imageSource: BufferedSource): BufferedSource {
|
||||
if (dualPageRotateToFit) {
|
||||
return rotateDualPage(imageSource)
|
||||
}
|
||||
|
||||
if (dualPageSplit) {
|
||||
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||
if (isDoublePage) {
|
||||
val upperSide = if (dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
|
||||
return ImageUtil.splitAndMerge(imageSource, upperSide)
|
||||
}
|
||||
}
|
||||
|
||||
return imageSource
|
||||
}
|
||||
|
||||
private fun DualPageSettings.rotateDualPage(imageSource: BufferedSource): BufferedSource {
|
||||
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||
return if (isDoublePage) {
|
||||
val rotation = if (dualPageRotateToFitInvert) -90f else 90f
|
||||
ImageUtil.rotateImage(imageSource, rotation)
|
||||
} else {
|
||||
imageSource
|
||||
}
|
||||
}
|
||||
|
||||
private fun DualPageSettings.splitInHalf(
|
||||
imageSource: BufferedSource,
|
||||
page: ReaderPage,
|
||||
isLeftToRight: Boolean,
|
||||
): BufferedSource {
|
||||
var side = when {
|
||||
isLeftToRight && page is InsertPage -> ImageUtil.Side.RIGHT
|
||||
!isLeftToRight && page is InsertPage -> ImageUtil.Side.LEFT
|
||||
isLeftToRight && page !is InsertPage -> ImageUtil.Side.LEFT
|
||||
else -> ImageUtil.Side.RIGHT
|
||||
}
|
||||
|
||||
if (dualPageInvert) {
|
||||
side = when (side) {
|
||||
ImageUtil.Side.RIGHT -> ImageUtil.Side.LEFT
|
||||
ImageUtil.Side.LEFT -> ImageUtil.Side.RIGHT
|
||||
}
|
||||
}
|
||||
|
||||
return ImageUtil.splitInHalf(imageSource, side)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import com.google.android.material.R
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
|
||||
|
||||
/**
|
||||
* A button class to be used by child views of the pager viewer. All tap gestures are handled by
|
||||
* the pager, but this class disables that behavior to allow clickable buttons.
|
||||
*/
|
||||
class ReaderButton @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.materialButtonStyle,
|
||||
) : MaterialButton(context, attrs, defStyleAttr) {
|
||||
|
||||
var viewer: PagerViewer? = null
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
viewer?.pager?.setGestureDetectorEnabled(false)
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
viewer?.pager?.setGestureDetectorEnabled(true)
|
||||
}
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.presentation.reader.ReaderError
|
||||
import eu.kanade.presentation.util.formattedMessage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
|
||||
/**
|
||||
* [ReaderError] for a [page] that failed with [error]: Retry asks its loader again, and an image from the web can
|
||||
* be opened in a WebView with the cookies of the source [sourceId].
|
||||
*/
|
||||
@Composable
|
||||
fun ReaderPageError(
|
||||
page: ReaderPage,
|
||||
error: Throwable?,
|
||||
sourceId: Long?,
|
||||
modifier: Modifier = Modifier,
|
||||
onButtonPressChange: ((pressed: Boolean) -> Unit)? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val imageUrl = page.imageUrl?.takeIf { it.startsWith("http", ignoreCase = true) }
|
||||
ReaderError(
|
||||
message = with(context) { error?.formattedMessage },
|
||||
onRetry = { page.chapter.pageLoader?.retryPage(page) },
|
||||
onOpenInWebView = imageUrl?.let {
|
||||
{ context.startActivity(WebViewActivity.newIntent(context, it, sourceId)) }
|
||||
},
|
||||
modifier = modifier,
|
||||
onButtonPressChange = onButtonPressChange,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [ReaderPageError] for the view-based viewers, filling the view, in the theme of its reader-themed [context].
|
||||
*/
|
||||
class ReaderPageErrorView(context: Context) : AbstractComposeView(context) {
|
||||
|
||||
private var data by mutableStateOf<Data?>(null)
|
||||
|
||||
fun bind(
|
||||
page: ReaderPage,
|
||||
error: Throwable?,
|
||||
sourceId: Long?,
|
||||
onButtonPressChange: ((pressed: Boolean) -> Unit)? = null,
|
||||
) {
|
||||
data = Data(page, error, sourceId, onButtonPressChange)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val data = data ?: return
|
||||
ReaderThemedContent(context) {
|
||||
ReaderPageError(
|
||||
page = data.page,
|
||||
error = data.error,
|
||||
sourceId = data.sourceId,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onButtonPressChange = data.onButtonPressChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class Data(
|
||||
val page: ReaderPage,
|
||||
val error: Throwable?,
|
||||
val sourceId: Long?,
|
||||
val onButtonPressChange: ((pressed: Boolean) -> Unit)?,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
|
||||
/**
|
||||
* [content] in the app's theme under the reader's own night mode, from [readerThemedContext].
|
||||
*
|
||||
* Pages sit on the reader background, so a light app over a black reader would otherwise draw dark text on black.
|
||||
* [TachiyomiTheme] reads that from the configuration, which the view-based viewers got by inflating their views
|
||||
* from a reader-themed context.
|
||||
*/
|
||||
@Composable
|
||||
fun ReaderThemedContent(readerThemedContext: Context, content: @Composable () -> Unit) {
|
||||
CompositionLocalProvider(
|
||||
LocalContext provides readerThemedContext,
|
||||
LocalConfiguration provides readerThemedContext.resources.configuration,
|
||||
) {
|
||||
TachiyomiTheme(content = content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.reader.ChapterTransition
|
||||
import eu.kanade.presentation.reader.reportPress
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun ReaderTransitionPage(
|
||||
transition: ChapterTransition,
|
||||
downloadState: TransitionDownloadState?,
|
||||
onRetry: (ReaderChapter) -> Unit,
|
||||
onButtonPressChange: (pressed: Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
statusPadding: PaddingValues = PaddingValues(),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (downloadState != null) {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.bodySmall,
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
||||
) {
|
||||
ChapterTransition(
|
||||
transition,
|
||||
downloadState.currChapterDownloaded,
|
||||
downloadState.goingToChapterDownloaded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val toChapter = transition.to ?: return@Column
|
||||
val state by toChapter.stateFlow.collectAsState()
|
||||
when (val state = state) {
|
||||
is ReaderChapter.State.Loading -> Column(
|
||||
modifier = Modifier.padding(statusPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Text(
|
||||
text = stringResource(MR.strings.transition_pages_loading),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
is ReaderChapter.State.Error -> Column(
|
||||
modifier = Modifier.padding(statusPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.transition_pages_error, state.error.message ?: ""),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Button(
|
||||
onClick = { onRetry(toChapter) },
|
||||
modifier = Modifier.reportPress(onButtonPressChange),
|
||||
) {
|
||||
Text(stringResource(MR.strings.action_retry))
|
||||
}
|
||||
}
|
||||
is ReaderChapter.State.Wait, is ReaderChapter.State.Loaded -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,91 +2,58 @@ package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
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.source.Source
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.source.local.isLocal
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
|
||||
/**
|
||||
* [ReaderTransitionPage] for the view-based viewers, in the theme of its reader-themed [context].
|
||||
*/
|
||||
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
AbstractComposeView(context, attrs) {
|
||||
|
||||
private var data: Data? by mutableStateOf(null)
|
||||
|
||||
init {
|
||||
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?, source: Source?) {
|
||||
data = if (manga != null && source != null) {
|
||||
val goingToChapter = transition.to?.chapter.takeUnless { manga.isLocal() }
|
||||
Data(
|
||||
transition = transition,
|
||||
currChapterDownloaded = transition.from.pageLoader?.isLocal == true,
|
||||
goingToChapterDownloaded = manga.isLocal() ||
|
||||
goingToChapter?.let {
|
||||
downloadManager.isChapterDownloaded(
|
||||
chapterName = it.name,
|
||||
chapterScanlator = it.scanlator,
|
||||
chapterUrl = it.url,
|
||||
mangaTitle = manga.title,
|
||||
sourceId = manga.source,
|
||||
)
|
||||
} ?: false,
|
||||
isGoingToChapterOnDisk = goingToChapter?.let {
|
||||
{
|
||||
downloadManager.isChapterDownloadedOnDisk(
|
||||
chapterName = it.name,
|
||||
chapterScanlator = it.scanlator,
|
||||
chapterUrl = it.url,
|
||||
mangaTitle = manga.title,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
fun bind(
|
||||
transition: ChapterTransition,
|
||||
downloadState: TransitionDownloadState?,
|
||||
onRetry: (ReaderChapter) -> Unit,
|
||||
onButtonPressChange: (pressed: Boolean) -> Unit = {},
|
||||
statusPadding: PaddingValues = PaddingValues(),
|
||||
) {
|
||||
data = Data(transition, downloadState, onRetry, onButtonPressChange, statusPadding)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
data?.let {
|
||||
val goingToChapterDownloaded by produceState(it.goingToChapterDownloaded, it) {
|
||||
it.isGoingToChapterOnDisk?.let { isOnDisk -> value = isOnDisk() }
|
||||
}
|
||||
TachiyomiTheme {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.bodySmall,
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
||||
) {
|
||||
ChapterTransition(
|
||||
transition = it.transition,
|
||||
currChapterDownloaded = it.currChapterDownloaded,
|
||||
goingToChapterDownloaded = goingToChapterDownloaded,
|
||||
)
|
||||
}
|
||||
}
|
||||
val data = data ?: return
|
||||
LaunchedEffect(data.downloadState) { data.downloadState?.checkDisk() }
|
||||
ReaderThemedContent(context) {
|
||||
ReaderTransitionPage(
|
||||
transition = data.transition,
|
||||
downloadState = data.downloadState,
|
||||
onRetry = data.onRetry,
|
||||
onButtonPressChange = data.onButtonPressChange,
|
||||
statusPadding = data.statusPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class Data(
|
||||
val transition: ChapterTransition,
|
||||
val currChapterDownloaded: Boolean,
|
||||
val goingToChapterDownloaded: Boolean,
|
||||
val isGoingToChapterOnDisk: (suspend () -> Boolean)?,
|
||||
val downloadState: TransitionDownloadState?,
|
||||
val onRetry: (ReaderChapter) -> Unit,
|
||||
val onButtonPressChange: (pressed: Boolean) -> Unit,
|
||||
val statusPadding: PaddingValues,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.source.local.isLocal
|
||||
|
||||
class TransitionDownloadState(
|
||||
val currChapterDownloaded: Boolean,
|
||||
goingToChapterDownloaded: Boolean,
|
||||
private val isGoingToChapterOnDisk: (suspend () -> Boolean)?,
|
||||
) {
|
||||
var goingToChapterDownloaded by mutableStateOf(goingToChapterDownloaded)
|
||||
private set
|
||||
|
||||
suspend fun checkDisk() {
|
||||
isGoingToChapterOnDisk?.let { goingToChapterDownloaded = it() }
|
||||
}
|
||||
}
|
||||
|
||||
fun DownloadManager.transitionDownloadState(
|
||||
transition: ChapterTransition,
|
||||
manga: Manga?,
|
||||
source: Source?,
|
||||
): TransitionDownloadState? {
|
||||
if (manga == null || source == null) return null
|
||||
val goingToChapter = transition.to?.chapter.takeUnless { manga.isLocal() }
|
||||
return TransitionDownloadState(
|
||||
currChapterDownloaded = transition.from.pageLoader?.isLocal == true,
|
||||
goingToChapterDownloaded = manga.isLocal() ||
|
||||
goingToChapter?.let {
|
||||
isChapterDownloaded(
|
||||
chapterName = it.name,
|
||||
chapterScanlator = it.scanlator,
|
||||
chapterUrl = it.url,
|
||||
mangaTitle = manga.title,
|
||||
sourceId = manga.source,
|
||||
)
|
||||
} ?: false,
|
||||
isGoingToChapterOnDisk = goingToChapter?.let {
|
||||
{
|
||||
isChapterDownloadedOnDisk(
|
||||
chapterName = it.name,
|
||||
chapterScanlator = it.scanlator,
|
||||
chapterUrl = it.url,
|
||||
mangaTitle = manga.title,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
|
||||
/**
|
||||
* The items of a viewer for [chapters], top to bottom: the previous chapter's pages, its transition, [currentPages],
|
||||
* then the next transition and chapter's pages, so moving between chapters is seamless. A transition is left out
|
||||
* when its chapter is loaded and adjacent, unless [forceTransition].
|
||||
*/
|
||||
fun viewerItems(
|
||||
chapters: ViewerChapters,
|
||||
forceTransition: Boolean,
|
||||
currentPages: List<ReaderPage>? = chapters.currChapter.pages,
|
||||
): MutableList<Any> {
|
||||
val items = mutableListOf<Any>()
|
||||
|
||||
// Forces chapter transition if there is missing chapters
|
||||
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
|
||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||
|
||||
// Add previous chapter pages and transition
|
||||
chapters.prevChapter?.pages?.let(items::addAll)
|
||||
|
||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||
items.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter))
|
||||
}
|
||||
|
||||
// Add current chapter.
|
||||
currentPages?.let(items::addAll)
|
||||
|
||||
// Add next chapter transition and pages.
|
||||
if (nextHasMissingChapters || forceTransition || chapters.nextChapter?.state !is ReaderChapter.State.Loaded) {
|
||||
items.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
|
||||
}
|
||||
|
||||
chapters.nextChapter?.pages?.let(items::addAll)
|
||||
|
||||
return items
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.viewerItems
|
||||
|
||||
/**
|
||||
* Items of a pager, where [ViewerChapters] updates are posted, inverted when the pager [isReversed] reads right to
|
||||
* left. Wide pages split in two get an [InsertPage] for their second half.
|
||||
*/
|
||||
class PagerItems(private val isReversed: Boolean) {
|
||||
|
||||
/**
|
||||
* List of currently set items.
|
||||
*/
|
||||
var items: List<Any> by mutableStateOf(emptyList())
|
||||
private set
|
||||
|
||||
/**
|
||||
* Holds preprocessed items so they don't get removed when changing chapter
|
||||
*/
|
||||
private var preprocessed: List<InsertPage> = emptyList()
|
||||
|
||||
var nextTransition: ChapterTransition.Next? = null
|
||||
private set
|
||||
|
||||
var currentChapter: ReaderChapter? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Updates the items with the given [chapters] while the pager is at [currentPosition], and returns where it must
|
||||
* go next.
|
||||
*/
|
||||
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean, currentPosition: Int): Update {
|
||||
var insertPageLastPage: InsertPage? = null
|
||||
|
||||
val currPages = chapters.currChapter.pages?.toMutableList()?.also { pages ->
|
||||
val lastPage = pages.last()
|
||||
|
||||
// Insert preprocessed pages into current page list. Pages of the adjacent chapters are split too, and
|
||||
// placed by their index, so only this chapter's belong here.
|
||||
preprocessed.filter { it.chapter.chapter.id == chapters.currChapter.chapter.id }
|
||||
.sortedByDescending { it.index }
|
||||
.forEach { page ->
|
||||
if (lastPage.index == page.index) {
|
||||
insertPageLastPage = page
|
||||
}
|
||||
pages.add(page.index + 1, page)
|
||||
}
|
||||
}
|
||||
|
||||
val newItems = viewerItems(chapters, forceTransition, currPages)
|
||||
currentChapter = chapters.currChapter
|
||||
nextTransition = ChapterTransition.Next(chapters.currChapter, chapters.nextChapter)
|
||||
|
||||
val currentItem = items.getOrNull(currentPosition)
|
||||
|
||||
if (isReversed) {
|
||||
newItems.reverse()
|
||||
}
|
||||
|
||||
val adjacentChapterIds = setOfNotNull(chapters.prevChapter?.chapter?.id, chapters.nextChapter?.chapter?.id)
|
||||
preprocessed = preprocessed.filter { it.chapter.chapter.id in adjacentChapterIds }
|
||||
items = newItems
|
||||
|
||||
// The pager only follows the pages it has laid out, so before its first layout it would stay at the same
|
||||
// position while pages are added in front, e.g. the next chapter's in right to left, and land in another
|
||||
// chapter
|
||||
val position = items.indexOf(currentItem).takeIf { it != -1 && it != currentPosition }
|
||||
return Update(position, insertPageLastPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds [newPage], the second half of [currentPage], and returns whether the items changed.
|
||||
*/
|
||||
fun onPageSplit(currentPage: Any?, newPage: InsertPage): Boolean {
|
||||
if (currentPage !is ReaderPage) return false
|
||||
|
||||
val currentIndex = items.indexOf(currentPage)
|
||||
|
||||
// Put aside pages of an adjacent chapter until it becomes the current one, so they are not lost on the change
|
||||
if (currentPage.chapter.chapter.id != currentChapter?.chapter?.id) {
|
||||
val chapterId = newPage.chapter.chapter.id
|
||||
if (preprocessed.none { it.chapter.chapter.id == chapterId && it.index == newPage.index }) {
|
||||
preprocessed = preprocessed + newPage
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val placeAtIndex = if (isReversed) currentIndex else currentIndex + 1
|
||||
|
||||
// It will enter a endless cycle of insert pages
|
||||
if (isReversed && placeAtIndex - 1 >= 0 && items[placeAtIndex - 1] is InsertPage) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Same here it will enter a endless cycle of insert pages
|
||||
if (items[placeAtIndex] is InsertPage) {
|
||||
return false
|
||||
}
|
||||
|
||||
items = items.toMutableList().apply { add(placeAtIndex, newPage) }
|
||||
return true
|
||||
}
|
||||
|
||||
fun cleanupPageSplit() {
|
||||
items = items.filterNot { it is InsertPage }
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the pager goes after the items change: to [position] to stay on the same item, then to [insertPage] at
|
||||
* the end of the chapter, which it would skip otherwise.
|
||||
*/
|
||||
class Update(val position: Int?, val insertPage: InsertPage?)
|
||||
}
|
||||
@@ -2,16 +2,12 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.view.isVisible
|
||||
import eu.kanade.presentation.util.formattedMessage
|
||||
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageErrorView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.processPagedImage
|
||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -22,11 +18,8 @@ import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import logcat.LogPriority
|
||||
import okio.Buffer
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
/**
|
||||
* View of the ViewPager that contains a page of a chapter.
|
||||
@@ -52,7 +45,7 @@ class PagerPageHolder(
|
||||
/**
|
||||
* Error layout to show when the image fails to load.
|
||||
*/
|
||||
private var errorLayout: ReaderErrorBinding? = null
|
||||
private var errorLayout: ReaderPageErrorView? = null
|
||||
|
||||
private val scope = MainScope()
|
||||
|
||||
@@ -153,7 +146,9 @@ class PagerPageHolder(
|
||||
val buffer = withContext(Dispatchers.IO) { streamFn().use { Buffer().readFrom(it) } }
|
||||
// Splitting, merging and sampling the background decode the image, which is CPU work
|
||||
val (source, isAnimated, background) = withContext(Dispatchers.Default) {
|
||||
val source = process(item, buffer)
|
||||
val source = viewer.simplePaged.processPagedImage(item, buffer, viewer is L2RPagerViewer) {
|
||||
viewer.onPageSplit(item, it)
|
||||
}
|
||||
val isAnimated = ImageUtil.isAnimatedAndSupported(source)
|
||||
val background = if (!isAnimated && viewer.general.automaticBackground) {
|
||||
ImageUtil.chooseBackground(context, source.peek().inputStream())
|
||||
@@ -187,63 +182,6 @@ class PagerPageHolder(
|
||||
}
|
||||
}
|
||||
|
||||
private fun process(page: ReaderPage, imageSource: BufferedSource): BufferedSource {
|
||||
if (viewer.simplePaged.dualPageRotateToFit) {
|
||||
return rotateDualPage(imageSource)
|
||||
}
|
||||
|
||||
if (!viewer.simplePaged.dualPageSplit) {
|
||||
return imageSource
|
||||
}
|
||||
|
||||
if (page is InsertPage) {
|
||||
return splitInHalf(imageSource)
|
||||
}
|
||||
|
||||
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||
if (!isDoublePage) {
|
||||
return imageSource
|
||||
}
|
||||
|
||||
onPageSplit(page)
|
||||
|
||||
return splitInHalf(imageSource)
|
||||
}
|
||||
|
||||
private fun rotateDualPage(imageSource: BufferedSource): BufferedSource {
|
||||
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||
return if (isDoublePage) {
|
||||
val rotation = if (viewer.simplePaged.dualPageRotateToFitInvert) -90f else 90f
|
||||
ImageUtil.rotateImage(imageSource, rotation)
|
||||
} else {
|
||||
imageSource
|
||||
}
|
||||
}
|
||||
|
||||
private fun splitInHalf(imageSource: BufferedSource): BufferedSource {
|
||||
var side = when {
|
||||
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT
|
||||
viewer !is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT
|
||||
viewer is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.LEFT
|
||||
viewer !is L2RPagerViewer && page !is InsertPage -> ImageUtil.Side.RIGHT
|
||||
else -> error("We should choose a side!")
|
||||
}
|
||||
|
||||
if (viewer.simplePaged.dualPageInvert) {
|
||||
side = when (side) {
|
||||
ImageUtil.Side.RIGHT -> ImageUtil.Side.LEFT
|
||||
ImageUtil.Side.LEFT -> ImageUtil.Side.RIGHT
|
||||
}
|
||||
}
|
||||
|
||||
return ImageUtil.splitInHalf(imageSource, side)
|
||||
}
|
||||
|
||||
private fun onPageSplit(page: ReaderPage) {
|
||||
val newPage = InsertPage(page)
|
||||
viewer.onPageSplit(page, newPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the page has an error.
|
||||
*/
|
||||
@@ -273,41 +211,24 @@ class PagerPageHolder(
|
||||
viewer.activity.hideMenu()
|
||||
}
|
||||
|
||||
private fun showErrorLayout(error: Throwable?): ReaderErrorBinding {
|
||||
if (errorLayout == null) {
|
||||
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
errorLayout?.actionRetry?.viewer = viewer
|
||||
errorLayout?.actionRetry?.setOnClickListener {
|
||||
page.chapter.pageLoader?.retryPage(page)
|
||||
}
|
||||
private fun showErrorLayout(error: Throwable?) {
|
||||
val errorLayout = errorLayout ?: ReaderPageErrorView(context).also {
|
||||
addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
||||
errorLayout = it
|
||||
}
|
||||
|
||||
val imageUrl = page.imageUrl
|
||||
errorLayout?.actionOpenInWebView?.isVisible = imageUrl != null
|
||||
if (imageUrl != null) {
|
||||
if (imageUrl.startsWith("http", true)) {
|
||||
errorLayout?.actionOpenInWebView?.viewer = viewer
|
||||
errorLayout?.actionOpenInWebView?.setOnClickListener {
|
||||
val sourceId = viewer.activity.viewModel.manga?.source
|
||||
|
||||
val intent = WebViewActivity.newIntent(context, imageUrl, sourceId)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorLayout?.errorMessage?.text = with(context) { error?.formattedMessage }
|
||||
?: context.stringResource(MR.strings.decode_image_error)
|
||||
|
||||
errorLayout?.root?.isVisible = true
|
||||
return errorLayout!!
|
||||
errorLayout.bind(
|
||||
page = page,
|
||||
error = error,
|
||||
sourceId = viewer.activity.viewModel.manga?.source,
|
||||
onButtonPressChange = { pressed -> viewer.pager.setGestureDetectorEnabled(!pressed) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the decode error layout from the holder, if found.
|
||||
*/
|
||||
private fun removeErrorLayout() {
|
||||
errorLayout?.root?.isVisible = false
|
||||
errorLayout?.let(::removeView)
|
||||
errorLayout = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
|
||||
/**
|
||||
* The item a pager of [items] is on, shared by the view and Compose pagers. It tells [activity] of each new page,
|
||||
* preloads the chapters around it, and gives [onPageHolderSelected] the page along with whether it was reached moving
|
||||
* forward. Chapters set while the pager moves are held until it's idle and then given to [setChaptersNow].
|
||||
*/
|
||||
class PagerPageTracker(
|
||||
private val activity: ReaderActivity,
|
||||
private val items: PagerItems,
|
||||
private val setChaptersNow: (ViewerChapters) -> Unit,
|
||||
private val onPageHolderSelected: (page: ReaderPage, forward: Boolean) -> Unit,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Currently active item. It can be a chapter page or a chapter transition.
|
||||
*/
|
||||
var currentPage: Any? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Viewer chapters to set when the pager enters idle mode. Otherwise, if the view was settling
|
||||
* or dragging, there'd be a noticeable and annoying jump.
|
||||
*/
|
||||
private var awaitingIdleViewerChapters: ViewerChapters? = null
|
||||
|
||||
/**
|
||||
* Whether the pager is currently in idle mode. It sets the awaiting chapters if setting
|
||||
* this field to true.
|
||||
*/
|
||||
var isIdle = true
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
awaitingIdleViewerChapters?.let { viewerChapters ->
|
||||
setChaptersNow(viewerChapters)
|
||||
awaitingIdleViewerChapters = null
|
||||
if (viewerChapters.currChapter.pages?.size == 1) {
|
||||
items.nextTransition?.to?.let(activity::requestPreloadChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given [chapters] now if the pager is idle, otherwise once it becomes idle.
|
||||
*/
|
||||
fun setChapters(chapters: ViewerChapters) {
|
||||
if (isIdle) {
|
||||
setChaptersNow(chapters)
|
||||
} else {
|
||||
awaitingIdleViewerChapters = chapters
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new page (either a [ReaderPage] or [ChapterTransition]) is marked as active
|
||||
*/
|
||||
fun onPageChange(position: Int) {
|
||||
val page = items.items.getOrNull(position)
|
||||
if (page != null && currentPage != page) {
|
||||
val allowPreload = checkAllowPreload(page as? ReaderPage)
|
||||
val forward = when {
|
||||
currentPage is ReaderPage && page is ReaderPage -> {
|
||||
// if both pages have the same number, it's a split page with an InsertPage
|
||||
if (page.number == (currentPage as ReaderPage).number) {
|
||||
// the InsertPage is always the second in the reading direction
|
||||
page is InsertPage
|
||||
} else {
|
||||
page.number > (currentPage as ReaderPage).number
|
||||
}
|
||||
}
|
||||
currentPage is ChapterTransition.Prev && page is ReaderPage ->
|
||||
false
|
||||
else -> true
|
||||
}
|
||||
currentPage = page
|
||||
when (page) {
|
||||
is ReaderPage -> onReaderPageSelected(page, allowPreload, forward)
|
||||
is ChapterTransition -> onTransitionSelected(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAllowPreload(page: ReaderPage?): Boolean {
|
||||
// Page is transition page - preload allowed
|
||||
page ?: return true
|
||||
|
||||
// Initial opening - preload allowed
|
||||
currentPage ?: return true
|
||||
|
||||
// Allow preload for
|
||||
// 1. Going to next chapter from chapter transition
|
||||
// 2. Going between pages of same chapter
|
||||
// 3. Next chapter page
|
||||
return when (page.chapter) {
|
||||
(currentPage as? ChapterTransition.Next)?.to -> true
|
||||
(currentPage as? ReaderPage)?.chapter -> true
|
||||
items.nextTransition?.to -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a [ReaderPage] is marked as active. It notifies the
|
||||
* activity of the change and requests the preload of the next chapter if this is the last page.
|
||||
*/
|
||||
private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean, forward: Boolean) {
|
||||
val pages = page.chapter.pages ?: return
|
||||
logcat { "onReaderPageSelected: ${page.number}/${pages.size}" }
|
||||
activity.onPageSelected(page)
|
||||
|
||||
// Notify holder of page change
|
||||
onPageHolderSelected(page, forward)
|
||||
|
||||
// Skip preload on inserts it causes unwanted page jumping
|
||||
if (page is InsertPage) {
|
||||
return
|
||||
}
|
||||
|
||||
// Preload next chapter once we're within the last 5 pages of the current chapter
|
||||
val inPreloadRange = pages.size - page.number < 5
|
||||
if (inPreloadRange && allowPreload && page.chapter == items.currentChapter) {
|
||||
logcat { "Request preload next chapter because we're at page ${page.number} of ${pages.size}" }
|
||||
items.nextTransition?.to?.let(activity::requestPreloadChapter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a [ChapterTransition] is marked as active. It request the
|
||||
* preload of the destination chapter of the transition.
|
||||
*/
|
||||
private fun onTransitionSelected(transition: ChapterTransition) {
|
||||
logcat { "onTransitionSelected: $transition" }
|
||||
val toChapter = transition.to
|
||||
if (toChapter != null) {
|
||||
logcat { "Request preload destination chapter because we're on the transition" }
|
||||
activity.requestPreloadChapter(toChapter)
|
||||
} else if (transition is ChapterTransition.Next) {
|
||||
// No more chapters, show menu because the user is probably going to close the reader
|
||||
activity.showMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-111
@@ -3,25 +3,12 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderButton
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderTransitionView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.transitionDownloadState
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
/**
|
||||
* View of the ViewPager that contains a chapter transition.
|
||||
@@ -33,25 +20,12 @@ class PagerTransitionHolder(
|
||||
val transition: ChapterTransition,
|
||||
) : LinearLayout(readerThemedContext), ViewPagerAdapter.PositionableView {
|
||||
|
||||
private val scope = MainScope()
|
||||
private var stateJob: Job? = null
|
||||
|
||||
/**
|
||||
* Item that identifies this view. Needed by the adapter to not recreate views.
|
||||
*/
|
||||
override val item: Any
|
||||
get() = transition
|
||||
|
||||
/**
|
||||
* View container of the current status of the transition page. Child views will be added
|
||||
* dynamically.
|
||||
*/
|
||||
private var pagesContainer = LinearLayout(context).apply {
|
||||
layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
orientation = VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
@@ -60,92 +34,16 @@ class PagerTransitionHolder(
|
||||
|
||||
val transitionView = ReaderTransitionView(context)
|
||||
addView(transitionView)
|
||||
addView(pagesContainer)
|
||||
|
||||
transitionView.bind(
|
||||
transition,
|
||||
viewer.downloadManager,
|
||||
viewer.activity.viewModel.manga,
|
||||
viewer.activity.viewModel.source,
|
||||
transition = transition,
|
||||
downloadState = viewer.downloadManager.transitionDownloadState(
|
||||
transition,
|
||||
viewer.activity.viewModel.manga,
|
||||
viewer.activity.viewModel.source,
|
||||
),
|
||||
onRetry = viewer.activity::requestPreloadChapter,
|
||||
onButtonPressChange = { pressed -> viewer.pager.setGestureDetectorEnabled(!pressed) },
|
||||
)
|
||||
|
||||
transition.to?.let(::observeStatus)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this view is detached from the window. Unsubscribes any active subscription.
|
||||
*/
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
stateJob?.cancel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the status of the page list of the next/previous chapter. Whenever there's a new
|
||||
* state, the pages container is cleaned up before setting the new state.
|
||||
*/
|
||||
private fun observeStatus(chapter: ReaderChapter) {
|
||||
stateJob?.cancel()
|
||||
stateJob = scope.launch {
|
||||
chapter.stateFlow
|
||||
.collectLatest { state ->
|
||||
pagesContainer.removeAllViews()
|
||||
when (state) {
|
||||
is ReaderChapter.State.Loading -> setLoading()
|
||||
is ReaderChapter.State.Error -> setError(state.error)
|
||||
is ReaderChapter.State.Wait, is ReaderChapter.State.Loaded -> {
|
||||
// No additional view is added
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the loading state on the pages container.
|
||||
*/
|
||||
private fun setLoading() {
|
||||
val progress = CircularProgressIndicator(context)
|
||||
progress.isIndeterminate = true
|
||||
|
||||
val textView = AppCompatTextView(context).apply {
|
||||
wrapContent()
|
||||
text = context.stringResource(MR.strings.transition_pages_loading)
|
||||
}
|
||||
|
||||
pagesContainer.addView(progress)
|
||||
pagesContainer.addView(textView)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the error state on the pages container.
|
||||
*/
|
||||
private fun setError(error: Throwable) {
|
||||
val textView = AppCompatTextView(context).apply {
|
||||
wrapContent()
|
||||
text = context.stringResource(MR.strings.transition_pages_error, error.message ?: "")
|
||||
}
|
||||
|
||||
val retryBtn = ReaderButton(context).apply {
|
||||
viewer = this@PagerTransitionHolder.viewer
|
||||
wrapContent()
|
||||
text = context.stringResource(MR.strings.action_retry)
|
||||
setOnClickListener {
|
||||
val toChapter = transition.to
|
||||
if (toChapter != null) {
|
||||
this@PagerTransitionHolder.viewer.activity.requestPreloadChapter(toChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pagesContainer.addView(textView)
|
||||
pagesContainer.addView(retryBtn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension method to set layout params to wrap content on this view.
|
||||
*/
|
||||
private fun View.wrapContent() {
|
||||
layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,50 +82,27 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
||||
},
|
||||
)
|
||||
|
||||
private val pagerItems = PagerItems(isReversed = this is R2LPagerViewer)
|
||||
|
||||
/**
|
||||
* Adapter of the pager.
|
||||
*/
|
||||
private val adapter = PagerViewerAdapter(this)
|
||||
private val adapter = PagerViewerAdapter(this, pagerItems)
|
||||
|
||||
/**
|
||||
* Currently active item. It can be a chapter page or a chapter transition.
|
||||
*/
|
||||
private var currentPage: Any? = null
|
||||
|
||||
/**
|
||||
* Viewer chapters to set when the pager enters idle mode. Otherwise, if the view was settling
|
||||
* or dragging, there'd be a noticeable and annoying jump.
|
||||
*/
|
||||
private var awaitingIdleViewerChapters: ViewerChapters? = null
|
||||
|
||||
/**
|
||||
* Whether the view pager is currently in idle mode. It sets the awaiting chapters if setting
|
||||
* this field to true.
|
||||
*/
|
||||
private var isIdle = true
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
awaitingIdleViewerChapters?.let { viewerChapters ->
|
||||
setChaptersInternal(viewerChapters)
|
||||
awaitingIdleViewerChapters = null
|
||||
if (viewerChapters.currChapter.pages?.size == 1) {
|
||||
adapter.nextTransition?.to?.let(activity::requestPreloadChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val tracker = PagerPageTracker(activity, pagerItems, ::setChaptersInternal) { page, forward ->
|
||||
getPageHolder(page)?.onPageSelected(forward)
|
||||
}
|
||||
|
||||
private val pagerListener = object : ViewPager.SimpleOnPageChangeListener() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
if (!activity.isScrollingThroughPages) {
|
||||
activity.hideMenu()
|
||||
}
|
||||
onPageChange(position)
|
||||
tracker.onPageChange(position)
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
isIdle = state == ViewPager.SCROLL_STATE_IDLE
|
||||
tracker.isIdle = state == ViewPager.SCROLL_STATE_IDLE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,105 +194,12 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
||||
.filterIsInstance(PagerPageHolder::class.java)
|
||||
.firstOrNull { it.item == page }
|
||||
|
||||
/**
|
||||
* Called when a new page (either a [ReaderPage] or [ChapterTransition]) is marked as active
|
||||
*/
|
||||
private fun onPageChange(position: Int) {
|
||||
val page = adapter.items.getOrNull(position)
|
||||
if (page != null && currentPage != page) {
|
||||
val allowPreload = checkAllowPreload(page as? ReaderPage)
|
||||
val forward = when {
|
||||
currentPage is ReaderPage && page is ReaderPage -> {
|
||||
// if both pages have the same number, it's a split page with an InsertPage
|
||||
if (page.number == (currentPage as ReaderPage).number) {
|
||||
// the InsertPage is always the second in the reading direction
|
||||
page is InsertPage
|
||||
} else {
|
||||
page.number > (currentPage as ReaderPage).number
|
||||
}
|
||||
}
|
||||
currentPage is ChapterTransition.Prev && page is ReaderPage ->
|
||||
false
|
||||
else -> true
|
||||
}
|
||||
currentPage = page
|
||||
when (page) {
|
||||
is ReaderPage -> onReaderPageSelected(page, allowPreload, forward)
|
||||
is ChapterTransition -> onTransitionSelected(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAllowPreload(page: ReaderPage?): Boolean {
|
||||
// Page is transition page - preload allowed
|
||||
page ?: return true
|
||||
|
||||
// Initial opening - preload allowed
|
||||
currentPage ?: return true
|
||||
|
||||
// Allow preload for
|
||||
// 1. Going to next chapter from chapter transition
|
||||
// 2. Going between pages of same chapter
|
||||
// 3. Next chapter page
|
||||
return when (page.chapter) {
|
||||
(currentPage as? ChapterTransition.Next)?.to -> true
|
||||
(currentPage as? ReaderPage)?.chapter -> true
|
||||
adapter.nextTransition?.to -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a [ReaderPage] is marked as active. It notifies the
|
||||
* activity of the change and requests the preload of the next chapter if this is the last page.
|
||||
*/
|
||||
private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean, forward: Boolean) {
|
||||
val pages = page.chapter.pages ?: return
|
||||
logcat { "onReaderPageSelected: ${page.number}/${pages.size}" }
|
||||
activity.onPageSelected(page)
|
||||
|
||||
// Notify holder of page change
|
||||
getPageHolder(page)?.onPageSelected(forward)
|
||||
|
||||
// Skip preload on inserts it causes unwanted page jumping
|
||||
if (page is InsertPage) {
|
||||
return
|
||||
}
|
||||
|
||||
// Preload next chapter once we're within the last 5 pages of the current chapter
|
||||
val inPreloadRange = pages.size - page.number < 5
|
||||
if (inPreloadRange && allowPreload && page.chapter == adapter.currentChapter) {
|
||||
logcat { "Request preload next chapter because we're at page ${page.number} of ${pages.size}" }
|
||||
adapter.nextTransition?.to?.let(activity::requestPreloadChapter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a [ChapterTransition] is marked as active. It request the
|
||||
* preload of the destination chapter of the transition.
|
||||
*/
|
||||
private fun onTransitionSelected(transition: ChapterTransition) {
|
||||
logcat { "onTransitionSelected: $transition" }
|
||||
val toChapter = transition.to
|
||||
if (toChapter != null) {
|
||||
logcat { "Request preload destination chapter because we're on the transition" }
|
||||
activity.requestPreloadChapter(toChapter)
|
||||
} else if (transition is ChapterTransition.Next) {
|
||||
// No more chapters, show menu because the user is probably going to close the reader
|
||||
activity.showMenu()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells this viewer to set the given [chapters] as active. If the pager is currently idle,
|
||||
* it sets the chapters immediately, otherwise they are saved and set when it becomes idle.
|
||||
*/
|
||||
override fun setChapters(chapters: ViewerChapters) {
|
||||
if (isIdle) {
|
||||
setChaptersInternal(chapters)
|
||||
} else {
|
||||
awaitingIdleViewerChapters = chapters
|
||||
}
|
||||
tracker.setChapters(chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -339,7 +223,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
||||
|
||||
pager.addOnPageChangeListener(pagerListener)
|
||||
// Manually call onPageChange to update the UI
|
||||
onPageChange(pager.currentItem)
|
||||
tracker.onPageChange(pager.currentItem)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,7 +236,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
||||
pager.setCurrentItem(position, true)
|
||||
// manually call onPageChange since ViewPager listener is not triggered in this case
|
||||
if (currentPosition == position) {
|
||||
onPageChange(position)
|
||||
tracker.onPageChange(position)
|
||||
}
|
||||
} else {
|
||||
logcat { "Page $page not found in adapter" }
|
||||
@@ -373,7 +257,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
||||
*/
|
||||
private fun moveRight() {
|
||||
if (pager.currentItem != adapter.count - 1) {
|
||||
val holder = (currentPage as? ReaderPage)?.let(::getPageHolder)
|
||||
val holder = (tracker.currentPage as? ReaderPage)?.let(::getPageHolder)
|
||||
if (holder != null && paged.navigateToPan && holder.canPanRight()) {
|
||||
holder.panRight()
|
||||
} else {
|
||||
@@ -387,7 +271,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
|
||||
*/
|
||||
private fun moveLeft() {
|
||||
if (pager.currentItem != 0) {
|
||||
val holder = (currentPage as? ReaderPage)?.let(::getPageHolder)
|
||||
val holder = (tracker.currentPage as? ReaderPage)?.let(::getPageHolder)
|
||||
if (holder != null && paged.navigateToPan && holder.canPanLeft()) {
|
||||
holder.panLeft()
|
||||
} else {
|
||||
|
||||
+12
-124
@@ -4,10 +4,8 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.calculateChapterGap
|
||||
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
@@ -15,23 +13,16 @@ import tachiyomi.core.common.util.system.logcat
|
||||
/**
|
||||
* Pager adapter used by this [viewer] to where [ViewerChapters] updates are posted.
|
||||
*/
|
||||
class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
class PagerViewerAdapter(
|
||||
private val viewer: PagerViewer,
|
||||
private val pagerItems: PagerItems,
|
||||
) : ViewPagerAdapter() {
|
||||
|
||||
/**
|
||||
* List of currently set items.
|
||||
*/
|
||||
var items: MutableList<Any> = mutableListOf()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Holds preprocessed items so they don't get removed when changing chapter
|
||||
*/
|
||||
private var preprocessed: List<InsertPage> = emptyList()
|
||||
|
||||
var nextTransition: ChapterTransition.Next? = null
|
||||
private set
|
||||
|
||||
var currentChapter: ReaderChapter? = null
|
||||
val items: List<Any>
|
||||
get() = pagerItems.items
|
||||
|
||||
/**
|
||||
* Context that has been wrapped to use the correct theme values based on the
|
||||
@@ -45,85 +36,13 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
* has R2L direction.
|
||||
*/
|
||||
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) {
|
||||
val newItems = mutableListOf<Any>()
|
||||
|
||||
// Forces chapter transition if there is missing chapters
|
||||
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
|
||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||
|
||||
// Add previous chapter pages and transition
|
||||
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||
newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter))
|
||||
}
|
||||
|
||||
var insertPageLastPage: InsertPage? = null
|
||||
|
||||
// Add current chapter.
|
||||
val currPages = chapters.currChapter.pages
|
||||
if (currPages != null) {
|
||||
val pages = currPages.toMutableList()
|
||||
|
||||
val lastPage = pages.last()
|
||||
|
||||
// Insert preprocessed pages into current page list. Pages of the adjacent chapters are split too, and
|
||||
// placed by their index, so only this chapter's belong here.
|
||||
preprocessed.filter { it.chapter.chapter.id == chapters.currChapter.chapter.id }
|
||||
.sortedByDescending { it.index }
|
||||
.forEach { page ->
|
||||
if (lastPage.index == page.index) {
|
||||
insertPageLastPage = page
|
||||
}
|
||||
pages.add(page.index + 1, page)
|
||||
}
|
||||
|
||||
newItems.addAll(pages)
|
||||
}
|
||||
|
||||
currentChapter = chapters.currChapter
|
||||
|
||||
// Add next chapter transition and pages.
|
||||
nextTransition = ChapterTransition.Next(chapters.currChapter, chapters.nextChapter)
|
||||
.also {
|
||||
if (
|
||||
nextHasMissingChapters ||
|
||||
forceTransition ||
|
||||
chapters.nextChapter?.state !is ReaderChapter.State.Loaded
|
||||
) {
|
||||
newItems.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
val currentItem = items.getOrNull(viewer.pager.currentItem)
|
||||
|
||||
// Resets double-page splits, else insert pages get misplaced
|
||||
items.filterIsInstance<InsertPage>().also { items.removeAll(it) }
|
||||
|
||||
if (viewer is R2LPagerViewer) {
|
||||
newItems.reverse()
|
||||
}
|
||||
|
||||
val adjacentChapterIds = setOfNotNull(chapters.prevChapter?.chapter?.id, chapters.nextChapter?.chapter?.id)
|
||||
preprocessed = preprocessed.filter { it.chapter.chapter.id in adjacentChapterIds }
|
||||
items = newItems
|
||||
val update = pagerItems.setChapters(chapters, forceTransition, viewer.pager.currentItem)
|
||||
notifyDataSetChanged()
|
||||
|
||||
// The pager only follows the pages it has created, so before its first layout it would stay at the same
|
||||
// position while pages are added in front, e.g. the next chapter's in right to left, and land in another
|
||||
// chapter
|
||||
val currentPosition = items.indexOf(currentItem)
|
||||
if (currentPosition != -1 && currentPosition != viewer.pager.currentItem) {
|
||||
viewer.pager.setCurrentItem(currentPosition, false)
|
||||
}
|
||||
update.position?.let { viewer.pager.setCurrentItem(it, false) }
|
||||
|
||||
// Will skip insert page otherwise
|
||||
if (insertPageLastPage != null) {
|
||||
viewer.moveToPage(insertPageLastPage)
|
||||
}
|
||||
update.insertPage?.let(viewer::moveToPage)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,44 +79,13 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
||||
}
|
||||
|
||||
fun onPageSplit(currentPage: Any?, newPage: InsertPage) {
|
||||
if (currentPage !is ReaderPage) return
|
||||
|
||||
val currentIndex = items.indexOf(currentPage)
|
||||
|
||||
// Put aside pages of an adjacent chapter until it becomes the current one, so they are not lost on the change
|
||||
if (currentPage.chapter.chapter.id != currentChapter?.chapter?.id) {
|
||||
val chapterId = newPage.chapter.chapter.id
|
||||
if (preprocessed.none { it.chapter.chapter.id == chapterId && it.index == newPage.index }) {
|
||||
preprocessed = preprocessed + newPage
|
||||
}
|
||||
return
|
||||
if (pagerItems.onPageSplit(currentPage, newPage)) {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
val placeAtIndex = when (viewer) {
|
||||
is L2RPagerViewer,
|
||||
is VerticalPagerViewer,
|
||||
-> currentIndex + 1
|
||||
else -> currentIndex
|
||||
}
|
||||
|
||||
// It will enter a endless cycle of insert pages
|
||||
if (viewer is R2LPagerViewer && placeAtIndex - 1 >= 0 && items[placeAtIndex - 1] is InsertPage) {
|
||||
return
|
||||
}
|
||||
|
||||
// Same here it will enter a endless cycle of insert pages
|
||||
if (items[placeAtIndex] is InsertPage) {
|
||||
return
|
||||
}
|
||||
|
||||
items.add(placeAtIndex, newPage)
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun cleanupPageSplit() {
|
||||
val insertPages = items.filterIsInstance<InsertPage>()
|
||||
items.removeAll(insertPages)
|
||||
pagerItems.cleanupPageSplit()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.DualPageSettings
|
||||
|
||||
/**
|
||||
* Settings only the pager viewers use, all of them part of what the pages are made with.
|
||||
*/
|
||||
data class SimplePagedSettings(
|
||||
val dualPageSplit: Boolean,
|
||||
val dualPageInvert: Boolean,
|
||||
val dualPageRotateToFit: Boolean,
|
||||
val dualPageRotateToFitInvert: Boolean,
|
||||
)
|
||||
override val dualPageSplit: Boolean,
|
||||
override val dualPageInvert: Boolean,
|
||||
override val dualPageRotateToFit: Boolean,
|
||||
override val dualPageRotateToFitInvert: Boolean,
|
||||
) : DualPageSettings
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -22,24 +19,21 @@ import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastCoerceIn
|
||||
import ca.mpreg.webgpuviewer.viewer.ComposePageSize
|
||||
import ca.mpreg.webgpuviewer.viewer.ImagePage
|
||||
import eu.kanade.presentation.reader.ChapterTransition
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.presentation.reader.ReaderError
|
||||
import eu.kanade.presentation.util.formattedMessage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderThemedContent
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.CombinedCircularProgressIndicator
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.source.local.isLocal
|
||||
|
||||
/** The side padding `PagerTransitionHolder` gave the same transition view. */
|
||||
@@ -68,21 +62,10 @@ abstract class ReaderComposePage(
|
||||
|
||||
/**
|
||||
* The theme goes on here because the library's page knows nothing of the app's colours.
|
||||
*
|
||||
* Under the reader's own night mode rather than the app's: the page sits on the reader
|
||||
* background, so a light app over a black reader would otherwise draw dark text on black.
|
||||
* [TachiyomiTheme] reads that from the configuration, which the pager and webtoon adapters
|
||||
* used to get by inflating their views from a reader-themed context.
|
||||
*/
|
||||
@Composable
|
||||
final override fun BoxScope.Content() {
|
||||
val context = viewer.readerThemedContext
|
||||
CompositionLocalProvider(
|
||||
LocalContext provides context,
|
||||
LocalConfiguration provides context.resources.configuration,
|
||||
) {
|
||||
TachiyomiTheme { PageContent() }
|
||||
}
|
||||
ReaderThemedContent(viewer.readerThemedContext) { PageContent() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -128,36 +111,6 @@ class ErrorPage internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/** `reader_error.xml`, in Compose. */
|
||||
@Composable
|
||||
private fun ReaderError(
|
||||
message: String?,
|
||||
onRetry: () -> Unit,
|
||||
onOpenInWebView: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = message ?: stringResource(MR.strings.decode_image_error),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Button(onClick = onRetry) {
|
||||
Text(stringResource(MR.strings.action_retry))
|
||||
}
|
||||
if (onOpenInWebView != null) {
|
||||
Button(onClick = onOpenInWebView) {
|
||||
Text(stringResource(MR.strings.action_open_in_web_view))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A page still downloading: the reader's progress ring. */
|
||||
class ProgressPage(viewer: WebGpuViewer) : ReaderComposePage(viewer) {
|
||||
|
||||
@@ -228,7 +181,7 @@ class TransitionPage(
|
||||
} else {
|
||||
Modifier.padding(horizontal = TRANSITION_SIDE_PADDING.dp)
|
||||
}
|
||||
Box(modifier = padding) {
|
||||
Box(modifier = padding.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
|
||||
ChapterTransition(
|
||||
transition = transition,
|
||||
currChapterDownloaded = transition.from.pageLoader?.isLocal == true,
|
||||
|
||||
+6
-5
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.DualPageSettings
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.NavigationSettings
|
||||
|
||||
/**
|
||||
@@ -16,9 +17,9 @@ data class SimpleLongStripSettings(
|
||||
*/
|
||||
data class Page(
|
||||
val sidePadding: Int,
|
||||
val dualPageSplit: Boolean,
|
||||
val dualPageInvert: Boolean,
|
||||
val dualPageRotateToFit: Boolean,
|
||||
val dualPageRotateToFitInvert: Boolean,
|
||||
)
|
||||
override val dualPageSplit: Boolean,
|
||||
override val dualPageInvert: Boolean,
|
||||
override val dualPageRotateToFit: Boolean,
|
||||
override val dualPageRotateToFitInvert: Boolean,
|
||||
) : DualPageSettings
|
||||
}
|
||||
|
||||
@@ -5,25 +5,24 @@ import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.calculateChapterGap
|
||||
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
||||
|
||||
/**
|
||||
* RecyclerView Adapter used by this [viewer] to where [ViewerChapters] updates are posted.
|
||||
*/
|
||||
class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
class WebtoonAdapter(
|
||||
val viewer: WebtoonViewer,
|
||||
private val webtoonItems: WebtoonItems,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
/**
|
||||
* List of currently set items.
|
||||
*/
|
||||
var items: List<Any> = emptyList()
|
||||
private set
|
||||
|
||||
var currentChapter: ReaderChapter? = null
|
||||
val items: List<Any>
|
||||
get() = webtoonItems.items
|
||||
|
||||
/**
|
||||
* Context that has been wrapped to use the correct theme values based on the
|
||||
@@ -36,42 +35,9 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
|
||||
* next/previous chapter to allow seamless transitions.
|
||||
*/
|
||||
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) {
|
||||
val newItems = mutableListOf<Any>()
|
||||
|
||||
// Forces chapter transition if there is missing chapters
|
||||
val prevHasMissingChapters = calculateChapterGap(chapters.currChapter, chapters.prevChapter) > 0
|
||||
val nextHasMissingChapters = calculateChapterGap(chapters.nextChapter, chapters.currChapter) > 0
|
||||
|
||||
// Add previous chapter pages and transition.
|
||||
chapters.prevChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||
if (prevHasMissingChapters || forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||
newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter))
|
||||
}
|
||||
|
||||
// Add current chapter.
|
||||
val currPages = chapters.currChapter.pages
|
||||
if (currPages != null) {
|
||||
newItems.addAll(currPages)
|
||||
}
|
||||
|
||||
currentChapter = chapters.currChapter
|
||||
|
||||
// Add next chapter transition and pages.
|
||||
if (nextHasMissingChapters || forceTransition || chapters.nextChapter?.state !is ReaderChapter.State.Loaded) {
|
||||
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
|
||||
}
|
||||
|
||||
chapters.nextChapter?.pages?.let(newItems::addAll)
|
||||
|
||||
updateItems(newItems)
|
||||
}
|
||||
|
||||
private fun updateItems(newItems: List<Any>) {
|
||||
val result = DiffUtil.calculateDiff(Callback(items, newItems))
|
||||
items = newItems
|
||||
result.dispatchUpdatesTo(this)
|
||||
val oldItems = items
|
||||
webtoonItems.setChapters(chapters, forceTransition)
|
||||
DiffUtil.calculateDiff(Callback(oldItems, items)).dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.viewerItems
|
||||
|
||||
/**
|
||||
* Items of a webtoon viewer, where [ViewerChapters] updates are posted.
|
||||
*/
|
||||
class WebtoonItems {
|
||||
|
||||
/**
|
||||
* List of currently set items.
|
||||
*/
|
||||
var items: List<Any> by mutableStateOf(emptyList())
|
||||
private set
|
||||
|
||||
var currentChapter: ReaderChapter? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Updates the items with the given [chapters]. It handles setting a few pages of the
|
||||
* next/previous chapter to allow seamless transitions.
|
||||
*/
|
||||
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) {
|
||||
currentChapter = chapters.currChapter
|
||||
items = viewerItems(chapters, forceTransition)
|
||||
}
|
||||
}
|
||||
+12
-66
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
@@ -9,13 +8,12 @@ import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updateMargins
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import eu.kanade.presentation.util.formattedMessage
|
||||
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageErrorView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.processStripImage
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -26,11 +24,8 @@ import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import logcat.LogPriority
|
||||
import okio.Buffer
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
/**
|
||||
* Holder of the webtoon reader for a single page of a chapter.
|
||||
@@ -58,7 +53,7 @@ class WebtoonPageHolder(
|
||||
/**
|
||||
* Error layout to show when the image fails to load.
|
||||
*/
|
||||
private var errorLayout: ReaderErrorBinding? = null
|
||||
private var errorLayout: ReaderPageErrorView? = null
|
||||
|
||||
/**
|
||||
* Getter to retrieve the height of the recycler view.
|
||||
@@ -191,7 +186,7 @@ class WebtoonPageHolder(
|
||||
val buffer = withContext(Dispatchers.IO) { streamFn().use { Buffer().readFrom(it) } }
|
||||
// Splitting and merging decode the image, which is CPU work
|
||||
val (source, isAnimated) = withContext(Dispatchers.Default) {
|
||||
val source = process(buffer)
|
||||
val source = viewer.simpleLongStrip.page.processStripImage(buffer)
|
||||
val isAnimated = ImageUtil.isAnimatedAndSupported(source)
|
||||
Pair(source, isAnimated)
|
||||
}
|
||||
@@ -215,36 +210,6 @@ class WebtoonPageHolder(
|
||||
}
|
||||
}
|
||||
|
||||
private fun process(imageSource: BufferedSource): BufferedSource {
|
||||
if (viewer.simpleLongStrip.page.dualPageRotateToFit) {
|
||||
return rotateDualPage(imageSource)
|
||||
}
|
||||
|
||||
if (viewer.simpleLongStrip.page.dualPageSplit) {
|
||||
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||
if (isDoublePage) {
|
||||
val upperSide = if (viewer.simpleLongStrip.page.dualPageInvert) {
|
||||
ImageUtil.Side.LEFT
|
||||
} else {
|
||||
ImageUtil.Side.RIGHT
|
||||
}
|
||||
return ImageUtil.splitAndMerge(imageSource, upperSide)
|
||||
}
|
||||
}
|
||||
|
||||
return imageSource
|
||||
}
|
||||
|
||||
private fun rotateDualPage(imageSource: BufferedSource): BufferedSource {
|
||||
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||
return if (isDoublePage) {
|
||||
val rotation = if (viewer.simpleLongStrip.page.dualPageRotateToFitInvert) -90f else 90f
|
||||
ImageUtil.rotateImage(imageSource, rotation)
|
||||
} else {
|
||||
imageSource
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the page has an error.
|
||||
*/
|
||||
@@ -278,34 +243,15 @@ class WebtoonPageHolder(
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a button to retry pages.
|
||||
* Shows the error of the page.
|
||||
*/
|
||||
private fun initErrorLayout(error: Throwable?): ReaderErrorBinding {
|
||||
if (errorLayout == null) {
|
||||
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true)
|
||||
errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt())
|
||||
errorLayout?.actionRetry?.setOnClickListener {
|
||||
page?.let { it.chapter.pageLoader?.retryPage(it) }
|
||||
}
|
||||
private fun initErrorLayout(error: Throwable?) {
|
||||
val page = page ?: return
|
||||
val errorLayout = errorLayout ?: ReaderPageErrorView(context).also {
|
||||
frame.addView(it, FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt()))
|
||||
errorLayout = it
|
||||
}
|
||||
|
||||
val imageUrl = page?.imageUrl
|
||||
errorLayout?.actionOpenInWebView?.isVisible = imageUrl != null
|
||||
if (imageUrl != null) {
|
||||
if (imageUrl.startsWith("http", true)) {
|
||||
errorLayout?.actionOpenInWebView?.setOnClickListener {
|
||||
val sourceId = viewer.activity.viewModel.manga?.source
|
||||
|
||||
val intent = WebViewActivity.newIntent(context, imageUrl, sourceId)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorLayout?.errorMessage?.text = with(context) { error?.formattedMessage }
|
||||
?: context.stringResource(MR.strings.decode_image_error)
|
||||
|
||||
return errorLayout!!
|
||||
errorLayout.bind(page = page, error = error, sourceId = viewer.activity.viewModel.manga?.source)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,7 +259,7 @@ class WebtoonPageHolder(
|
||||
*/
|
||||
private fun removeErrorLayout() {
|
||||
errorLayout?.let {
|
||||
frame.removeView(it.root)
|
||||
frame.removeView(it)
|
||||
errorLayout = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* The item a webtoon viewer of [items] is on, shared by the view and Compose viewers. It tells [activity] of each new
|
||||
* page and preloads the chapters around it. A scroll further than [hideThreshold] hides the menu.
|
||||
*/
|
||||
class WebtoonPageTracker(
|
||||
private val activity: ReaderActivity,
|
||||
private val items: WebtoonItems,
|
||||
private val hideThreshold: Int,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Currently active item. It can be a chapter page or a chapter transition.
|
||||
*/
|
||||
var currentPage: Any? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Marks the item at [position], the last one whose end is visible, as active.
|
||||
*/
|
||||
fun onScrolled(position: Int) {
|
||||
val item = items.items.getOrNull(position)
|
||||
val allowPreload = checkAllowPreload(item as? ReaderPage)
|
||||
if (item != null && currentPage != item) {
|
||||
currentPage = item
|
||||
when (item) {
|
||||
is ReaderPage -> onPageSelected(item, allowPreload)
|
||||
is ChapterTransition -> onTransitionSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the list scrolled by [dy] pixels, leaving the items from [firstVisible] to [lastEndVisible] on
|
||||
* screen.
|
||||
*/
|
||||
fun onScrolledBy(dy: Float, firstVisible: Int, lastEndVisible: Int) {
|
||||
if (abs(dy) > hideThreshold && activity.viewModel.state.value.menuVisible) {
|
||||
activity.hideMenu()
|
||||
}
|
||||
|
||||
if (dy < 0) {
|
||||
val firstItem = items.items.getOrNull(firstVisible)
|
||||
if (firstItem is ChapterTransition.Prev && firstItem.to != null) {
|
||||
activity.requestPreloadChapter(firstItem.to)
|
||||
}
|
||||
}
|
||||
|
||||
val lastItem = items.items.getOrNull(lastEndVisible)
|
||||
if (lastItem is ChapterTransition.Next && lastItem.to == null) {
|
||||
activity.showMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAllowPreload(page: ReaderPage?): Boolean {
|
||||
// Page is transition page - preload allowed
|
||||
page ?: return true
|
||||
|
||||
// Initial opening - preload allowed
|
||||
currentPage ?: return true
|
||||
|
||||
// Allow preload for
|
||||
// 1. Going between pages of same chapter
|
||||
// 2. Next chapter page
|
||||
return when (page.chapter) {
|
||||
(currentPage as? ReaderPage)?.chapter -> true
|
||||
lastChapter() -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a [page] is marked as active. It notifies the activity of the change and requests the preload of
|
||||
* the next chapter if this is the last page.
|
||||
*/
|
||||
private fun onPageSelected(page: ReaderPage, allowPreload: Boolean) {
|
||||
val pages = page.chapter.pages ?: return
|
||||
logcat { "onPageSelected: ${page.number}/${pages.size}" }
|
||||
activity.onPageSelected(page)
|
||||
|
||||
// Preload next chapter once we're within the last 5 pages of the current chapter
|
||||
val inPreloadRange = pages.size - page.number < 5
|
||||
if (inPreloadRange && allowPreload && page.chapter == items.currentChapter) {
|
||||
logcat { "Request preload next chapter because we're at page ${page.number} of ${pages.size}" }
|
||||
val transitionChapter = lastChapter()
|
||||
if (transitionChapter != null) {
|
||||
logcat { "Requesting to preload chapter ${transitionChapter.chapter.chapterNumber}" }
|
||||
activity.requestPreloadChapter(transitionChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a [transition] is marked as active. It request the preload of the destination chapter of the
|
||||
* transition.
|
||||
*/
|
||||
private fun onTransitionSelected(transition: ChapterTransition) {
|
||||
logcat { "onTransitionSelected: $transition" }
|
||||
val toChapter = transition.to
|
||||
if (toChapter != null) {
|
||||
logcat { "Request preload destination chapter because we're on the transition" }
|
||||
activity.requestPreloadChapter(toChapter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The chapter at the end of the list: the next transition's, or that of the next chapter's pages.
|
||||
*/
|
||||
private fun lastChapter() = when (val lastItem = items.items.lastOrNull()) {
|
||||
is ChapterTransition.Next -> lastItem.to
|
||||
is ReaderPage -> lastItem.chapter
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
+12
-106
@@ -4,21 +4,12 @@ import android.view.Gravity
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.widget.AppCompatButton
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.view.isNotEmpty
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderTransitionView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.transitionDownloadState
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
/**
|
||||
* Holder of the webtoon viewer that contains a chapter transition.
|
||||
@@ -28,20 +19,8 @@ class WebtoonTransitionHolder(
|
||||
viewer: WebtoonViewer,
|
||||
) : WebtoonBaseHolder(layout, viewer) {
|
||||
|
||||
private val scope = MainScope()
|
||||
private var stateJob: Job? = null
|
||||
|
||||
private val transitionView = ReaderTransitionView(context)
|
||||
|
||||
/**
|
||||
* View container of the current status of the transition page. Child views will be added
|
||||
* dynamically.
|
||||
*/
|
||||
private var pagesContainer = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
|
||||
init {
|
||||
layout.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
layout.orientation = LinearLayout.VERTICAL
|
||||
@@ -51,95 +30,22 @@ class WebtoonTransitionHolder(
|
||||
val paddingHorizontal = 32.dpToPx
|
||||
layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
|
||||
|
||||
val childMargins = 16.dpToPx
|
||||
val childParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
|
||||
setMargins(0, childMargins, 0, childMargins)
|
||||
}
|
||||
|
||||
layout.addView(transitionView)
|
||||
layout.addView(pagesContainer, childParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the given [transition] with this view holder, subscribing to its state.
|
||||
* Binds the given [transition] with this view holder.
|
||||
*/
|
||||
fun bind(transition: ChapterTransition) {
|
||||
transitionView.bind(
|
||||
transition,
|
||||
viewer.downloadManager,
|
||||
viewer.activity.viewModel.manga,
|
||||
viewer.activity.viewModel.source,
|
||||
transition = transition,
|
||||
downloadState = viewer.downloadManager.transitionDownloadState(
|
||||
transition,
|
||||
viewer.activity.viewModel.manga,
|
||||
viewer.activity.viewModel.source,
|
||||
),
|
||||
onRetry = viewer.activity::requestPreloadChapter,
|
||||
statusPadding = PaddingValues(vertical = 16.dp),
|
||||
)
|
||||
|
||||
transition.to?.let { observeStatus(it, transition) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is recycled and being added to the view pool.
|
||||
*/
|
||||
override fun recycle() {
|
||||
stateJob?.cancel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the status of the page list of the next/previous chapter. Whenever there's a new
|
||||
* state, the pages container is cleaned up before setting the new state.
|
||||
*/
|
||||
private fun observeStatus(chapter: ReaderChapter, transition: ChapterTransition) {
|
||||
stateJob?.cancel()
|
||||
stateJob = scope.launch {
|
||||
chapter.stateFlow
|
||||
.collectLatest { state ->
|
||||
pagesContainer.removeAllViews()
|
||||
when (state) {
|
||||
is ReaderChapter.State.Loading -> setLoading()
|
||||
is ReaderChapter.State.Error -> setError(state.error, transition)
|
||||
is ReaderChapter.State.Wait, is ReaderChapter.State.Loaded -> {
|
||||
// No additional view is added
|
||||
}
|
||||
}
|
||||
pagesContainer.isVisible = pagesContainer.isNotEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the loading state on the pages container.
|
||||
*/
|
||||
private fun setLoading() {
|
||||
val progress = CircularProgressIndicator(context)
|
||||
progress.isIndeterminate = true
|
||||
|
||||
val textView = AppCompatTextView(context).apply {
|
||||
wrapContent()
|
||||
text = context.stringResource(MR.strings.transition_pages_loading)
|
||||
}
|
||||
|
||||
pagesContainer.addView(progress)
|
||||
pagesContainer.addView(textView)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the error state on the pages container.
|
||||
*/
|
||||
private fun setError(error: Throwable, transition: ChapterTransition) {
|
||||
val textView = AppCompatTextView(context).apply {
|
||||
wrapContent()
|
||||
text = context.stringResource(MR.strings.transition_pages_error, error.message ?: "")
|
||||
}
|
||||
|
||||
val retryBtn = AppCompatButton(context).apply {
|
||||
wrapContent()
|
||||
text = context.stringResource(MR.strings.action_retry)
|
||||
setOnClickListener {
|
||||
val toChapter = transition.to
|
||||
if (toChapter != null) {
|
||||
viewer.activity.requestPreloadChapter(toChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pagesContainer.addView(textView)
|
||||
pagesContainer.addView(retryBtn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,17 +85,18 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
val general: GeneralSettings
|
||||
get() = settingsViewModel.general.value
|
||||
|
||||
private val webtoonItems = WebtoonItems()
|
||||
|
||||
/**
|
||||
* Adapter of the recycler view.
|
||||
*/
|
||||
private val adapter = WebtoonAdapter(this)
|
||||
private val adapter = WebtoonAdapter(this, webtoonItems)
|
||||
|
||||
/**
|
||||
* Currently active item. It can be a chapter page or a chapter transition.
|
||||
*/
|
||||
private var currentPage: Any? = null
|
||||
|
||||
private val threshold: Int by lazy { readerPreferences.readerHideThreshold.get().threshold }
|
||||
private val tracker = WebtoonPageTracker(
|
||||
activity,
|
||||
webtoonItems,
|
||||
readerPreferences.readerHideThreshold.get().threshold,
|
||||
)
|
||||
|
||||
init {
|
||||
recycler.sidePaddingPercent = simpleLongStrip.page.sidePadding
|
||||
@@ -110,24 +111,11 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
onScrolled()
|
||||
|
||||
if ((dy > threshold || dy < -threshold) && activity.viewModel.state.value.menuVisible) {
|
||||
activity.hideMenu()
|
||||
}
|
||||
|
||||
if (dy < 0) {
|
||||
val firstIndex = layoutManager.findFirstVisibleItemPosition()
|
||||
val firstItem = adapter.items.getOrNull(firstIndex)
|
||||
if (firstItem is ChapterTransition.Prev && firstItem.to != null) {
|
||||
activity.requestPreloadChapter(firstItem.to)
|
||||
}
|
||||
}
|
||||
|
||||
val lastIndex = layoutManager.findLastEndVisibleItemPosition()
|
||||
val lastItem = adapter.items.getOrNull(lastIndex)
|
||||
if (lastItem is ChapterTransition.Next && lastItem.to == null) {
|
||||
activity.showMenu()
|
||||
}
|
||||
tracker.onScrolledBy(
|
||||
dy = dy.toFloat(),
|
||||
firstVisible = layoutManager.findFirstVisibleItemPosition(),
|
||||
lastEndVisible = layoutManager.findLastEndVisibleItemPosition(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -185,26 +173,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
frame.addView(recycler)
|
||||
}
|
||||
|
||||
private fun checkAllowPreload(page: ReaderPage?): Boolean {
|
||||
// Page is transition page - preload allowed
|
||||
page ?: return true
|
||||
|
||||
// Initial opening - preload allowed
|
||||
currentPage ?: return true
|
||||
|
||||
val nextItem = adapter.items.getOrNull(adapter.items.size - 1)
|
||||
val nextChapter = (nextItem as? ChapterTransition.Next)?.to ?: (nextItem as? ReaderPage)?.chapter
|
||||
|
||||
// Allow preload for
|
||||
// 1. Going between pages of same chapter
|
||||
// 2. Next chapter page
|
||||
return when (page.chapter) {
|
||||
(currentPage as? ReaderPage)?.chapter -> true
|
||||
nextChapter -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/** The page images' colours, for pages bound later - see [LocalPageColorFilter]. */
|
||||
var pageColorFilter: ColorFilter? = null
|
||||
private set
|
||||
@@ -235,46 +203,11 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the RecyclerView listener when a [page] is marked as active. It notifies the
|
||||
* activity of the change and requests the preload of the next chapter if this is the last page.
|
||||
*/
|
||||
private fun onPageSelected(page: ReaderPage, allowPreload: Boolean) {
|
||||
val pages = page.chapter.pages ?: return
|
||||
logcat { "onPageSelected: ${page.number}/${pages.size}" }
|
||||
activity.onPageSelected(page)
|
||||
|
||||
// Preload next chapter once we're within the last 5 pages of the current chapter
|
||||
val inPreloadRange = pages.size - page.number < 5
|
||||
if (inPreloadRange && allowPreload && page.chapter == adapter.currentChapter) {
|
||||
logcat { "Request preload next chapter because we're at page ${page.number} of ${pages.size}" }
|
||||
val nextItem = adapter.items.getOrNull(adapter.items.size - 1)
|
||||
val transitionChapter = (nextItem as? ChapterTransition.Next)?.to ?: (nextItem as?ReaderPage)?.chapter
|
||||
if (transitionChapter != null) {
|
||||
logcat { "Requesting to preload chapter ${transitionChapter.chapter.chapterNumber}" }
|
||||
activity.requestPreloadChapter(transitionChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the RecyclerView listener when a [transition] is marked as active. It request the
|
||||
* preload of the destination chapter of the transition.
|
||||
*/
|
||||
private fun onTransitionSelected(transition: ChapterTransition) {
|
||||
logcat { "onTransitionSelected: $transition" }
|
||||
val toChapter = transition.to
|
||||
if (toChapter != null) {
|
||||
logcat { "Request preload destination chapter because we're on the transition" }
|
||||
activity.requestPreloadChapter(toChapter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells this viewer to set the given [chapters] as active.
|
||||
*/
|
||||
override fun setChapters(chapters: ViewerChapters) {
|
||||
val forceTransition = general.alwaysShowChapterTransition || currentPage is ChapterTransition
|
||||
val forceTransition = general.alwaysShowChapterTransition || tracker.currentPage is ChapterTransition
|
||||
adapter.setChapters(chapters, forceTransition)
|
||||
|
||||
if (recycler.isGone) {
|
||||
@@ -300,17 +233,8 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
}
|
||||
}
|
||||
|
||||
fun onScrolled(pos: Int? = null) {
|
||||
val position = pos ?: layoutManager.findLastEndVisibleItemPosition()
|
||||
val item = adapter.items.getOrNull(position)
|
||||
val allowPreload = checkAllowPreload(item as? ReaderPage)
|
||||
if (item != null && currentPage != item) {
|
||||
currentPage = item
|
||||
when (item) {
|
||||
is ReaderPage -> onPageSelected(item, allowPreload)
|
||||
is ChapterTransition -> onTransitionSelected(item)
|
||||
}
|
||||
}
|
||||
private fun onScrolled(pos: Int? = null) {
|
||||
tracker.onScrolled(pos ?: layoutManager.findLastEndVisibleItemPosition())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/decode_image_error"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium" />
|
||||
|
||||
<eu.kanade.tachiyomi.ui.reader.viewer.ReaderButton
|
||||
android:id="@+id/action_retry"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/action_retry" />
|
||||
|
||||
<eu.kanade.tachiyomi.ui.reader.viewer.ReaderButton
|
||||
android:id="@+id/action_open_in_web_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/action_open_in_web_view"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
Reference in New Issue
Block a user