Replace RxJava in ChapterLoader and ReaderViewModel (#8915)
* Replace RxJava in ChapterLoader * Don't swallow CancellationException * Simplify loadChapter behavior * Add error handling to loadAdjacent
This commit is contained in:
parent
e7937fe562
commit
62480f090b
@ -6,7 +6,6 @@ import android.net.Uri
|
|||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import eu.kanade.core.util.asFlow
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||||
@ -60,17 +59,15 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|||||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
import eu.kanade.tachiyomi.util.system.isOnline
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -79,9 +76,6 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
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.Date
|
import java.util.Date
|
||||||
@ -141,11 +135,6 @@ class ReaderViewModel(
|
|||||||
*/
|
*/
|
||||||
private var chapterReadStartTime: Long? = null
|
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
|
private var chapterToDownload: Download? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -279,45 +268,39 @@ class ReaderViewModel(
|
|||||||
val source = sourceManager.getOrStub(manga.source)
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
|
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
|
||||||
|
|
||||||
getLoadObservable(loader!!, chapterList.first { chapterId == it.chapter.id })
|
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
|
||||||
.asFlow()
|
|
||||||
.first()
|
|
||||||
Result.success(true)
|
Result.success(true)
|
||||||
} else {
|
} else {
|
||||||
// Unlikely but okay
|
// Unlikely but okay
|
||||||
Result.success(false)
|
Result.success(false)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable that loads the given [chapter] with this [loader]. This observable
|
* Loads the given [chapter] with this [loader] and updates the currently active chapters.
|
||||||
* handles main thread synchronization and updating the currently active chapters on
|
* Callers must handle errors.
|
||||||
* [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.
|
|
||||||
*/
|
*/
|
||||||
private fun getLoadObservable(
|
private suspend fun loadChapter(
|
||||||
loader: ChapterLoader,
|
loader: ChapterLoader,
|
||||||
chapter: ReaderChapter,
|
chapter: ReaderChapter,
|
||||||
): Observable<ViewerChapters> {
|
): ViewerChapters {
|
||||||
return loader.loadChapter(chapter)
|
loader.loadChapter(chapter)
|
||||||
.andThen(
|
|
||||||
Observable.fromCallable {
|
|
||||||
val chapterPos = chapterList.indexOf(chapter)
|
|
||||||
|
|
||||||
ViewerChapters(
|
val chapterPos = chapterList.indexOf(chapter)
|
||||||
|
val newChapters = ViewerChapters(
|
||||||
chapter,
|
chapter,
|
||||||
chapterList.getOrNull(chapterPos - 1),
|
chapterList.getOrNull(chapterPos - 1),
|
||||||
chapterList.getOrNull(chapterPos + 1),
|
chapterList.getOrNull(chapterPos + 1),
|
||||||
)
|
)
|
||||||
},
|
|
||||||
)
|
withUIContext {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnNext { newChapters ->
|
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
// Add new references first to avoid unnecessary recycling
|
// Add new references first to avoid unnecessary recycling
|
||||||
newChapters.ref()
|
newChapters.ref()
|
||||||
@ -327,6 +310,7 @@ class ReaderViewModel(
|
|||||||
it.copy(viewerChapters = newChapters)
|
it.copy(viewerChapters = newChapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return newChapters
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -339,17 +323,19 @@ class ReaderViewModel(
|
|||||||
logcat { "Loading ${chapter.chapter.url}" }
|
logcat { "Loading ${chapter.chapter.url}" }
|
||||||
|
|
||||||
withIOContext {
|
withIOContext {
|
||||||
getLoadObservable(loader, chapter)
|
try {
|
||||||
.asFlow()
|
loadChapter(loader, chapter)
|
||||||
.catch { logcat(LogPriority.ERROR, it) }
|
} catch (e: Throwable) {
|
||||||
.first()
|
if (e is CancellationException) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the user is going to load the prev/next chapter through the menu button. It
|
* Called when the user is going to load the prev/next chapter through the menu button.
|
||||||
* sets the [isLoadingAdjacentChapterRelay] that the view uses to prevent any further
|
|
||||||
* interaction until the chapter is loaded.
|
|
||||||
*/
|
*/
|
||||||
private suspend fun loadAdjacent(chapter: ReaderChapter) {
|
private suspend fun loadAdjacent(chapter: ReaderChapter) {
|
||||||
val loader = loader ?: return
|
val loader = loader ?: return
|
||||||
@ -357,13 +343,19 @@ class ReaderViewModel(
|
|||||||
logcat { "Loading adjacent ${chapter.chapter.url}" }
|
logcat { "Loading adjacent ${chapter.chapter.url}" }
|
||||||
|
|
||||||
mutableState.update { it.copy(isLoadingAdjacentChapter = true) }
|
mutableState.update { it.copy(isLoadingAdjacentChapter = true) }
|
||||||
|
try {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
getLoadObservable(loader, chapter)
|
loadChapter(loader, chapter)
|
||||||
.asFlow()
|
|
||||||
.first()
|
|
||||||
}
|
}
|
||||||
|
} 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) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so
|
* Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so
|
||||||
@ -393,12 +385,15 @@ class ReaderViewModel(
|
|||||||
|
|
||||||
val loader = loader ?: return
|
val loader = loader ?: return
|
||||||
withIOContext {
|
withIOContext {
|
||||||
|
try {
|
||||||
loader.loadChapter(chapter)
|
loader.loadChapter(chapter)
|
||||||
.doOnCompleted { eventChannel.trySend(Event.ReloadViewerChapters) }
|
} catch (e: Throwable) {
|
||||||
.onErrorComplete()
|
if (e is CancellationException) {
|
||||||
.toObservable<Unit>()
|
throw e
|
||||||
.asFlow()
|
}
|
||||||
.firstOrNull()
|
return@withIOContext
|
||||||
|
}
|
||||||
|
eventChannel.trySend(Event.ReloadViewerChapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,11 +11,9 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
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 eu.kanade.tachiyomi.util.system.logcat
|
||||||
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.
|
* Loader used to retrieve the [PageLoader] for a given chapter.
|
||||||
@ -29,43 +27,40 @@ class ChapterLoader(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a completable that assigns the page loader and loads the its pages. It just
|
* Assigns the chapter's page loader and loads the its pages. Returns immediately if the chapter
|
||||||
* completes if the chapter is already loaded.
|
* is already loaded.
|
||||||
*/
|
*/
|
||||||
fun loadChapter(chapter: ReaderChapter): Completable {
|
suspend fun loadChapter(chapter: ReaderChapter) {
|
||||||
if (chapterIsReady(chapter)) {
|
if (chapterIsReady(chapter)) {
|
||||||
return Completable.complete()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return Observable.just(chapter)
|
chapter.state = ReaderChapter.State.Loading
|
||||||
.doOnNext { chapter.state = ReaderChapter.State.Loading }
|
withIOContext {
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.flatMap { readerChapter ->
|
|
||||||
logcat { "Loading pages for ${chapter.chapter.name}" }
|
logcat { "Loading pages for ${chapter.chapter.name}" }
|
||||||
|
try {
|
||||||
val loader = getPageLoader(readerChapter)
|
val loader = getPageLoader(chapter)
|
||||||
chapter.pageLoader = loader
|
chapter.pageLoader = loader
|
||||||
|
|
||||||
loader.getPages().take(1).doOnNext { pages ->
|
val pages = loader.getPages().awaitSingle()
|
||||||
pages.forEach { it.chapter = chapter }
|
.onEach { it.chapter = chapter }
|
||||||
}
|
|
||||||
}
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnError { chapter.state = ReaderChapter.State.Error(it) }
|
|
||||||
.doOnNext { pages ->
|
|
||||||
if (pages.isEmpty()) {
|
if (pages.isEmpty()) {
|
||||||
throw Exception(context.getString(R.string.page_list_empty_error))
|
throw Exception(context.getString(R.string.page_list_empty_error))
|
||||||
}
|
}
|
||||||
|
|
||||||
chapter.state = ReaderChapter.State.Loaded(pages)
|
|
||||||
|
|
||||||
// If the chapter is partially read, set the starting page to the last the user read
|
// If the chapter is partially read, set the starting page to the last the user read
|
||||||
// otherwise use the requested page.
|
// otherwise use the requested page.
|
||||||
if (!chapter.chapter.read) {
|
if (!chapter.chapter.read) {
|
||||||
chapter.requestedPage = chapter.chapter.last_page_read
|
chapter.requestedPage = chapter.chapter.last_page_read
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chapter.state = ReaderChapter.State.Loaded(pages)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
chapter.state = ReaderChapter.State.Error(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.toCompletable()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user