@ -3,8 +3,10 @@ package eu.kanade.tachiyomi.ui.reader
import android.app.Application
import android.content.Context
import android.net.Uri
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
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.UpdateChapter
@ -16,17 +18,15 @@ import eu.kanade.domain.history.interactor.UpsertHistory
import eu.kanade.domain.history.model.HistoryUpdate
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.isLocal
import eu.kanade.domain.manga.model.toDbManga
import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.data.database.models.toDomainManga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.data.download.model.Download
@ -54,37 +54,41 @@ import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import eu.kanade.tachiyomi.util.lang.takeBytes
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir
import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
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
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import nucleus.presenter.RxPresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
import eu.kanade.domain.manga.model.Manga as DomainManga
/ * *
* Presenter used by the activity to perform background operations .
* /
class ReaderPresenter (
class ReaderViewModel (
private val savedState : SavedStateHandle = SavedStateHandle ( ) ,
private val sourceManager : SourceManager = Injekt . get ( ) ,
private val downloadManager : DownloadManager = Injekt . get ( ) ,
private val downloadProvider : DownloadProvider = Injekt . get ( ) ,
@ -102,20 +106,28 @@ class ReaderPresenter(
private val upsertHistory : UpsertHistory = Injekt . get ( ) ,
private val updateChapter : UpdateChapter = Injekt . get ( ) ,
private val setMangaViewerFlags : SetMangaViewerFlags = Injekt . get ( ) ,
) : RxPresenter< ReaderActivity > ( ) {
) : ViewModel ( ) {
private val coroutineScope : CoroutineScope = MainScope ( )
private val mutableState = MutableStateFlow ( State ( ) )
val state = mutableState . asStateFlow ( )
private val eventChannel = Channel < Event > ( )
val eventFlow = eventChannel . receiveAsFlow ( )
/ * *
* The manga loaded in the reader . It can be null when instantiated for a short time .
* /
va r manga : Manga ? = null
private set
va l manga : Manga ?
get( ) = state . value . manga
/ * *
* The chapter id of the currently loaded chapter . Used to restore from process kill .
* /
private var chapterId = - 1L
private var chapterId = savedState . get < Long > ( " chapter_id " ) ?: - 1L
set ( value ) {
savedState [ " chapter_id " ] = value
field = value
}
/ * *
* The chapter loader for the loaded manga . It ' ll be null until [ manga ] is set .
@ -132,16 +144,6 @@ class ReaderPresenter(
* /
private var activeChapterSubscription : Subscription ? = null
/ * *
* Relay for currently active viewer chapters .
* /
private val viewerChaptersRelay = BehaviorRelay . create < ViewerChapters > ( )
/ * *
* Used when loading prev / next chapter needed to lock the UI ( with a dialog ) .
* /
private val isLoadingAdjacentChapterEvent = Channel < Boolean > ( )
private var chapterToDownload : Download ? = null
/ * *
@ -149,7 +151,7 @@ class ReaderPresenter(
* time in a background thread to avoid blocking the UI .
* /
private val chapterList by lazy {
val manga = manga !! . toDomainManga ( ) !!
val manga = manga !!
val chapters = runBlocking { getChapterByMangaId . await ( manga . id ) }
val selectedChapter = chapters . find { it . id == chapterId }
@ -161,12 +163,12 @@ class ReaderPresenter(
when {
readerPreferences . skipRead ( ) . get ( ) && it . read -> true
readerPreferences . skipFiltered ( ) . get ( ) -> {
( manga . unreadFilterRaw == Domain Manga. CHAPTER _SHOW _READ && ! it . read ) ||
( manga . unreadFilterRaw == Domain Manga. CHAPTER _SHOW _UNREAD && it . read ) ||
( manga . downloadedFilterRaw == Domain Manga. CHAPTER _SHOW _DOWNLOADED && ! downloadManager . isChapterDownloaded ( it . name , it . scanlator , manga . title , manga . source ) ) ||
( manga . downloadedFilterRaw == Domain Manga. CHAPTER _SHOW _NOT _DOWNLOADED && downloadManager . isChapterDownloaded ( it . name , it . scanlator , manga . title , manga . source ) ) ||
( manga . bookmarkedFilterRaw == Domain Manga. CHAPTER _SHOW _BOOKMARKED && ! it . bookmark ) ||
( manga . bookmarkedFilterRaw == Domain Manga. CHAPTER _SHOW _NOT _BOOKMARKED && it . bookmark )
( manga . unreadFilterRaw == Manga. CHAPTER _SHOW _READ && ! it . read ) ||
( manga . unreadFilterRaw == Manga. CHAPTER _SHOW _UNREAD && it . read ) ||
( manga . downloadedFilterRaw == Manga. CHAPTER _SHOW _DOWNLOADED && ! downloadManager . isChapterDownloaded ( it . name , it . scanlator , manga . title , manga . source ) ) ||
( manga . downloadedFilterRaw == Manga. CHAPTER _SHOW _NOT _DOWNLOADED && downloadManager . isChapterDownloaded ( it . name , it . scanlator , manga . title , manga . source ) ) ||
( manga . bookmarkedFilterRaw == Manga. CHAPTER _SHOW _BOOKMARKED && ! it . bookmark ) ||
( manga . bookmarkedFilterRaw == Manga. CHAPTER _SHOW _NOT _BOOKMARKED && it . bookmark )
}
else -> false
}
@ -188,32 +190,15 @@ class ReaderPresenter(
}
private var hasTrackers : Boolean = false
private val checkTrackers : ( Domain Manga) -> Unit = { manga ->
private val checkTrackers : ( Manga) -> Unit = { manga ->
val tracks = runBlocking { getTracks . await ( manga . id ) }
hasTrackers = tracks . isNotEmpty ( )
}
private val incognitoMode = preferences . incognitoMode ( ) . get ( )
/ * *
* Called when the presenter is created . It retrieves the saved active chapter if the process
* was restored .
* /
override fun onCreate ( savedState : Bundle ? ) {
super . onCreate ( savedState )
if ( savedState != null ) {
chapterId = savedState . getLong ( :: chapterId . name , - 1 )
}
}
/ * *
* Called when the presenter is destroyed . It saves the current progress and cleans up
* references on the currently active chapters .
* /
override fun onDestroy ( ) {
super . onDestroy ( )
coroutineScope . cancel ( )
val currentChapters = viewerChaptersRelay . value
override fun onCleared ( ) {
val currentChapters = state . value . viewerChapters
if ( currentChapters != null ) {
currentChapters . unref ( )
saveReadingProgress ( currentChapters . currChapter )
@ -223,24 +208,24 @@ class ReaderPresenter(
}
}
/ * *
* Called when the presenter instance is being saved . It saves the currently active chapter
* id and the last page read .
* /
override fun onSave ( state : Bundle ) {
super . onSave ( state )
val currentChapter = getCurrentChapter ( )
if ( currentChapter != null ) {
currentChapter . requestedPage = currentChapter . chapter . last _page _read
state . putLong ( :: chapterId . name , currentChapter . chapter . id !! )
}
init {
// To save state
state . map { it . viewerChapters ?. currChapter }
. distinctUntilChanged ( )
. onEach { currentChapter ->
if ( currentChapter != null ) {
currentChapter . requestedPage = currentChapter . chapter . last _page _read
chapterId = currentChapter . chapter . id !!
}
}
. launchIn ( viewModelScope )
}
/ * *
* Called when the user pressed the back button and is going to leave the reader . Used to
* trigger deletion of the downloaded chapters .
* /
fun on BackPressed ( ) {
fun on ActivityFinish ( ) {
deletePendingChapters ( )
}
@ -250,7 +235,7 @@ class ReaderPresenter(
* /
fun onSaveInstanceStateNonConfigurationChange ( ) {
val currentChapter = getCurrentChapter ( ) ?: return
coroutine Scope. launchNonCancellable {
viewModel Scope. launchNonCancellable {
saveChapterProgress ( currentChapter )
}
}
@ -266,58 +251,33 @@ class ReaderPresenter(
* Initializes this presenter with the given [ mangaId ] and [ initialChapterId ] . This method will
* fetch the manga from the database and initialize the initial chapter .
* /
fun init ( mangaId : Long , initialChapterId : Long ) {
if ( ! needsInit ( ) ) return
coroutineScope . launchIO {
suspend fun init ( mangaId : Long , initialChapterId : Long ) : Result < Boolean > {
if ( ! needsInit ( ) ) return Result . success ( true )
return withIOContext {
try {
val manga = getManga . await ( mangaId )
withUIContext {
manga ?. let { init ( it . toDbManga ( ) , initialChapterId ) }
}
} catch ( e : Throwable ) {
view ?. setInitialChapterError ( e )
}
}
}
/ * *
* Initializes this presenter with the given [ manga ] and [ initialChapterId ] . This method will
* set the chapter loader , view subscriptions and trigger an initial load .
* /
private fun init ( manga : Manga , initialChapterId : Long ) {
if ( ! needsInit ( ) ) return
if ( manga != null ) {
mutableState . update { it . copy ( manga = manga ) }
if ( chapterId == - 1L ) chapterId = initialChapterId
this . manga = manga
if ( chapterId == - 1L ) chapterId = initialChapterId
checkTrackers ( manga )
checkTrackers ( manga . toDomainManga ( ) !! )
val context = Injekt . get < Application > ( )
val source = sourceManager . getOrStub ( manga . source )
loader = ChapterLoader ( context , downloadManager , downloadProvider , manga , source )
val context = Injekt . get < Application > ( )
val source = sourceManager . getOrStub ( manga . source )
loader = ChapterLoader ( context , downloadManager , downloadProvider , manga . toDomainManga ( ) !! , source )
Observable . just ( manga ) . subscribeLatestCache ( ReaderActivity :: setManga )
viewerChaptersRelay . subscribeLatestCache ( ReaderActivity :: setChapters )
coroutineScope . launch {
isLoadingAdjacentChapterEvent . receiveAsFlow ( ) . collectLatest {
view ?. setProgressDialog ( it )
getLoadObservable ( loader !! , chapterList . first { chapterId == it . chapter . id } )
. asFlow ( )
. first ( )
Result . success ( true )
} else {
// Unlikely but okay
Result . success ( false )
}
} catch ( e : Throwable ) {
Result . failure ( e )
}
}
// Read chapterList from an io thread because it's retrieved lazily and would block main.
activeChapterSubscription ?. unsubscribe ( )
activeChapterSubscription = Observable
. fromCallable { chapterList . first { chapterId == it . chapter . id } }
. flatMap { getLoadObservable ( loader !! , it ) }
. subscribeOn ( Schedulers . io ( ) )
. observeOn ( AndroidSchedulers . mainThread ( ) )
. subscribeFirst (
{ _ , _ ->
// Ignore onNext event
} ,
ReaderActivity :: setInitialChapterError ,
)
}
/ * *
@ -345,14 +305,14 @@ class ReaderPresenter(
)
. observeOn ( AndroidSchedulers . mainThread ( ) )
. doOnNext { newChapters ->
val oldChapters = viewerChaptersRelay . value
// Add new references first to avoid unnecessary recycling
newChapters . ref ( )
oldChapters ?. unref ( )
mutableState . update {
// Add new references first to avoid unnecessary recycling
newChapters . ref ( )
it . viewerChapters ?. unref ( )
chapterToDownload = cancelQueuedDownloads ( newChapters . currChapter )
viewerChaptersRelay . call ( newChapters )
chapterToDownload = cancelQueuedDownloads ( newChapters . currChapter )
it . copy ( viewerChapters = newChapters )
}
}
}
@ -360,17 +320,17 @@ class ReaderPresenter(
* Called when the user changed to the given [ chapter ] when changing pages from the viewer .
* It ' s used only to set this chapter as active .
* /
private fun loadNewChapter ( chapter : ReaderChapter ) {
private suspend fun loadNewChapter ( chapter : ReaderChapter ) {
val loader = loader ?: return
logcat { " Loading ${chapter.chapter.url} " }
activeChapterSubscription?. unsubscribe ( )
activeChapterSubscription = getLoadObservable ( loader , chapter )
. toCompletable ( )
. onErrorComplete ( )
. subscribe ( )
. also ( :: add )
withIOContext {
getLoadObservable ( loader , chapter )
. asFlow ( )
. catch { logcat ( LogPriority . ERROR , it ) }
. first ( )
}
}
/ * *
@ -378,30 +338,25 @@ class ReaderPresenter(
* sets the [ isLoadingAdjacentChapterRelay ] that the view uses to prevent any further
* interaction until the chapter is loaded .
* /
private fun loadAdjacent ( chapter : ReaderChapter ) {
private suspend fun loadAdjacent ( chapter : ReaderChapter ) {
val loader = loader ?: return
logcat { " Loading adjacent ${chapter.chapter.url} " }
activeChapterSubscription ?. unsubscribe ( )
activeChapterSubscription = getLoadObservable ( loader , chapter )
. doOnSubscribe { coroutineScope . launch { isLoadingAdjacentChapterEvent . send ( true ) } }
. doOnUnsubscribe { coroutineScope . launch { isLoadingAdjacentChapterEvent . send ( false ) } }
. subscribeFirst (
{ view , _ ->
view . moveToPageIndex ( 0 )
} ,
{ _ , _ ->
// Ignore onError event, viewers handle that state
} ,
)
mutableState . update { it . copy ( isLoadingAdjacentChapter = true ) }
withIOContext {
getLoadObservable ( loader , chapter )
. asFlow ( )
. first ( )
}
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
* that the user doesn ' t have to wait too long to continue reading .
* /
private fun preload ( chapter : ReaderChapter ) {
private suspend fun preload ( chapter : ReaderChapter ) {
if ( chapter . pageLoader is HttpPageLoader ) {
val manga = manga ?: return
val dbChapter = chapter . chapter
@ -424,13 +379,14 @@ class ReaderPresenter(
logcat { " Preloading ${chapter.chapter.url} " }
val loader = loader ?: return
loader . loadChapter ( chapter )
. observeOn ( AndroidSchedulers . mainThread ( ) )
// Update current chapters whenever a chapter is preloaded
. doOnCompleted { viewerChaptersRelay . value ?. let ( viewerChaptersRelay :: call ) }
. onErrorComplete ( )
. subscribe ( )
. also ( :: add )
withIOContext {
loader . loadChapter ( chapter )
. doOnCompleted { eventChannel . trySend ( Event . ReloadViewerChapters ) }
. onErrorComplete ( )
. toObservable < Unit > ( )
. asFlow ( )
. firstOrNull ( )
}
}
/ * *
@ -439,7 +395,7 @@ class ReaderPresenter(
* [ page ] ' s chapter is different from the currently active .
* /
fun onPageSelected ( page : ReaderPage ) {
val currentChapters = viewerChaptersRelay. value ?: return
val currentChapters = state. value . viewerChapters ?: return
val selectedChapter = page . chapter
@ -461,7 +417,7 @@ class ReaderPresenter(
logcat { " Setting ${selectedChapter.chapter.url} as active " }
saveReadingProgress ( currentChapters . currChapter )
setReadStartTime ( )
loadNewChapter( selectedChapter )
viewModelScope. launch { loadNewChapter( selectedChapter ) }
}
val pages = page . chapter . pages ?: return
val inDownloadRange = page . number . toDouble ( ) / pages . size > 0.25
@ -477,9 +433,9 @@ class ReaderPresenter(
// Only download ahead if current + next chapter is already downloaded too to avoid jank
if ( getCurrentChapter ( ) ?. pageLoader !is DownloadPageLoader ) return
val nextChapter = viewerChaptersRelay. value ?. nextChapter ?. chapter ?: return
val nextChapter = state. value . viewerChapters?. nextChapter ?. chapter ?: return
coroutine Scope. launchIO {
viewModel Scope. launchIO {
val isNextChapterDownloaded = downloadManager . isChapterDownloaded (
nextChapter . name ,
nextChapter . scanlator ,
@ -488,10 +444,10 @@ class ReaderPresenter(
)
if ( !is NextChapterDownloaded ) return @launchIO
val chaptersToDownload = getNextChapters . await ( manga . id !! , nextChapter . id !! )
val chaptersToDownload = getNextChapters . await ( manga . id , nextChapter . id !! )
. take ( amount )
downloadManager . downloadChapters (
manga .toDomainManga ( ) !! ,
manga ,
chaptersToDownload ,
)
}
@ -535,7 +491,7 @@ class ReaderPresenter(
* Called when reader chapter is changed in reader or when activity is paused .
* /
private fun saveReadingProgress ( readerChapter : ReaderChapter ) {
coroutine Scope. launchNonCancellable {
viewModel Scope. launchNonCancellable {
saveChapterProgress ( readerChapter )
saveChapterHistory ( readerChapter )
}
@ -583,23 +539,23 @@ class ReaderPresenter(
/ * *
* Called from the activity to preload the given [ chapter ] .
* /
fun preloadChapter ( chapter : ReaderChapter ) {
suspend fun preloadChapter ( chapter : ReaderChapter ) {
preload ( chapter )
}
/ * *
* Called from the activity to load and set the next chapter as active .
* /
fun loadNextChapter ( ) {
val nextChapter = viewerChaptersRelay. value ?. nextChapter ?: return
suspend fun loadNextChapter ( ) {
val nextChapter = state. value . viewerChapters?. nextChapter ?: return
loadAdjacent ( nextChapter )
}
/ * *
* Called from the activity to load and set the previous chapter as active .
* /
fun loadPreviousChapter ( ) {
val prevChapter = viewerChaptersRelay. value ?. prevChapter ?: return
suspend fun loadPreviousChapter ( ) {
val prevChapter = state. value . viewerChapters?. prevChapter ?: return
loadAdjacent ( prevChapter )
}
@ -607,7 +563,7 @@ class ReaderPresenter(
* Returns the currently active chapter .
* /
fun getCurrentChapter ( ) : ReaderChapter ? {
return viewerChaptersRelay. value ?. currChapter
return state. value . viewerChapters?. currChapter
}
fun getSource ( ) = manga ?. source ?. let { sourceManager . getOrStub ( it ) } as ? HttpSource
@ -625,7 +581,7 @@ class ReaderPresenter(
fun bookmarkCurrentChapter ( bookmarked : Boolean ) {
val chapter = getCurrentChapter ( ) ?. chapter ?: return
chapter . bookmark = bookmarked // Otherwise the bookmark icon doesn't update
coroutine Scope. launchNonCancellable {
viewModel Scope. launchNonCancellable {
updateChapter . await (
ChapterUpdate (
id = chapter . id !! . toLong ( ) ,
@ -640,10 +596,10 @@ class ReaderPresenter(
* /
fun getMangaReadingMode ( resolveDefault : Boolean = true ) : Int {
val default = readerPreferences . defaultReadingMode ( ) . get ( )
val readingMode = ReadingModeType . fromPreference ( manga ?. readingModeType )
val readingMode = ReadingModeType . fromPreference ( manga ?. readingModeType ?. toInt ( ) )
return when {
resolveDefault && readingMode == ReadingModeType . DEFAULT -> default
else -> manga ?. readingModeType ?: default
else -> manga ?. readingModeType ?. toInt ( ) ?: default
}
}
@ -652,22 +608,21 @@ class ReaderPresenter(
* /
fun setMangaReadingMode ( readingModeType : Int ) {
val manga = manga ?: return
manga . readingModeType = readingModeType
coroutineScope . launchIO {
setMangaViewerFlags . awaitSetMangaReadingMode ( manga . id !! . toLong ( ) , readingModeType . toLong ( ) )
delay ( 250 )
val currChapters = viewerChaptersRelay . value
viewModelScope . launchIO {
setMangaViewerFlags . awaitSetMangaReadingMode ( manga . id , readingModeType . toLong ( ) )
val currChapters = state . value . viewerChapters
if ( currChapters != null ) {
// Save current page
val currChapter = currChapters . currChapter
currChapter . requestedPage = currChapter . chapter . last _page _read
withUIContext {
// Emit manga and chapters to the new viewer
view ?. setManga ( manga )
view ?. setChapters ( currChapters )
mutableState . update {
it . copy (
manga = getManga . await ( manga . id ) ,
viewerChapters = currChapters ,
)
}
eventChannel . send ( Event . ReloadViewerChapters )
}
}
}
@ -677,10 +632,10 @@ class ReaderPresenter(
* /
fun getMangaOrientationType ( resolveDefault : Boolean = true ) : Int {
val default = readerPreferences . defaultOrientationType ( ) . get ( )
val orientation = OrientationType . fromPreference ( manga ?. orientationType )
val orientation = OrientationType . fromPreference ( manga ?. orientationType ?. toInt ( ) )
return when {
resolveDefault && orientation == OrientationType . DEFAULT -> default
else -> manga ?. orientationType ?: default
else -> manga ?. orientationType ?. toInt ( ) ?: default
}
}
@ -689,14 +644,22 @@ class ReaderPresenter(
* /
fun setMangaOrientationType ( rotationType : Int ) {
val manga = manga ?: return
manga . orientationType = rotationType
coroutineScope . launchIO {
setMangaViewerFlags . awaitSetOrientationType ( manga . id !! . toLong ( ) , rotationType . toLong ( ) )
delay ( 250 )
val currChapters = viewerChaptersRelay . value
viewModelScope . launchIO {
setMangaViewerFlags . awaitSetOrientationType ( manga . id , rotationType . toLong ( ) )
val currChapters = state . value . viewerChapters
if ( currChapters != null ) {
withUIContext { view ?. setOrientation ( getMangaOrientationType ( ) ) }
// Save current page
val currChapter = currChapters . currChapter
currChapter . requestedPage = currChapter . chapter . last _page _read
mutableState . update {
it . copy (
manga = getManga . await ( manga . id ) ,
viewerChapters = currChapters ,
)
}
eventChannel . send ( Event . SetOrientation ( getMangaOrientationType ( ) ) )
eventChannel . send ( Event . ReloadViewerChapters )
}
}
}
@ -733,8 +696,8 @@ class ReaderPresenter(
val relativePath = if ( readerPreferences . folderPerManga ( ) . get ( ) ) DiskUtil . buildValidFilename ( manga . title ) else " "
// Copy file in background.
try {
coroutineScope . launchNonCancellable {
viewModelScope . launchNonCancellable {
try {
val uri = imageSaver . save (
image = Image . Page (
inputStream = page . stream !! ,
@ -744,12 +707,12 @@ class ReaderPresenter(
)
withUIContext {
notifier . onComplete ( uri )
view?. onSaveImageResult ( SaveImageResult . Success ( uri ) )
eventChannel. send ( Event . SavedImage ( SaveImageResult . Success ( uri ) ) )
}
} catch ( e : Throwable ) {
notifier . onError ( e . message )
eventChannel . send ( Event . SavedImage ( SaveImageResult . Error ( e ) ) )
}
} catch ( e : Throwable ) {
notifier . onError ( e . message )
view ?. onSaveImageResult ( SaveImageResult . Error ( e ) )
}
}
@ -770,7 +733,7 @@ class ReaderPresenter(
val filename = generateFilename ( manga , page )
try {
coroutine Scope. launchNonCancellable {
viewModel Scope. launchNonCancellable {
destDir . deleteRecursively ( )
val uri = imageSaver . save (
image = Image . Page (
@ -779,9 +742,7 @@ class ReaderPresenter(
location = Location . Cache ,
) ,
)
withUIContext {
view ?. onShareImageResult ( uri , page )
}
eventChannel . send ( Event . ShareImage ( uri , page ) )
}
} catch ( e : Throwable ) {
logcat ( LogPriority . ERROR , e )
@ -793,24 +754,21 @@ class ReaderPresenter(
* /
fun setAsCover ( context : Context , page : ReaderPage ) {
if ( page . status != Page . State . READY ) return
val manga = manga ?. toDomainManga ( ) ?: return
val manga = manga ?: return
val stream = page . stream ?: return
coroutine Scope. launchNonCancellable {
try {
viewModel Scope. launchNonCancellable {
val result = try {
manga . editCover ( context , stream ( ) )
withUIContext {
view ?. onSetAsCoverResult (
if ( manga . isLocal ( ) || manga . favorite ) {
SetAsCoverResult . Success
} else {
SetAsCoverResult . AddToLibraryFirst
} ,
)
if ( manga . isLocal ( ) || manga . favorite ) {
SetAsCoverResult . Success
} else {
SetAsCoverResult . AddToLibraryFirst
}
} catch ( e : Exception ) {
withUIContext { view ?. onSetAsCoverResult ( SetAsCoverResult . Error ) }
SetAsCoverResult . Error
}
eventChannel . send ( Event . SetCoverResult ( result ) )
}
}
@ -842,8 +800,8 @@ class ReaderPresenter(
val trackManager = Injekt . get < TrackManager > ( )
val context = Injekt . get < Application > ( )
coroutine Scope. launchNonCancellable {
getTracks . await ( manga . id !! )
viewModel Scope. launchNonCancellable {
getTracks . await ( manga . id )
. mapNotNull { track ->
val service = trackManager . getService ( track . syncId )
if ( service != null && service . isLogged && chapterRead > track . lastChapterRead ) {
@ -882,8 +840,8 @@ class ReaderPresenter(
if ( ! chapter . chapter . read ) return
val manga = manga ?: return
coroutine Scope. launchNonCancellable {
downloadManager . enqueueChaptersToDelete ( listOf ( chapter . chapter . toDomainChapter ( ) !! ) , manga .toDomainManga ( ) !! )
viewModel Scope. launchNonCancellable {
downloadManager . enqueueChaptersToDelete ( listOf ( chapter . chapter . toDomainChapter ( ) !! ) , manga )
}
}
@ -892,34 +850,25 @@ class ReaderPresenter(
* are ignored .
* /
private fun deletePendingChapters ( ) {
coroutine Scope. launchNonCancellable {
viewModel Scope. launchNonCancellable {
downloadManager . deletePendingChapters ( )
}
}
// We're trying to avoid using Rx, so we "undeprecate" this
@Suppress ( " DEPRECATION " )
override fun getView ( ) : ReaderActivity ? {
return super . getView ( )
}
data class State (
val manga : Manga ? = null ,
val viewerChapters : ViewerChapters ? = null ,
val isLoadingAdjacentChapter : Boolean = false ,
)
/ * *
* Subscribes an observable with [ deliverFirst ] and adds it to the presenter ' s lifecycle
* subscription list .
*
* @param onNext function to execute when the observable emits an item .
* @param onError function to execute when the observable throws an error .
* /
private fun < T > Observable < T > . subscribeFirst ( onNext : ( ReaderActivity , T ) -> Unit , onError : ( ( ReaderActivity , Throwable ) -> Unit ) = { _ , _ -> } ) = compose ( deliverFirst < T > ( ) ) . subscribe ( split ( onNext , onError ) ) . apply { add ( this ) }
sealed class Event {
object ReloadViewerChapters : Event ( )
data class SetOrientation ( val orientation : Int ) : Event ( )
data class SetCoverResult ( val result : SetAsCoverResult ) : Event ( )
/ * *
* Subscribes an observable with [ deliverLatestCache ] and adds it to the presenter ' s lifecycle
* subscription list .
*
* @param onNext function to execute when the observable emits an item .
* @param onError function to execute when the observable throws an error .
* /
private fun < T > Observable < T > . subscribeLatestCache ( onNext : ( ReaderActivity , T ) -> Unit , onError : ( ( ReaderActivity , Throwable ) -> Unit ) = { _ , _ -> } ) = compose ( deliverLatestCache < T > ( ) ) . subscribe ( split ( onNext , onError ) ) . apply { add ( this ) }
data class SavedImage ( val result : SaveImageResult ) : Event ( )
data class ShareImage ( val uri : Uri , val page : ReaderPage ) : Event ( )
}
companion object {
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8)