Replace PageLoader.getPage() with PageLoader.loadPage() (#8976)

* Follow page status via StateFlow

Keep getPage subscription since it's needed to load the pages

* Replace PageLoader.getPage with PageLoader.loadPage
This commit is contained in:
Two-Ai 2023-01-23 17:10:44 -05:00 committed by GitHub
parent 1a319601de
commit 2ef1f07aae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 83 additions and 92 deletions

View file

@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import rx.Observable
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -30,9 +29,7 @@ class DirectoryPageLoader(val file: File) : PageLoader() {
} }
/** /**
* Returns an observable that emits a ready state. * No additional action required to load the page
*/ */
override fun getPage(page: ReaderPage): Observable<Page.State> { override suspend fun loadPage(page: ReaderPage) {}
return Observable.just(Page.State.READY)
}
} }

View file

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import rx.Observable
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
@ -65,7 +64,7 @@ class DownloadPageLoader(
} }
} }
override fun getPage(page: ReaderPage): Observable<Page.State> { override suspend fun loadPage(page: ReaderPage) {
return zipPageLoader?.getPage(page) ?: Observable.just(Page.State.READY) zipPageLoader?.loadPage(page)
} }
} }

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import rx.Observable
import java.io.File import java.io.File
/** /**
@ -39,15 +38,9 @@ class EpubPageLoader(file: File) : PageLoader() {
} }
/** /**
* Returns an observable that emits a ready state unless the loader was recycled. * No additional action required to load the page
*/ */
override fun getPage(page: ReaderPage): Observable<Page.State> { override suspend fun loadPage(page: ReaderPage) {
return Observable.just( check(!isRecycled)
if (isRecycled) {
Page.State.ERROR
} else {
Page.State.READY
},
)
} }
} }

