Add chapter loader, drop non seamless mode

This commit is contained in:
len 2016-06-14 15:15:31 +02:00
parent 21ba371a32
commit 658860fdff
13 changed files with 534 additions and 332 deletions

View File

@ -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)

View File

@ -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"))

View File

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

View File

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

View File

@ -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
}

View File

@ -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
/**
* 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<Chapter> { 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<MangaSync>? = null
private val retryPageSubject by lazy { PublishSubject.create<Page>() }
private val pageInitializerSubject by lazy { PublishSubject.create<Chapter>() }
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<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)!!
// 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<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
}
.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<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)
}
// 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<ReaderChapter> {
loader.restart()
return loader.loadChapter(chapter)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnCompleted { stopPreloadingNextChapter() }
}
private fun getMangaSyncObservable(): Observable<List<MangaSync>> {
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<Chapter>())
.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<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()
}
}

View File

@ -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...).

View File

@ -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

View File

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

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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">