Browse Source

Add chapter loader, drop non seamless mode

len 8 years ago
parent
commit
658860fdff

+ 0 - 2
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt

@@ -54,8 +54,6 @@ class PreferenceKeys(context: Context) {
 
     val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
 
-    val seamlessMode = context.getString(R.string.pref_seamless_mode_key)
-
     val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
 
     val enabledLanguages = context.getString(R.string.pref_source_languages)

+ 0 - 2
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt

@@ -101,8 +101,6 @@ class PreferencesHelper(private val context: Context) {
 
     fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
 
-    fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
-
     fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
 
     fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN"))

+ 138 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt

@@ -0,0 +1,138 @@
+package eu.kanade.tachiyomi.ui.reader
+
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.download.DownloadManager
+import eu.kanade.tachiyomi.data.source.Source
+import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.util.plusAssign
+import rx.Observable
+import rx.schedulers.Schedulers
+import rx.subscriptions.CompositeSubscription
+import timber.log.Timber
+import java.util.concurrent.PriorityBlockingQueue
+import java.util.concurrent.atomic.AtomicInteger
+
+class ChapterLoader(
+        private val downloadManager: DownloadManager,
+        private val manga: Manga,
+        private val source: Source
+) {
+
+    private val queue = PriorityBlockingQueue<PriorityPage>()
+    private val subscriptions = CompositeSubscription()
+
+    fun init() {
+        prepareOnlineReading()
+    }
+
+    fun restart() {
+        cleanup()
+        init()
+    }
+
+    fun cleanup() {
+        subscriptions.clear()
+        queue.clear()
+    }
+
+    private fun prepareOnlineReading() {
+        subscriptions += Observable.defer { Observable.just(queue.take().page) }
+                .filter { it.status == Page.QUEUE }
+                .concatMap { source.fetchImage(it) }
+                .repeat()
+                .subscribeOn(Schedulers.io())
+                .subscribe({
+                }, {
+                    if (it !is InterruptedException) {
+                        Timber.e(it, it.message)
+                    }
+                })
+    }
+
+    fun loadChapter(chapter: ReaderChapter) = Observable.just(chapter)
+            .flatMap {
+                if (chapter.pages == null)
+                    retrievePageList(chapter)
+                else
+                    Observable.just(chapter.pages!!)
+            }
+            .doOnNext { pages ->
+                // Now that the number of pages is known, fix the requested page if the last one
+                // was requested.
+                if (chapter.requestedPage == -1) {
+                    chapter.requestedPage = pages.lastIndex
+                }
+
+                loadPages(chapter)
+            }
+            .map { chapter }
+
+    private fun retrievePageList(chapter: ReaderChapter) = Observable.just(chapter)
+            .flatMap {
+                // Check if the chapter is downloaded.
+                chapter.isDownloaded = downloadManager.isChapterDownloaded(source, manga, chapter)
+
+                // Fetch the page list from disk.
+                if (chapter.isDownloaded)
+                    Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!)
+                // Fetch the page list from cache or fallback to network
+                else
+                    source.fetchPageList(chapter)
+            }
+            .doOnNext { pages ->
+                chapter.pages = pages
+                pages.forEach { it.chapter = chapter }
+            }
+
+    private fun loadPages(chapter: ReaderChapter) {
+        if (chapter.isDownloaded) {
+            loadDownloadedPages(chapter)
+        } else {
+            loadOnlinePages(chapter)
+        }
+    }
+
+    private fun loadDownloadedPages(chapter: ReaderChapter) {
+        val chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter)
+        subscriptions += Observable.from(chapter.pages!!)
+                .flatMap { downloadManager.getDownloadedImage(it, chapterDir) }
+                .subscribeOn(Schedulers.io())
+                .subscribe()
+    }
+
+    private fun loadOnlinePages(chapter: ReaderChapter) {
+        chapter.pages?.let { pages ->
+            val startPage = chapter.requestedPage
+            val pagesToLoad = if (startPage == 0)
+                pages
+            else
+                pages.drop(startPage)
+
+            pagesToLoad.forEach { queue.offer(PriorityPage(it, 0)) }
+        }
+    }
+
+    fun loadPriorizedPage(page: Page) {
+        queue.offer(PriorityPage(page, 1))
+    }
+
+    fun retryPage(page: Page) {
+        queue.offer(PriorityPage(page, 2))
+    }
+
+    private data class PriorityPage(val page: Page, val priority: Int): Comparable<PriorityPage> {
+
+        companion object {
+            private val idGenerator = AtomicInteger()
+        }
+
+        private val identifier = idGenerator.incrementAndGet()
+
+        override fun compareTo(other: PriorityPage): Int {
+            val p = other.priority.compareTo(priority)
+            return if (p != 0) p else identifier.compareTo(other.identifier)
+        }
+
+    }
+
+}

