Replace RxJava in ChapterLoader and ReaderViewModel (#8915)

* Replace RxJava in ChapterLoader

* Don't swallow CancellationException

* Simplify loadChapter behavior

* Add error handling to loadAdjacent

(cherry picked from commit 62480f090b3007487b7125c6a2cd63a6103486cc)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt
This commit is contained in:
Two-Ai 2023-01-14 18:22:27 -05:00 committed by Jobobby04
parent 886485a472
commit 843c0a4588
2 changed files with 75 additions and 85 deletions

View File

@ -7,7 +7,6 @@ import androidx.annotation.ColorInt
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import eu.kanade.core.util.asFlow
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId
@ -79,18 +78,16 @@ import exh.source.getMainSource
import exh.source.isEhBasedManga
import exh.util.defaultReaderType
import exh.util.mangaType
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -99,9 +96,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import tachiyomi.decoder.ImageDecoder
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -171,11 +165,6 @@ class ReaderViewModel(
*/
private var chapterReadStartTime: Long? = null
/**
* Subscription to prevent setting chapters as active from multiple threads.
*/
private var activeChapterSubscription: Subscription? = null
private var chapterToDownload: Download? = null
/**
@ -332,15 +321,16 @@ class ReaderViewModel(
// val source = sourceManager.getOrStub(manga.source)
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source, /* SY --> */sourceManager, mergedReferences, mergedManga/* SY <-- */)
getLoadObservable(loader!!, chapterList.first { chapterId == it.chapter.id } /* SY --> */, page/* SY <-- */)
.asFlow()
.first()
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id } /* SY --> */, page/* SY <-- */)
Result.success(true)
} else {
// Unlikely but okay
Result.success(false)
}
} catch (e: Throwable) {
if (e is CancellationException) {
throw e
}
Result.failure(e)
}
}
@ -369,42 +359,36 @@ class ReaderViewModel(
// SY <--
/**
* Returns an observable that loads the given [chapter] with this [loader]. This observable
* handles main thread synchronization and updating the currently active chapters on
* [viewerChaptersRelay], however callers must ensure there won't be more than one
* subscription active by unsubscribing any existing [activeChapterSubscription] before.
* Callers must also handle the onError event.
* Loads the given [chapter] with this [loader] and updates the currently active chapters.
* Callers must handle errors.
*/
private fun getLoadObservable(
private suspend fun loadChapter(
loader: ChapterLoader,
chapter: ReaderChapter,
// SY -->
page: Int? = null,
// SY <--
): Observable<ViewerChapters> {
return loader.loadChapter(chapter /* SY --> */, page/* SY <-- */)
.andThen(
Observable.fromCallable {
val chapterPos = chapterList.indexOf(chapter)
): ViewerChapters {
loader.loadChapter(chapter /* SY --> */, page/* SY <-- */)
ViewerChapters(
chapter,
chapterList.getOrNull(chapterPos - 1),
chapterList.getOrNull(chapterPos + 1),
)
},
)
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { newChapters ->
mutableState.update {
// Add new references first to avoid unnecessary recycling
newChapters.ref()
it.viewerChapters?.unref()
val chapterPos = chapterList.indexOf(chapter)
val newChapters = ViewerChapters(
chapter,
chapterList.getOrNull(chapterPos - 1),
chapterList.getOrNull(chapterPos + 1),
)
chapterToDownload = cancelQueuedDownloads(newChapters.currChapter)
it.copy(viewerChapters = newChapters)
}
withUIContext {
mutableState.update {
// Add new references first to avoid unnecessary recycling
newChapters.ref()
it.viewerChapters?.unref()
chapterToDownload = cancelQueuedDownloads(newChapters.currChapter)
it.copy(viewerChapters = newChapters)
}
}
return newChapters
}
/**
@ -417,10 +401,14 @@ class ReaderViewModel(
logcat { "Loading ${chapter.chapter.url}" }
withIOContext {
getLoadObservable(loader, chapter)
.asFlow()
.catch { logcat(LogPriority.ERROR, it) }
.first()
try {
loadChapter(loader, chapter)
} catch (e: Throwable) {
if (e is CancellationException) {
throw e
}
logcat(LogPriority.ERROR, e)
}
}
}
@ -430,9 +418,7 @@ class ReaderViewModel(
}
/**
* Called when the user is going to load the prev/next chapter through the menu button. It
* sets the [isLoadingAdjacentChapterRelay] that the view uses to prevent any further
* interaction until the chapter is loaded.
* Called when the user is going to load the prev/next chapter through the menu button.
*/
private suspend fun loadAdjacent(chapter: ReaderChapter) {
val loader = loader ?: return
@ -440,12 +426,18 @@ class ReaderViewModel(
logcat { "Loading adjacent ${chapter.chapter.url}" }
mutableState.update { it.copy(isLoadingAdjacentChapter = true) }
withIOContext {
getLoadObservable(loader, chapter)
.asFlow()
.first()
try {
withIOContext {
loadChapter(loader, chapter)
}
} catch (e: Throwable) {
if (e is CancellationException) {
throw e
}
logcat(LogPriority.ERROR, e)
} finally {
mutableState.update { it.copy(isLoadingAdjacentChapter = false) }
}
mutableState.update { it.copy(isLoadingAdjacentChapter = false) }
}
/**
@ -476,12 +468,15 @@ class ReaderViewModel(
val loader = loader ?: return
withIOContext {
loader.loadChapter(chapter)
.doOnCompleted { eventChannel.trySend(Event.ReloadViewerChapters) }
.onErrorComplete()
.toObservable<Unit>()
.asFlow()
.firstOrNull()
try {
loader.loadChapter(chapter)
} catch (e: Throwable) {
if (e is CancellationException) {
throw e
}
return@withIOContext
}
eventChannel.trySend(Event.ReloadViewerChapters)
}
}

View File

@ -12,13 +12,11 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat
import exh.debug.DebugFunctions.readerPrefs
import exh.merged.sql.models.MergedMangaReference
import rx.Completable
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
/**
* Loader used to retrieve the [PageLoader] for a given chapter.
@ -37,36 +35,28 @@ class ChapterLoader(
) {
/**
* Returns a completable that assigns the page loader and loads the its pages. It just
* completes if the chapter is already loaded.
* Assigns the chapter's page loader and loads the its pages. Returns immediately if the chapter
* is already loaded.
*/
fun loadChapter(chapter: ReaderChapter /* SY --> */, page: Int? = null/* SY <-- */): Completable {
suspend fun loadChapter(chapter: ReaderChapter /* SY --> */, page: Int? = null/* SY <-- */) {
if (chapterIsReady(chapter)) {
return Completable.complete()
return
}
return Observable.just(chapter)
.doOnNext { chapter.state = ReaderChapter.State.Loading }
.observeOn(Schedulers.io())
.flatMap { readerChapter ->
logcat { "Loading pages for ${chapter.chapter.name}" }
chapter.state = ReaderChapter.State.Loading
withIOContext {
logcat { "Loading pages for ${chapter.chapter.name}" }
try {
val loader = getPageLoader(chapter)
chapter.pageLoader = loader
val loader = getPageLoader(readerChapter)
val pages = loader.getPages().awaitSingle()
.onEach { it.chapter = chapter }
loader.getPages().take(1).doOnNext { pages ->
pages.forEach { it.chapter = chapter }
}.map { pages -> loader to pages }
}
.observeOn(AndroidSchedulers.mainThread())
.doOnError { chapter.state = ReaderChapter.State.Error(it) }
.doOnNext { (loader, pages) ->
if (pages.isEmpty()) {
throw Exception(context.getString(R.string.page_list_empty_error))
}
chapter.pageLoader = loader // Assign here to fix race with unref
chapter.state = ReaderChapter.State.Loaded(pages)
// If the chapter is partially read, set the starting page to the last the user read
// otherwise use the requested page.
if (!chapter.chapter.read /* --> EH */ || readerPrefs
@ -75,8 +65,13 @@ class ChapterLoader(
) {
chapter.requestedPage = /* SY --> */ page ?: /* SY <-- */ chapter.chapter.last_page_read
}
chapter.state = ReaderChapter.State.Loaded(pages)
} catch (e: Throwable) {
chapter.state = ReaderChapter.State.Error(e)
throw e
}
.toCompletable()
}
}
/**