|
@@ -8,66 +8,127 @@ import eu.kanade.tachiyomi.data.database.models.History
|
|
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
|
|
-import eu.kanade.tachiyomi.data.download.model.Download
|
|
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
|
|
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
|
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
|
-import eu.kanade.tachiyomi.data.source.Source
|
|
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
|
|
import eu.kanade.tachiyomi.data.source.model.Page
|
|
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
|
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
|
+import eu.kanade.tachiyomi.util.RetryWithDelay
|
|
|
import eu.kanade.tachiyomi.util.SharedData
|
|
|
import rx.Observable
|
|
|
import rx.Subscription
|
|
|
import rx.android.schedulers.AndroidSchedulers
|
|
|
import rx.schedulers.Schedulers
|
|
|
-import rx.subjects.PublishSubject
|
|
|
-import timber.log.Timber
|
|
|
+import uy.kohesive.injekt.injectLazy
|
|
|
import java.io.File
|
|
|
import java.util.*
|
|
|
-import javax.inject.Inject
|
|
|
|
|
|
+/**
|
|
|
+ * Presenter of [ReaderActivity].
|
|
|
+ */
|
|
|
class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|
|
|
|
|
- @Inject lateinit var prefs: PreferencesHelper
|
|
|
- @Inject lateinit var db: DatabaseHelper
|
|
|
- @Inject lateinit var downloadManager: DownloadManager
|
|
|
- @Inject lateinit var syncManager: MangaSyncManager
|
|
|
- @Inject lateinit var sourceManager: SourceManager
|
|
|
- @Inject lateinit var chapterCache: ChapterCache
|
|
|
+ /**
|
|
|
+ * Preferences.
|
|
|
+ */
|
|
|
+ val prefs: PreferencesHelper by injectLazy()
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Database.
|
|
|
+ */
|
|
|
+ val db: DatabaseHelper by injectLazy()
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Download manager.
|
|
|
+ */
|
|
|
+ val downloadManager: DownloadManager by injectLazy()
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sync manager.
|
|
|
+ */
|
|
|
+ val syncManager: MangaSyncManager by injectLazy()
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Source manager.
|
|
|
+ */
|
|
|
+ val sourceManager: SourceManager by injectLazy()
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Chapter cache.
|
|
|
+ */
|
|
|
+ val chapterCache: ChapterCache by injectLazy()
|
|
|
|
|
|
+ /**
|
|
|
+ * Manga being read.
|
|
|
+ */
|
|
|
lateinit var manga: Manga
|
|
|
private set
|
|
|
|
|
|
- lateinit var chapter: Chapter
|
|
|
+ /**
|
|
|
+ * Active chapter.
|
|
|
+ */
|
|
|
+ lateinit var chapter: ReaderChapter
|
|
|
private set
|
|
|
|
|
|
- lateinit var source: Source
|
|
|
- private set
|
|
|
+ /**
|
|
|
+ * Previous chapter of the active.
|
|
|
+ */
|
|
|
+ private var prevChapter: ReaderChapter? = null
|
|
|
|
|
|
- var requestedPage: Int = 0
|
|
|
- var currentPage: Page? = null
|
|
|
- private var nextChapter: Chapter? = null
|
|
|
- private var previousChapter: Chapter? = null
|
|
|
- private var mangaSyncList: List<MangaSync>? = null
|
|
|
+ /**
|
|
|
+ * Next chapter of the active.
|
|
|
+ */
|
|
|
+ private var nextChapter: ReaderChapter? = null
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Source of the manga.
|
|
|
+ */
|
|
|
+ private val source by lazy { sourceManager.get(manga.source)!! }
|
|
|
|
|
|
- private val retryPageSubject by lazy { PublishSubject.create<Page>() }
|
|
|
- private val pageInitializerSubject by lazy { PublishSubject.create<Chapter>() }
|
|
|
+ /**
|
|
|
+ * Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
|
|
|
+ * time in a background thread to avoid blocking the UI.
|
|
|
+ */
|
|
|
+ private val chapterList by lazy {
|
|
|
+ val dbChapters = db.getChapters(manga).executeAsBlocking().map { it.toModel() }
|
|
|
+
|
|
|
+ val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
|
|
+ Manga.SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
|
|
+ Manga.SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
|
|
+ else -> throw NotImplementedError("Unknown sorting method")
|
|
|
+ }
|
|
|
+
|
|
|
+ dbChapters.sortedWith(Comparator<Chapter> { c1, c2 -> sortFunction(c1, c2) })
|
|
|
+ }
|
|
|
|
|
|
- val isSeamlessMode by lazy { prefs.seamlessMode() }
|
|
|
+ /**
|
|
|
+ * List of manga services linked to the active manga, or null if auto syncing is not enabled.
|
|
|
+ */
|
|
|
+ private var mangaSyncList: List<MangaSync>? = null
|
|
|
|
|
|
+ /**
|
|
|
+ * Chapter loader whose job is to obtain the chapter list and initialize every page.
|
|
|
+ */
|
|
|
+ private val loader by lazy { ChapterLoader(downloadManager, manga, source) }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Subscription for appending a chapter to the reader (seamless mode).
|
|
|
+ */
|
|
|
private var appenderSubscription: Subscription? = null
|
|
|
|
|
|
- private val PREPARE_READER = 1
|
|
|
- private val GET_PAGE_LIST = 2
|
|
|
- private val GET_ADJACENT_CHAPTERS = 3
|
|
|
- private val GET_MANGA_SYNC = 4
|
|
|
- private val PRELOAD_NEXT_CHAPTER = 5
|
|
|
+ /**
|
|
|
+ * Subscription for retrieving the adjacent chapters to the current one.
|
|
|
+ */
|
|
|
+ private var adjacentChaptersSubscription: Subscription? = null
|
|
|
|
|
|
- private val MANGA_KEY = "manga_key"
|
|
|
- private val CHAPTER_KEY = "chapter_key"
|
|
|
- private val PAGE_KEY = "page_key"
|
|
|
+ companion object {
|
|
|
+ /**
|
|
|
+ * Id of the restartable that loads the active chapter.
|
|
|
+ */
|
|
|
+ private const val LOAD_ACTIVE_CHAPTER = 1
|
|
|
+ }
|
|
|
|
|
|
override fun onCreate(savedState: Bundle?) {
|
|
|
super.onCreate(savedState)
|
|
@@ -75,306 +136,287 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|
|
if (savedState == null) {
|
|
|
val event = SharedData.get(ReaderEvent::class.java) ?: return
|
|
|
manga = event.manga
|
|
|
- chapter = event.chapter
|
|
|
+ chapter = event.chapter.toModel()
|
|
|
} else {
|
|
|
- manga = savedState.getSerializable(MANGA_KEY) as Manga
|
|
|
- chapter = savedState.getSerializable(CHAPTER_KEY) as Chapter
|
|
|
- requestedPage = savedState.getInt(PAGE_KEY)
|
|
|
+ manga = savedState.getSerializable(ReaderPresenter::manga.name) as Manga
|
|
|
+ chapter = savedState.getSerializable(ReaderPresenter::chapter.name) as ReaderChapter
|
|
|
}
|
|
|
|
|
|
- source = sourceManager.get(manga.source)!!
|
|
|
-
|
|
|
- initializeSubjects()
|
|
|
-
|
|
|
- restartableLatestCache(PREPARE_READER,
|
|
|
- { Observable.just(manga) },
|
|
|
- { view, manga -> view.onMangaOpen(manga) })
|
|
|
-
|
|
|
- startableLatestCache(GET_ADJACENT_CHAPTERS,
|
|
|
- { getAdjacentChaptersObservable() },
|
|
|
- { view, pair -> view.onAdjacentChapters(pair.first, pair.second) })
|
|
|
+ // Send the active manga to the view to initialize the reader.
|
|
|
+ Observable.just(manga)
|
|
|
+ .subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) })
|
|
|
|
|
|
- startable(PRELOAD_NEXT_CHAPTER,
|
|
|
- { getPreloadNextChapterObservable() },
|
|
|
- { },
|
|
|
- { error -> Timber.e("Error preloading chapter") })
|
|
|
-
|
|
|
-
|
|
|
- restartable(GET_MANGA_SYNC,
|
|
|
- { getMangaSyncObservable().subscribe() })
|
|
|
+ // Retrieve the sync list if auto syncing is enabled.
|
|
|
+ if (prefs.autoUpdateMangaSync()) {
|
|
|
+ add(db.getMangasSync(manga).asRxSingle()
|
|
|
+ .subscribe({ mangaSyncList = it }))
|
|
|
+ }
|
|
|
|
|
|
- restartableLatestCache(GET_PAGE_LIST,
|
|
|
- { getPageListObservable(chapter) },
|
|
|
- { view, chapter -> view.onChapterReady(manga, this.chapter, currentPage) },
|
|
|
+ restartableLatestCache(LOAD_ACTIVE_CHAPTER,
|
|
|
+ { loadChapterObservable(chapter) },
|
|
|
+ { view, chapter -> view.onChapterReady(this.chapter) },
|
|
|
{ view, error -> view.onChapterError(error) })
|
|
|
|
|
|
if (savedState == null) {
|
|
|
- start(PREPARE_READER)
|
|
|
loadChapter(chapter)
|
|
|
- if (prefs.autoUpdateMangaSync()) {
|
|
|
- start(GET_MANGA_SYNC)
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
override fun onSave(state: Bundle) {
|
|
|
+ chapter.requestedPage = chapter.last_page_read
|
|
|
onChapterLeft()
|
|
|
- state.putSerializable(MANGA_KEY, manga)
|
|
|
- state.putSerializable(CHAPTER_KEY, chapter)
|
|
|
- state.putSerializable(PAGE_KEY, currentPage?.pageNumber ?: 0)
|
|
|
+ state.putSerializable(ReaderPresenter::manga.name, manga)
|
|
|
+ state.putSerializable(ReaderPresenter::chapter.name, chapter)
|
|
|
super.onSave(state)
|
|
|
}
|
|
|
|
|
|
- private fun initializeSubjects() {
|
|
|
- // Listen for pages initialization events
|
|
|
- add(pageInitializerSubject.observeOn(Schedulers.io())
|
|
|
- .concatMap { ch ->
|
|
|
- val observable: Observable<Page>
|
|
|
- if (ch.isDownloaded) {
|
|
|
- val chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, ch)
|
|
|
- observable = Observable.from(ch.pages)
|
|
|
- .flatMap { downloadManager.getDownloadedImage(it, chapterDir) }
|
|
|
- } else {
|
|
|
- observable = source.let { source ->
|
|
|
- if (source is OnlineSource) {
|
|
|
- source.fetchAllImageUrlsFromPageList(ch.pages)
|
|
|
- .flatMap({ source.getCachedImage(it) }, 2)
|
|
|
- .doOnCompleted { source.savePageList(ch, ch.pages) }
|
|
|
- } else {
|
|
|
- Observable.from(ch.pages)
|
|
|
- .flatMap { source.fetchImage(it) }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- observable.doOnCompleted {
|
|
|
- if (!isSeamlessMode && chapter === ch) {
|
|
|
- preloadNextChapter()
|
|
|
- }
|
|
|
- }
|
|
|
- }.subscribe())
|
|
|
-
|
|
|
- // Listen por retry events
|
|
|
- add(retryPageSubject.observeOn(Schedulers.io())
|
|
|
- .flatMap { source.fetchImage(it) }
|
|
|
- .subscribe())
|
|
|
+ override fun onDestroy() {
|
|
|
+ loader.cleanup()
|
|
|
+ super.onDestroy()
|
|
|
}
|
|
|
|
|
|
- // Returns the page list of a chapter
|
|
|
- private fun getPageListObservable(chapter: Chapter): Observable<Chapter> {
|
|
|
- val observable: Observable<List<Page>> = if (chapter.isDownloaded)
|
|
|
- // Fetch the page list from disk
|
|
|
- Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!)
|
|
|
- else
|
|
|
- // Fetch the page list from cache or fallback to network
|
|
|
- source.fetchPageList(chapter)
|
|
|
- .subscribeOn(Schedulers.io())
|
|
|
- .observeOn(AndroidSchedulers.mainThread())
|
|
|
-
|
|
|
- return observable.map { pages ->
|
|
|
- for (page in pages) {
|
|
|
- page.chapter = chapter
|
|
|
- }
|
|
|
- chapter.pages = pages
|
|
|
- if (requestedPage >= -1 || currentPage == null) {
|
|
|
- if (requestedPage == -1) {
|
|
|
- currentPage = pages[pages.size - 1]
|
|
|
- } else {
|
|
|
- currentPage = pages[requestedPage]
|
|
|
- }
|
|
|
- }
|
|
|
- requestedPage = -2
|
|
|
- pageInitializerSubject.onNext(chapter)
|
|
|
- chapter
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Converts a chapter to a [ReaderChapter] if needed.
|
|
|
+ */
|
|
|
+ private fun Chapter.toModel(): ReaderChapter {
|
|
|
+ if (this is ReaderChapter) return this
|
|
|
+ return ReaderChapter(this)
|
|
|
}
|
|
|
|
|
|
- private fun getAdjacentChaptersObservable(): Observable<Pair<Chapter, Chapter>> {
|
|
|
- val strategy = getAdjacentChaptersStrategy()
|
|
|
- return Observable.zip(strategy.first, strategy.second) { prev, next -> Pair(prev, next) }
|
|
|
- .doOnNext { pair ->
|
|
|
- previousChapter = pair.first
|
|
|
- nextChapter = pair.second
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Returns an observable that loads the given chapter, discarding any previous work.
|
|
|
+ *
|
|
|
+ * @param chapter the now active chapter.
|
|
|
+ */
|
|
|
+ private fun loadChapterObservable(chapter: ReaderChapter): Observable<ReaderChapter> {
|
|
|
+ loader.restart()
|
|
|
+ return loader.loadChapter(chapter)
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
}
|
|
|
|
|
|
- private fun getAdjacentChaptersStrategy() = when (manga.sorting) {
|
|
|
- Manga.SORTING_NUMBER -> Pair(
|
|
|
- db.getPreviousChapter(chapter).asRxObservable().take(1),
|
|
|
- db.getNextChapter(chapter).asRxObservable().take(1))
|
|
|
- Manga.SORTING_SOURCE -> Pair(
|
|
|
- db.getPreviousChapterBySource(chapter).asRxObservable().take(1),
|
|
|
- db.getNextChapterBySource(chapter).asRxObservable().take(1))
|
|
|
- else -> throw AssertionError("Unknown sorting method")
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Obtains the adjacent chapters of the given one in a background thread, and notifies the view
|
|
|
+ * when they are known.
|
|
|
+ *
|
|
|
+ * @param chapter the current active chapter.
|
|
|
+ */
|
|
|
+ private fun getAdjacentChapters(chapter: ReaderChapter) {
|
|
|
+ // Keep only one subscription
|
|
|
+ adjacentChaptersSubscription?.let { remove(it) }
|
|
|
|
|
|
- // Preload the first pages of the next chapter. Only for non seamless mode
|
|
|
- private fun getPreloadNextChapterObservable(): Observable<Page> {
|
|
|
- val nextChapter = nextChapter ?: return Observable.error(Exception("No next chapter"))
|
|
|
- return source.fetchPageList(nextChapter)
|
|
|
- .flatMap { pages ->
|
|
|
- nextChapter.pages = pages
|
|
|
- val pagesToPreload = Math.min(pages.size, 5)
|
|
|
- Observable.from(pages).take(pagesToPreload)
|
|
|
+ adjacentChaptersSubscription = Observable
|
|
|
+ .fromCallable { getAdjacentChaptersStrategy(chapter) }
|
|
|
+ .doOnNext { pair ->
|
|
|
+ prevChapter = pair.first
|
|
|
+ nextChapter = pair.second
|
|
|
}
|
|
|
- // Preload up to 5 images
|
|
|
- .concatMap { source.fetchImage(it) }
|
|
|
.subscribeOn(Schedulers.io())
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
- .doOnCompleted { stopPreloadingNextChapter() }
|
|
|
+ .subscribeLatestCache({ view, pair ->
|
|
|
+ view.onAdjacentChapters(pair.first, pair.second)
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- private fun getMangaSyncObservable(): Observable<List<MangaSync>> {
|
|
|
- return db.getMangasSync(manga).asRxObservable()
|
|
|
- .take(1)
|
|
|
- .doOnNext { mangaSyncList = it }
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Returns the previous and next chapters of the given one in a [Pair] according to the sorting
|
|
|
+ * strategy set for the manga.
|
|
|
+ *
|
|
|
+ * @param chapter the current active chapter.
|
|
|
+ */
|
|
|
+ private fun getAdjacentChaptersStrategy(chapter: ReaderChapter) = when (manga.sorting) {
|
|
|
+ Manga.SORTING_SOURCE -> {
|
|
|
+ val currChapterIndex = chapterList.indexOfFirst { chapter.id == it.id }
|
|
|
+ val nextChapter = chapterList.getOrNull(currChapterIndex + 1)
|
|
|
+ val prevChapter = chapterList.getOrNull(currChapterIndex - 1)
|
|
|
+ Pair(prevChapter, nextChapter)
|
|
|
+ }
|
|
|
+ Manga.SORTING_NUMBER -> {
|
|
|
+ val currChapterIndex = chapterList.indexOfFirst { chapter.id == it.id }
|
|
|
+ val chapterNumber = chapter.chapter_number
|
|
|
+
|
|
|
+ var prevChapter: ReaderChapter? = null
|
|
|
+ for (i in (currChapterIndex - 1) downTo 0) {
|
|
|
+ val c = chapterList[i]
|
|
|
+ if (c.chapter_number < chapterNumber && c.chapter_number >= chapterNumber - 1) {
|
|
|
+ prevChapter = c
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // Loads the given chapter
|
|
|
- private fun loadChapter(chapter: Chapter, requestedPage: Int = 0) {
|
|
|
- if (isSeamlessMode) {
|
|
|
- if (appenderSubscription != null)
|
|
|
- remove(appenderSubscription)
|
|
|
- } else {
|
|
|
- stopPreloadingNextChapter()
|
|
|
+ var nextChapter: ReaderChapter? = null
|
|
|
+ for (i in (currChapterIndex + 1) until chapterList.size) {
|
|
|
+ val c = chapterList[i]
|
|
|
+ if (c.chapter_number > chapterNumber && c.chapter_number <= chapterNumber + 1) {
|
|
|
+ nextChapter = c
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Pair(prevChapter, nextChapter)
|
|
|
}
|
|
|
+ else -> throw NotImplementedError("Unknown sorting method")
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Loads the given chapter and sets it as the active one. This method also accepts a requested
|
|
|
+ * page, which will be set as active when it's displayed in the view.
|
|
|
+ *
|
|
|
+ * @param chapter the chapter to load.
|
|
|
+ * @param requestedPage the requested page from the view.
|
|
|
+ */
|
|
|
+ private fun loadChapter(chapter: ReaderChapter, requestedPage: Int = 0) {
|
|
|
+ // Cleanup any append.
|
|
|
+ appenderSubscription?.let { remove(it) }
|
|
|
|
|
|
this.chapter = chapter
|
|
|
- chapter.status = if (isChapterDownloaded(chapter)) Download.DOWNLOADED else Download.NOT_DOWNLOADED
|
|
|
|
|
|
// If the chapter is partially read, set the starting page to the last the user read
|
|
|
- if (!chapter.read && chapter.last_page_read != 0)
|
|
|
- this.requestedPage = chapter.last_page_read
|
|
|
- else
|
|
|
- this.requestedPage = requestedPage
|
|
|
+ // otherwise use the requested page.
|
|
|
+ chapter.requestedPage = if (!chapter.read) chapter.last_page_read else requestedPage
|
|
|
|
|
|
// Reset next and previous chapter. They have to be fetched again
|
|
|
nextChapter = null
|
|
|
- previousChapter = null
|
|
|
+ prevChapter = null
|
|
|
|
|
|
- start(GET_PAGE_LIST)
|
|
|
- start(GET_ADJACENT_CHAPTERS)
|
|
|
+ start(LOAD_ACTIVE_CHAPTER)
|
|
|
+ getAdjacentChapters(chapter)
|
|
|
}
|
|
|
|
|
|
- fun setActiveChapter(chapter: Chapter) {
|
|
|
+ /**
|
|
|
+ * Changes the active chapter, but doesn't load anything. Called when changing chapters from
|
|
|
+ * the reader with the seamless mode.
|
|
|
+ *
|
|
|
+ * @param chapter the chapter to set as active.
|
|
|
+ */
|
|
|
+ fun setActiveChapter(chapter: ReaderChapter) {
|
|
|
onChapterLeft()
|
|
|
this.chapter = chapter
|
|
|
nextChapter = null
|
|
|
- previousChapter = null
|
|
|
- start(GET_ADJACENT_CHAPTERS)
|
|
|
+ prevChapter = null
|
|
|
+ getAdjacentChapters(chapter)
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Appends the next chapter to the reader, if possible.
|
|
|
+ */
|
|
|
fun appendNextChapter() {
|
|
|
- if (nextChapter == null)
|
|
|
- return
|
|
|
-
|
|
|
- if (appenderSubscription != null)
|
|
|
- remove(appenderSubscription)
|
|
|
-
|
|
|
- nextChapter?.let {
|
|
|
- if (appenderSubscription != null)
|
|
|
- remove(appenderSubscription)
|
|
|
-
|
|
|
- it.status = if (isChapterDownloaded(it)) Download.DOWNLOADED else Download.NOT_DOWNLOADED
|
|
|
+ appenderSubscription?.let { remove(it) }
|
|
|
|
|
|
- appenderSubscription = getPageListObservable(it).subscribeOn(Schedulers.io())
|
|
|
- .observeOn(AndroidSchedulers.mainThread())
|
|
|
- .compose(deliverLatestCache<Chapter>())
|
|
|
- .subscribe(split({ view, chapter ->
|
|
|
- view.onAppendChapter(chapter)
|
|
|
- }, { view, error ->
|
|
|
- view.onChapterAppendError()
|
|
|
- }))
|
|
|
+ val nextChapter = nextChapter ?: return
|
|
|
|
|
|
- add(appenderSubscription)
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- // Check whether the given chapter is downloaded
|
|
|
- fun isChapterDownloaded(chapter: Chapter): Boolean {
|
|
|
- return downloadManager.isChapterDownloaded(source, manga, chapter)
|
|
|
+ appenderSubscription = loader.loadChapter(nextChapter)
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
+ .retryWhen(RetryWithDelay(1, { 3000 }))
|
|
|
+ .observeOn(AndroidSchedulers.mainThread())
|
|
|
+ .subscribeLatestCache({ view, chapter ->
|
|
|
+ view.onAppendChapter(chapter)
|
|
|
+ }, { view, error ->
|
|
|
+ view.onChapterAppendError()
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Retries a page that failed to load due to network error or corruption.
|
|
|
+ *
|
|
|
+ * @param page the page that failed.
|
|
|
+ */
|
|
|
fun retryPage(page: Page?) {
|
|
|
- if (page != null) {
|
|
|
+ if (page != null && source is OnlineSource) {
|
|
|
page.status = Page.QUEUE
|
|
|
if (page.imagePath != null) {
|
|
|
val file = File(page.imagePath)
|
|
|
chapterCache.removeFileFromCache(file.name)
|
|
|
}
|
|
|
- retryPageSubject.onNext(page)
|
|
|
+ loader.retryPage(page)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Called before loading another chapter or leaving the reader. It allows to do operations
|
|
|
- // over the chapter read like saving progress
|
|
|
+ /**
|
|
|
+ * Called before loading another chapter or leaving the reader. It allows to do operations
|
|
|
+ * over the chapter read like saving progress
|
|
|
+ */
|
|
|
fun onChapterLeft() {
|
|
|
val pages = chapter.pages ?: return
|
|
|
|
|
|
- // Get the last page read
|
|
|
- var activePageNumber = chapter.last_page_read
|
|
|
+ // Reference these locally because they are needed later from another thread.
|
|
|
+ val chapter = chapter
|
|
|
+ val prevChapter = prevChapter
|
|
|
|
|
|
- // Just in case, avoid out of index exceptions
|
|
|
- if (activePageNumber >= pages.size) {
|
|
|
- activePageNumber = pages.size - 1
|
|
|
- }
|
|
|
- val activePage = pages[activePageNumber]
|
|
|
+ Observable
|
|
|
+ .fromCallable {
|
|
|
+ if (!chapter.isDownloaded) {
|
|
|
+ source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
|
|
|
+ }
|
|
|
|
|
|
- // Cache current page list progress for online chapters to allow a faster reopen
|
|
|
- if (!chapter.isDownloaded) {
|
|
|
- source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
|
|
|
- }
|
|
|
+ // Cache current page list progress for online chapters to allow a faster reopen
|
|
|
+ if (chapter.read) {
|
|
|
+ // Check if remove after read is selected by user
|
|
|
+ if (prefs.removeAfterRead()) {
|
|
|
+ if (prefs.removeAfterReadPrevious() ) {
|
|
|
+ if (prevChapter != null) {
|
|
|
+ deleteChapter(prevChapter, manga)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ deleteChapter(chapter, manga)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // Save current progress of the chapter. Mark as read if the chapter is finished
|
|
|
- if (activePage.isLastPage) {
|
|
|
- chapter.read = true
|
|
|
+ db.updateChapterProgress(chapter).executeAsBlocking()
|
|
|
|
|
|
- // Check if remove after read is selected by user
|
|
|
- if (prefs.removeAfterRead()) {
|
|
|
- if (prefs.removeAfterReadPrevious() ) {
|
|
|
- if (previousChapter != null) {
|
|
|
- deleteChapter(previousChapter!!, manga)
|
|
|
- }
|
|
|
- } else {
|
|
|
- deleteChapter(chapter, manga)
|
|
|
+ val history = History.create(chapter).apply { last_read = Date().time }
|
|
|
+ db.updateHistoryLastRead(history).executeAsBlocking()
|
|
|
}
|
|
|
- }
|
|
|
- }
|
|
|
- db.updateChapterProgress(chapter).asRxObservable().subscribe()
|
|
|
- // Update last read data
|
|
|
- db.updateHistoryLastRead(History.create(chapter)
|
|
|
- .apply { last_read = Date().time })
|
|
|
- .asRxObservable()
|
|
|
- .doOnError { Timber.e(it.message) }
|
|
|
+ .subscribeOn(Schedulers.io())
|
|
|
.subscribe()
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Called when the active page changes in the reader.
|
|
|
+ *
|
|
|
+ * @param page the active page
|
|
|
+ */
|
|
|
+ fun onPageChanged(page: Page) {
|
|
|
+ val chapter = page.chapter
|
|
|
+ chapter.last_page_read = page.pageNumber
|
|
|
+ if (chapter.pages!!.last() === page) {
|
|
|
+ chapter.read = true
|
|
|
+ }
|
|
|
+ if (!chapter.isDownloaded && page.status == Page.QUEUE) {
|
|
|
+ loader.loadPriorizedPage(page)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Delete selected chapter
|
|
|
+ *
|
|
|
* @param chapter chapter that is selected
|
|
|
- * *
|
|
|
* @param manga manga that belongs to chapter
|
|
|
*/
|
|
|
- fun deleteChapter(chapter: Chapter, manga: Manga) {
|
|
|
- val source = sourceManager.get(manga.source)!!
|
|
|
+ fun deleteChapter(chapter: ReaderChapter, manga: Manga) {
|
|
|
+ chapter.isDownloaded = false
|
|
|
+ chapter.pages?.forEach { it.status == Page.QUEUE }
|
|
|
downloadManager.deleteChapter(source, manga, chapter)
|
|
|
}
|
|
|
|
|
|
- // If the current chapter has been read, we check with this one
|
|
|
- // If not, we check if the previous chapter has been read
|
|
|
- // We know the chapter we have to check, but we don't know yet if an update is required.
|
|
|
- // This boolean is used to return 0 if no update is required
|
|
|
+ /**
|
|
|
+ * Returns the chapter to be marked as last read in sync services or 0 if no update required.
|
|
|
+ */
|
|
|
fun getMangaSyncChapterToUpdate(): Int {
|
|
|
if (chapter.pages == null || mangaSyncList == null || mangaSyncList!!.isEmpty())
|
|
|
return 0
|
|
|
|
|
|
var lastChapterReadLocal = 0
|
|
|
+
|
|
|
+ // If the current chapter has been read, we check with this one
|
|
|
if (chapter.read)
|
|
|
lastChapterReadLocal = Math.floor(chapter.chapter_number.toDouble()).toInt()
|
|
|
- else if (previousChapter != null && previousChapter!!.read)
|
|
|
- lastChapterReadLocal = Math.floor(previousChapter!!.chapter_number.toDouble()).toInt()
|
|
|
+ // If not, we check if the previous chapter has been read
|
|
|
+ else if (prevChapter != null && prevChapter!!.read)
|
|
|
+ lastChapterReadLocal = Math.floor(prevChapter!!.chapter_number.toDouble()).toInt()
|
|
|
+
|
|
|
+ // We know the chapter we have to check, but we don't know yet if an update is required.
|
|
|
+ // This boolean is used to return 0 if no update is required
|
|
|
var hasToUpdate = false
|
|
|
|
|
|
for (mangaSync in mangaSyncList!!) {
|
|
@@ -387,6 +429,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|
|
return if (hasToUpdate) lastChapterReadLocal else 0
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Starts the service that updates the last chapter read in sync services
|
|
|
+ */
|
|
|
fun updateMangaSyncLastChapterRead() {
|
|
|
for (mangaSync in mangaSyncList ?: emptyList()) {
|
|
|
val service = syncManager.getService(mangaSync.sync_id) ?: continue
|
|
@@ -396,6 +441,11 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Loads the next chapter.
|
|
|
+ *
|
|
|
+ * @return true if the next chapter is being loaded, false if there is no next chapter.
|
|
|
+ */
|
|
|
fun loadNextChapter(): Boolean {
|
|
|
nextChapter?.let {
|
|
|
onChapterLeft()
|
|
@@ -405,44 +455,42 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Loads the next chapter.
|
|
|
+ *
|
|
|
+ * @return true if the previous chapter is being loaded, false if there is no previous chapter.
|
|
|
+ */
|
|
|
fun loadPreviousChapter(): Boolean {
|
|
|
- previousChapter?.let {
|
|
|
+ prevChapter?.let {
|
|
|
onChapterLeft()
|
|
|
- loadChapter(it, 0)
|
|
|
+ loadChapter(it, if (it.read) -1 else 0)
|
|
|
return true
|
|
|
}
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Returns true if there's a next chapter.
|
|
|
+ */
|
|
|
fun hasNextChapter(): Boolean {
|
|
|
return nextChapter != null
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Returns true if there's a previous chapter.
|
|
|
+ */
|
|
|
fun hasPreviousChapter(): Boolean {
|
|
|
- return previousChapter != null
|
|
|
- }
|
|
|
-
|
|
|
- private fun preloadNextChapter() {
|
|
|
- nextChapter?.let {
|
|
|
- if (!isChapterDownloaded(it)) {
|
|
|
- start(PRELOAD_NEXT_CHAPTER)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private fun stopPreloadingNextChapter() {
|
|
|
- if (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) {
|
|
|
- stop(PRELOAD_NEXT_CHAPTER)
|
|
|
- nextChapter?.let { chapter ->
|
|
|
- if (chapter.pages != null) {
|
|
|
- source.let { if (it is OnlineSource) it.savePageList(chapter, chapter.pages) }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ return prevChapter != null
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Updates the viewer for this manga.
|
|
|
+ *
|
|
|
+ * @param viewer the id of the viewer to set.
|
|
|
+ */
|
|
|
fun updateMangaViewer(viewer: Int) {
|
|
|
manga.viewer = viewer
|
|
|
db.insertManga(manga).executeAsBlocking()
|
|
|
}
|
|
|
+
|
|
|
}
|