+ 12 - 19
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

@@ -20,7 +20,6 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.data.preference.getOrDefault
-import eu.kanade.tachiyomi.data.source.model.Page
 import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
 import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
 import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
@@ -116,16 +115,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
         setSystemUiVisibility()
     }
 
-    override fun onPause() {
-        viewer?.let {
-            val activePage = it.getActivePage()
-            if (activePage != null) {
-                presenter.currentPage = activePage
-            }
-        }
-        super.onPause()
-    }
-
     override fun onDestroy() {
         subscriptions.unsubscribe()
         popupMenu?.dismiss()
@@ -230,6 +219,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
         // Ignore
     }
 
+    /**
+     * Called from the presenter at startup, allowing to prepare the selected reader.
+     */
     fun onMangaOpen(manga: Manga) {
         if (viewer == null) {
             viewer = getOrCreateViewer(manga)
@@ -243,22 +235,23 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
         please_wait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long))
     }
 
-    fun onChapterReady(manga: Manga, chapter: Chapter, currentPage: Page?) {
+    fun onChapterReady(chapter: ReaderChapter) {
         please_wait.visibility = View.GONE
-        val activePage = currentPage ?: chapter.pages.last()
+        val pages = chapter.pages ?: run { onChapterError(Exception("Null pages")); return }
+        val activePage = pages.getOrElse(chapter.requestedPage) { pages.first() }
 
         viewer?.onPageListReady(chapter, activePage)
         setActiveChapter(chapter, activePage.pageNumber)
     }
 
-    fun onEnterChapter(chapter: Chapter, currentPage: Int) {
-        val activePage = if (currentPage == -1) chapter.pages.lastIndex else currentPage
+    fun onEnterChapter(chapter: ReaderChapter, currentPage: Int) {
+        val activePage = if (currentPage == -1) chapter.pages!!.lastIndex else currentPage
         presenter.setActiveChapter(chapter)
         setActiveChapter(chapter, activePage)
     }
 
-    fun setActiveChapter(chapter: Chapter, currentPage: Int) {
-        val numPages = chapter.pages.size
+    fun setActiveChapter(chapter: ReaderChapter, currentPage: Int) {
+        val numPages = chapter.pages!!.size
         if (page_seekbar.rotation != 180f) {
             right_page_text.text = "$numPages"
             left_page_text.text = "${currentPage + 1}"
@@ -275,7 +268,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
             chapter.name)
     }
 
-    fun onAppendChapter(chapter: Chapter) {
+    fun onAppendChapter(chapter: ReaderChapter) {
         viewer?.onPageListAppendReady(chapter)
     }
 
@@ -324,7 +317,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
         viewer?.let {
             val activePage = it.getActivePage()
             if (activePage != null) {
-                val requestedPage = activePage.chapter.pages[pageIndex]
+                val requestedPage = activePage.chapter.pages!![pageIndex]
                 it.setActivePage(requestedPage)
             }
 

+ 13 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderChapter.kt

@@ -0,0 +1,13 @@
+package eu.kanade.tachiyomi.ui.reader
+
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.source.model.Page
+
+class ReaderChapter(c: Chapter) : Chapter by c {
+
+    @Transient var pages: List<Page>? = null
+
+    var isDownloaded: Boolean = false
+
+    var requestedPage: Int = 0
+}

+ 313 - 265
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

@@ -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()
     }
+
 }

+ 22 - 23
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt

@@ -1,11 +1,11 @@
 package eu.kanade.tachiyomi.ui.reader.viewer.base
 
 import com.davemorrissey.labs.subscaleview.decoder.*
-import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.preference.getOrDefault
 import eu.kanade.tachiyomi.data.source.model.Page
 import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.ui.reader.ReaderChapter
 import java.util.*
 
 /**
@@ -29,7 +29,7 @@ abstract class BaseReader : BaseFragment() {
     /**
      * List of chapters added in the reader.
      */
-    private var chapters = ArrayList<Chapter>()
+    private val chapters = ArrayList<ReaderChapter>()
 
     /**
      * List of pages added in the reader. It can contain pages from more than one chapter.
@@ -72,7 +72,7 @@ abstract class BaseReader : BaseFragment() {
     fun updatePageNumber() {
         val activePage = getActivePage()
         if (activePage != null) {
-            readerActivity.onPageChanged(activePage.pageNumber, activePage.chapter.pages.size)
+            readerActivity.onPageChanged(activePage.pageNumber, activePage.chapter.pages!!.size)
         }
     }
 
@@ -91,23 +91,22 @@ abstract class BaseReader : BaseFragment() {
     fun onPageChanged(position: Int) {
         val oldPage = pages[currentPage]
         val newPage = pages[position]
-        newPage.chapter.last_page_read = newPage.pageNumber
+        readerActivity.presenter.onPageChanged(newPage)
 
-        if (readerActivity.presenter.isSeamlessMode) {
-            val oldChapter = oldPage.chapter
-            val newChapter = newPage.chapter
+        val oldChapter = oldPage.chapter
+        val newChapter = newPage.chapter
 
-            // Active chapter has changed.
-            if (oldChapter.id != newChapter.id) {
-                readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber)
-            }
-            // Request next chapter only when the conditions are met.
-            if (pages.size - position < 5 && chapters.last().id == newChapter.id
-                    && readerActivity.presenter.hasNextChapter() && !hasRequestedNextChapter) {
-                hasRequestedNextChapter = true
-                readerActivity.presenter.appendNextChapter()
-            }
+        // Active chapter has changed.
+        if (oldChapter.id != newChapter.id) {
+            readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber)
         }
+        // Request next chapter only when the conditions are met.
+        if (pages.size - position < 5 && chapters.last().id == newChapter.id
+                && readerActivity.presenter.hasNextChapter() && !hasRequestedNextChapter) {
+            hasRequestedNextChapter = true
+            readerActivity.presenter.appendNextChapter()
+        }
+
         currentPage = position
         updatePageNumber()
     }
@@ -144,10 +143,10 @@ abstract class BaseReader : BaseFragment() {
      * @param chapter the chapter to set.
      * @param currentPage the initial page to display.
      */
-    fun onPageListReady(chapter: Chapter, currentPage: Page) {
+    fun onPageListReady(chapter: ReaderChapter, currentPage: Page) {
         if (!chapters.contains(chapter)) {
             // if we reset the loaded page we also need to reset the loaded chapters
-            chapters = ArrayList<Chapter>()
+            chapters.clear()
             chapters.add(chapter)
             pages = ArrayList(chapter.pages)
             onChapterSet(chapter, currentPage)
@@ -162,11 +161,11 @@ abstract class BaseReader : BaseFragment() {
      *
      * @param chapter the chapter to append.
      */
-    fun onPageListAppendReady(chapter: Chapter) {
+    fun onPageListAppendReady(chapter: ReaderChapter) {
         if (!chapters.contains(chapter)) {
             hasRequestedNextChapter = false
             chapters.add(chapter)
-            pages.addAll(chapter.pages)
+            pages.addAll(chapter.pages!!)
             onChapterAppended(chapter)
         }
     }
@@ -184,14 +183,14 @@ abstract class BaseReader : BaseFragment() {
      * @param chapter the chapter set.
      * @param currentPage the initial page to display.
      */
-    abstract fun onChapterSet(chapter: Chapter, currentPage: Page)
+    abstract fun onChapterSet(chapter: ReaderChapter, currentPage: Page)
 
     /**
      * Called when a chapter is appended in [BaseReader].
      *
      * @param chapter the chapter appended.
      */
-    abstract fun onChapterAppended(chapter: Chapter)
+    abstract fun onChapterAppended(chapter: ReaderChapter)
 
     /**
      * Moves pages forward. Implementations decide how to move (by a page, by some distance...).

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt

@@ -5,8 +5,8 @@ import android.view.MotionEvent
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.ui.reader.ReaderChapter
 import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
 import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
 import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader
@@ -181,7 +181,7 @@ abstract class PagerReader : BaseReader() {
      * @param chapter the chapter set.
      * @param currentPage the initial page to display.
      */
-    override fun onChapterSet(chapter: Chapter, currentPage: Page) {
+    override fun onChapterSet(chapter: ReaderChapter, currentPage: Page) {
         this.currentPage = getPageIndex(currentPage) // we might have a new page object
 
         // Make sure the view is already initialized.
@@ -195,7 +195,7 @@ abstract class PagerReader : BaseReader() {
      *
      * @param chapter the chapter appended.
      */
-    override fun onChapterAppended(chapter: Chapter) {
+    override fun onChapterAppended(chapter: ReaderChapter) {
         // Make sure the view is already initialized.
         if (view != null) {
             adapter.pages = pages

+ 5 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt

@@ -6,8 +6,8 @@ import android.view.*
 import android.view.GestureDetector.SimpleOnGestureListener
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.ui.reader.ReaderChapter
 import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
 import eu.kanade.tachiyomi.widget.PreCachingLayoutManager
 import rx.subscriptions.CompositeSubscription
@@ -147,7 +147,7 @@ class WebtoonReader : BaseReader() {
      * @param chapter the chapter set.
      * @param currentPage the initial page to display.
      */
-    override fun onChapterSet(chapter: Chapter, currentPage: Page) {
+    override fun onChapterSet(chapter: ReaderChapter, currentPage: Page) {
         // Restoring current page is not supported. It's getting weird scrolling jumps
         // this.currentPage = currentPage;
 
@@ -162,11 +162,11 @@ class WebtoonReader : BaseReader() {
      *
      * @param chapter the chapter appended.
      */
-    override fun onChapterAppended(chapter: Chapter) {
+    override fun onChapterAppended(chapter: ReaderChapter) {
         // Make sure the view is already initialized.
         if (view != null) {
-            val insertStart = pages.size - chapter.pages.size
-            adapter.notifyItemRangeInserted(insertStart, chapter.pages.size)
+            val insertStart = pages.size - chapter.pages!!.size
+            adapter.notifyItemRangeInserted(insertStart, chapter.pages!!.size)
         }
     }
 

+ 22 - 0
app/src/main/java/eu/kanade/tachiyomi/util/RetryWithDelay.kt

@@ -0,0 +1,22 @@
+package eu.kanade.tachiyomi.util
+
+import rx.Observable
+import rx.functions.Func1
+import java.util.concurrent.TimeUnit.MILLISECONDS
+
+class RetryWithDelay(
+        private val maxRetries: Int = 1,
+        private val retryStrategy: (Int) -> Int = { 1000 }
+) : Func1<Observable<out Throwable>, Observable<*>> {
+
+    private var retryCount = 0
+
+    override fun call(attempts: Observable<out Throwable>) = attempts.flatMap { error ->
+        val count = ++retryCount
+        if (count <= maxRetries) {
+            Observable.timer(retryStrategy(count).toLong(), MILLISECONDS)
+        } else {
+            Observable.error(error as Throwable)
+        }
+    }
+}

+ 0 - 1
app/src/main/res/values/keys.xml

@@ -30,7 +30,6 @@
     <string name="pref_custom_brightness_value_key">pref_custom_brightness_value_key</string>
     <string name="pref_reader_theme_key">pref_reader_theme_key</string>
     <string name="pref_image_decoder_key">pref_image_decoder_key</string>
-    <string name="pref_seamless_mode_key">pref_seamless_mode_key</string>
     <string name="pref_read_with_volume_keys_key">reader_volume_keys</string>
     <string name="pref_read_with_tapping_key">reader_tap</string>
     <string name="pref_reencode_key">reencode_image</string>

+ 0 - 1
app/src/main/res/values/strings.xml

@@ -101,7 +101,6 @@
     <string name="pref_enable_transitions">Enable transitions</string>
     <string name="pref_show_page_number">Show page number</string>
     <string name="pref_custom_brightness">Use custom brightness</string>
-    <string name="pref_seamless_mode">Seamless chapter transitions</string>
     <string name="pref_keep_screen_on">Keep screen on</string>
     <string name="pref_reader_navigation">Navigation</string>
     <string name="pref_read_with_volume_keys">Volume keys</string>

+ 0 - 5
app/src/main/res/xml/pref_reader.xml

@@ -75,11 +75,6 @@
         android:key="@string/pref_keep_screen_on_key"
         android:defaultValue="true" />
 
-    <SwitchPreferenceCompat
-        android:title="@string/pref_seamless_mode"
-        android:key="@string/pref_seamless_mode_key"
-        android:defaultValue="true" />
-
     <PreferenceCategory
         android:title="@string/pref_reader_navigation">