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