From 658860fdffeea2ade221c3d7d23ad1e4ea87232b Mon Sep 17 00:00:00 2001 From: len Date: Tue, 14 Jun 2016 15:15:31 +0200 Subject: [PATCH] Add chapter loader, drop non seamless mode --- .../data/preference/PreferenceKeys.kt | 2 - .../data/preference/PreferencesHelper.kt | 2 - .../tachiyomi/ui/reader/ChapterLoader.kt | 138 ++++ .../tachiyomi/ui/reader/ReaderActivity.kt | 31 +- .../tachiyomi/ui/reader/ReaderChapter.kt | 13 + .../tachiyomi/ui/reader/ReaderPresenter.kt | 590 ++++++++++-------- .../ui/reader/viewer/base/BaseReader.kt | 45 +- .../ui/reader/viewer/pager/PagerReader.kt | 6 +- .../ui/reader/viewer/webtoon/WebtoonReader.kt | 10 +- .../kanade/tachiyomi/util/RetryWithDelay.kt | 22 + app/src/main/res/values/keys.xml | 1 - app/src/main/res/values/strings.xml | 1 - app/src/main/res/xml/pref_reader.xml | 5 - 13 files changed, 534 insertions(+), 332 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderChapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/RetryWithDelay.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 3d09756ff..c0eaffc84 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 4e31a5c04..ef2e2cce4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/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")) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ChapterLoader.kt new file mode 100644 index 000000000..225a48aa5 --- /dev/null +++ b/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() + 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 { + + 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) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index c59bc313b..7f6b27d0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/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() { 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() { // 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() { 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() { chapter.name) } - fun onAppendChapter(chapter: Chapter) { + fun onAppendChapter(chapter: ReaderChapter) { viewer?.onPageListAppendReady(chapter) } @@ -324,7 +317,7 @@ class ReaderActivity : BaseRxActivity() { viewer?.let { val activePage = it.getActivePage() if (activePage != null) { - val requestedPage = activePage.chapter.pages[pageIndex] + val requestedPage = activePage.chapter.pages!![pageIndex] it.setActivePage(requestedPage) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderChapter.kt new file mode 100644 index 000000000..56de9c972 --- /dev/null +++ b/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? = null + + var isDownloaded: Boolean = false + + var requestedPage: Int = 0 +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index a37593a6b..58c202c8f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/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() { - @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 + /** + * Next chapter of the active. + */ + private var nextChapter: ReaderChapter? = null + + /** + * Source of the manga. + */ + private val source by lazy { sourceManager.get(manga.source)!! } + + /** + * 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 { c1, c2 -> sortFunction(c1, c2) }) + } + + /** + * List of manga services linked to the active manga, or null if auto syncing is not enabled. + */ private var mangaSyncList: List? = null - private val retryPageSubject by lazy { PublishSubject.create() } - private val pageInitializerSubject by lazy { PublishSubject.create() } - - val isSeamlessMode by lazy { prefs.seamlessMode() } + /** + * 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() { 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)!! + // Send the active manga to the view to initialize the reader. + Observable.just(manga) + .subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) }) - initializeSubjects() + // Retrieve the sync list if auto syncing is enabled. + if (prefs.autoUpdateMangaSync()) { + add(db.getMangasSync(manga).asRxSingle() + .subscribe({ mangaSyncList = it })) + } - restartableLatestCache(PREPARE_READER, - { Observable.just(manga) }, - { view, manga -> view.onMangaOpen(manga) }) - - startableLatestCache(GET_ADJACENT_CHAPTERS, - { getAdjacentChaptersObservable() }, - { view, pair -> view.onAdjacentChapters(pair.first, pair.second) }) - - startable(PRELOAD_NEXT_CHAPTER, - { getPreloadNextChapterObservable() }, - { }, - { error -> Timber.e("Error preloading chapter") }) - - - restartable(GET_MANGA_SYNC, - { getMangaSyncObservable().subscribe() }) - - 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 - 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 { - val observable: Observable> = 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> { - val strategy = getAdjacentChaptersStrategy() - return Observable.zip(strategy.first, strategy.second) { prev, next -> Pair(prev, next) } - .doOnNext { pair -> - previousChapter = pair.first - nextChapter = pair.second - } - .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") - } - - // Preload the first pages of the next chapter. Only for non seamless mode - private fun getPreloadNextChapterObservable(): Observable { - 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) - } - // Preload up to 5 images - .concatMap { source.fetchImage(it) } + /** + * Returns an observable that loads the given chapter, discarding any previous work. + * + * @param chapter the now active chapter. + */ + private fun loadChapterObservable(chapter: ReaderChapter): Observable { + loader.restart() + return loader.loadChapter(chapter) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .doOnCompleted { stopPreloadingNextChapter() } } - private fun getMangaSyncObservable(): Observable> { - return db.getMangasSync(manga).asRxObservable() - .take(1) - .doOnNext { mangaSyncList = it } + /** + * 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) } + + adjacentChaptersSubscription = Observable + .fromCallable { getAdjacentChaptersStrategy(chapter) } + .doOnNext { pair -> + prevChapter = pair.first + nextChapter = pair.second + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache({ view, pair -> + view.onAdjacentChapters(pair.first, pair.second) + }) } - // Loads the given chapter - private fun loadChapter(chapter: Chapter, requestedPage: Int = 0) { - if (isSeamlessMode) { - if (appenderSubscription != null) - remove(appenderSubscription) - } else { - stopPreloadingNextChapter() + /** + * 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 + } + } + + 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 + appenderSubscription?.let { remove(it) } - if (appenderSubscription != null) - remove(appenderSubscription) + val nextChapter = nextChapter ?: return - nextChapter?.let { - if (appenderSubscription != null) - remove(appenderSubscription) - - it.status = if (isChapterDownloaded(it)) Download.DOWNLOADED else Download.NOT_DOWNLOADED - - appenderSubscription = getPageListObservable(it).subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(deliverLatestCache()) - .subscribe(split({ view, chapter -> - view.onAppendChapter(chapter) - }, { view, error -> - view.onChapterAppendError() - })) - - 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] - - // 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) } - } - - // Save current progress of the chapter. Mark as read if the chapter is finished - if (activePage.isLastPage) { - chapter.read = true - - // Check if remove after read is selected by user - if (prefs.removeAfterRead()) { - if (prefs.removeAfterReadPrevious() ) { - if (previousChapter != null) { - deleteChapter(previousChapter!!, manga) + Observable + .fromCallable { + if (!chapter.isDownloaded) { + source.let { if (it is OnlineSource) it.savePageList(chapter, pages) } } - } else { - deleteChapter(chapter, manga) + + // 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) + } + } + } + + db.updateChapterProgress(chapter).executeAsBlocking() + + 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() { 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() { } } + /** + * 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() { 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() } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt index 7897b563f..360696b62 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt +++ b/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() + private val chapters = ArrayList() /** * 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() + 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...). diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt index f49126cc1..f1c0e32a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt +++ b/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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt index 43adbb516..7565f8d5e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt +++ b/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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/RetryWithDelay.kt b/app/src/main/java/eu/kanade/tachiyomi/util/RetryWithDelay.kt new file mode 100644 index 000000000..237bfcbdc --- /dev/null +++ b/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<*>> { + + private var retryCount = 0 + + override fun call(attempts: Observable) = attempts.flatMap { error -> + val count = ++retryCount + if (count <= maxRetries) { + Observable.timer(retryStrategy(count).toLong(), MILLISECONDS) + } else { + Observable.error(error as Throwable) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 1d66c4250..9b0ba28e0 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -30,7 +30,6 @@ pref_custom_brightness_value_key pref_reader_theme_key pref_image_decoder_key - pref_seamless_mode_key reader_volume_keys reader_tap reencode_image diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de372754c..83aa3376b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -101,7 +101,6 @@ Enable transitions Show page number Use custom brightness - Seamless chapter transitions Keep screen on Navigation Volume keys diff --git a/app/src/main/res/xml/pref_reader.xml b/app/src/main/res/xml/pref_reader.xml index 900037e69..7a716c91f 100644 --- a/app/src/main/res/xml/pref_reader.xml +++ b/app/src/main/res/xml/pref_reader.xml @@ -75,11 +75,6 @@ android:key="@string/pref_keep_screen_on_key" android:defaultValue="true" /> - -