View file

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -16,10 +17,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import rx.Observable import kotlinx.coroutines.suspendCancellableCoroutine
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
import rx.subjects.SerializedSubject
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.PriorityBlockingQueue import java.util.concurrent.PriorityBlockingQueue
@ -53,7 +51,7 @@ class HttpPageLoader(
} }
.filter { it.status == Page.State.QUEUE } .filter { it.status == Page.State.QUEUE }
.collect { .collect {
loadPage(it) _loadPage(it)
} }
} }
} }
@ -103,11 +101,10 @@ class HttpPageLoader(
} }
/** /**
* Returns an observable that loads a page through the queue and listens to its result to * Loads a page through the queue. Handles re-enqueueing pages if they were evicted from the cache.
* emit new states. It handles re-enqueueing pages if they were evicted from the cache.
*/ */
override fun getPage(page: ReaderPage): Observable<Page.State> { override suspend fun loadPage(page: ReaderPage) {
return Observable.defer { withIOContext {
val imageUrl = page.imageUrl val imageUrl = page.imageUrl
// Check if the image has been deleted // Check if the image has been deleted
@ -120,26 +117,22 @@ class HttpPageLoader(
page.status = Page.State.QUEUE page.status = Page.State.QUEUE
} }
val statusSubject = SerializedSubject(PublishSubject.create<Page.State>())
page.statusSubject = statusSubject
val queuedPages = mutableListOf<PriorityPage>() val queuedPages = mutableListOf<PriorityPage>()
if (page.status == Page.State.QUEUE) { if (page.status == Page.State.QUEUE) {
queuedPages += PriorityPage(page, 1).also { queue.offer(it) } queuedPages += PriorityPage(page, 1).also { queue.offer(it) }
} }
queuedPages += preloadNextPages(page, preloadSize) queuedPages += preloadNextPages(page, preloadSize)
statusSubject.startWith(page.status) suspendCancellableCoroutine<Nothing> { continuation ->
.doOnUnsubscribe { continuation.invokeOnCancellation {
queuedPages.forEach { queuedPages.forEach {
if (it.page.status == Page.State.QUEUE) { if (it.page.status == Page.State.QUEUE) {
queue.remove(it) queue.remove(it)
} }
} }
} }
}
} }
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
} }
/** /**
@ -197,7 +190,7 @@ class HttpPageLoader(
* *
* @param page the page whose source image has to be downloaded. * @param page the page whose source image has to be downloaded.
*/ */
private suspend fun loadPage(page: ReaderPage) { private suspend fun _loadPage(page: ReaderPage) {
try { try {
if (page.imageUrl.isNullOrEmpty()) { if (page.imageUrl.isNullOrEmpty()) {
page.status = Page.State.LOAD_PAGE page.status = Page.State.LOAD_PAGE

View file

@ -1,9 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import rx.Observable
/** /**
* A loader used to load pages into the reader. Any open resources must be cleaned up when the * A loader used to load pages into the reader. Any open resources must be cleaned up when the
@ -32,9 +30,11 @@ abstract class PageLoader {
abstract suspend fun getPages(): List<ReaderPage> abstract suspend fun getPages(): List<ReaderPage>
/** /**
* Returns an observable that should inform of the progress of the page * Loads the page. May also preload other pages.
* Progress of the page loading should be followed via [page.statusFlow].
* [loadPage] is not currently guaranteed to complete, so it should be launched asynchronously.
*/ */
abstract fun getPage(page: ReaderPage): Observable<Page.State> abstract suspend fun loadPage(page: ReaderPage)
/** /**
* Retries the given [page] in case it failed to load. This method only makes sense when an * Retries the given [page] in case it failed to load. This method only makes sense when an

View file

@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import rx.Observable
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.PipedInputStream import java.io.PipedInputStream
@ -55,16 +54,10 @@ class RarPageLoader(file: File) : PageLoader() {
} }
/** /**
* Returns an observable that emits a ready state unless the loader was recycled. * No additional action required to load the page
*/ */
override fun getPage(page: ReaderPage): Observable<Page.State> { override suspend fun loadPage(page: ReaderPage) {
return Observable.just( check(!isRecycled)
if (isRecycled) {
Page.State.ERROR
} else {
Page.State.READY
},
)
} }
/** /**

View file

@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import rx.Observable
import java.io.File import java.io.File
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -49,15 +48,9 @@ class ZipPageLoader(file: File) : PageLoader() {
} }
/** /**
* Returns an observable that emits a ready state unless the loader was recycled. * No additional action required to load the page
*/ */
override fun getPage(page: ReaderPage): Observable<Page.State> { override suspend fun loadPage(page: ReaderPage) {
return Observable.just( check(!isRecycled)
if (isRecycled) {
Page.State.ERROR
} else {
Page.State.READY
},
)
} }
} }

View file

@ -60,9 +60,14 @@ class PagerPageHolder(
private val scope = MainScope() private val scope = MainScope()
/** /**
* Subscription for status changes of the page. * Job for loading the page.
*/ */
private var statusSubscription: Subscription? = null private var loadJob: Job? = null
/**
* Job for status changes of the page.
*/
private var statusJob: Job? = null
/** /**
* Job for progress changes of the page. * Job for progress changes of the page.
@ -77,7 +82,7 @@ class PagerPageHolder(
init { init {
addView(progressIndicator) addView(progressIndicator)
observeStatus() launchLoadJob()
} }
/** /**
@ -87,22 +92,26 @@ class PagerPageHolder(
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
super.onDetachedFromWindow() super.onDetachedFromWindow()
cancelProgressJob() cancelProgressJob()
unsubscribeStatus() cancelLoadJob()
unsubscribeReadImageHeader() unsubscribeReadImageHeader()
} }
/** /**
* Observes the status of the page and notify the changes. * Starts loading the page and processing changes to the page's status.
* *
* @see processStatus * @see processStatus
*/ */
private fun observeStatus() { private fun launchLoadJob() {
statusSubscription?.unsubscribe() loadJob?.cancel()
statusJob?.cancel()
val loader = page.chapter.pageLoader ?: return val loader = page.chapter.pageLoader ?: return
statusSubscription = loader.getPage(page) loadJob = scope.launch {
.observeOn(AndroidSchedulers.mainThread()) loader.loadPage(page)
.subscribe { processStatus(it) } }
statusJob = scope.launch {
page.statusFlow.collectLatest { processStatus(it) }
}
} }
private fun launchProgressJob() { private fun launchProgressJob() {
@ -137,11 +146,13 @@ class PagerPageHolder(
} }
/** /**
* Unsubscribes from the status subscription. * Cancels loading the page and processing changes to the page's status.
*/ */
private fun unsubscribeStatus() { private fun cancelLoadJob() {
statusSubscription?.unsubscribe() loadJob?.cancel()
statusSubscription = null loadJob = null
statusJob?.cancel()
statusJob = null
} }
private fun cancelProgressJob() { private fun cancelProgressJob() {

View file

@ -73,9 +73,14 @@ class WebtoonPageHolder(
private val scope = MainScope() private val scope = MainScope()
/** /**
* Subscription for status changes of the page. * Job for loading the page.
*/ */
private var statusSubscription: Subscription? = null private var loadJob: Job? = null
/**
* Job for status changes of the page.
*/
private var statusJob: Job? = null
/** /**
* Job for progress changes of the page. * Job for progress changes of the page.
@ -101,7 +106,7 @@ class WebtoonPageHolder(
*/ */
fun bind(page: ReaderPage) { fun bind(page: ReaderPage) {
this.page = page this.page = page
observeStatus() launchLoadJob()
refreshLayoutParams() refreshLayoutParams()
} }
@ -121,7 +126,7 @@ class WebtoonPageHolder(
* Called when the view is recycled and added to the view pool. * Called when the view is recycled and added to the view pool.
*/ */
override fun recycle() { override fun recycle() {
unsubscribeStatus() cancelLoadJob()
cancelProgressJob() cancelProgressJob()
unsubscribeReadImageHeader() unsubscribeReadImageHeader()
@ -131,20 +136,21 @@ class WebtoonPageHolder(
} }
/** /**
* Observes the status of the page and notify the changes. * Starts loading the page and processing changes to the page's status.
* *
* @see processStatus * @see processStatus
*/ */
private fun observeStatus() { private fun launchLoadJob() {
unsubscribeStatus() cancelLoadJob()
val page = page ?: return val page = page ?: return
val loader = page.chapter.pageLoader ?: return val loader = page.chapter.pageLoader ?: return
statusSubscription = loader.getPage(page) loadJob = scope.launch {
.observeOn(AndroidSchedulers.mainThread()) loader.loadPage(page)
.subscribe { processStatus(it) } }
statusJob = scope.launch {
addSubscription(statusSubscription) page.statusFlow.collectLatest { processStatus(it) }
}
} }
/** /**
@ -185,11 +191,13 @@ class WebtoonPageHolder(
} }
/** /**
* Unsubscribes from the status subscription. * Cancels loading the page and processing changes to the page's status.
*/ */
private fun unsubscribeStatus() { private fun cancelLoadJob() {
removeSubscription(statusSubscription) loadJob?.cancel()
statusSubscription = null loadJob = null
statusJob?.cancel()
statusJob = null
} }
/** /**

View file

@ -20,10 +20,14 @@ open class Page(
get() = index + 1 get() = index + 1
@Transient @Transient
@Volatile private val _statusFlow = MutableStateFlow(State.QUEUE)
var status: State = State.QUEUE
@Transient
val statusFlow = _statusFlow.asStateFlow()
var status: State
get() = _statusFlow.value
set(value) { set(value) {
field = value _statusFlow.value = value
statusSubject?.onNext(value) statusSubject?.onNext(value)
} }