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 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 catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
val enabledLanguages = context.getString(R.string.pref_source_languages) 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 lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false) fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN")) 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.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault 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.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader 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.LeftToRightReader
@ -116,16 +115,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
setSystemUiVisibility() setSystemUiVisibility()
} }
override fun onPause() {
viewer?.let {
val activePage = it.getActivePage()
if (activePage != null) {
presenter.currentPage = activePage
}
}
super.onPause()
}
override fun onDestroy() { override fun onDestroy() {
subscriptions.unsubscribe() subscriptions.unsubscribe()
popupMenu?.dismiss() popupMenu?.dismiss()
@ -230,6 +219,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
// Ignore // Ignore
} }
/**
* Called from the presenter at startup, allowing to prepare the selected reader.
*/
fun onMangaOpen(manga: Manga) { fun onMangaOpen(manga: Manga) {
if (viewer == null) { if (viewer == null) {
viewer = getOrCreateViewer(manga) viewer = getOrCreateViewer(manga)
@ -243,22 +235,23 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
please_wait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long)) 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 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) viewer?.onPageListReady(chapter, activePage)
setActiveChapter(chapter, activePage.pageNumber) setActiveChapter(chapter, activePage.pageNumber)
} }
fun onEnterChapter(chapter: Chapter, currentPage: Int) { fun onEnterChapter(chapter: ReaderChapter, currentPage: Int) {
val activePage = if (currentPage == -1) chapter.pages.lastIndex else currentPage val activePage = if (currentPage == -1) chapter.pages!!.lastIndex else currentPage
presenter.setActiveChapter(chapter) presenter.setActiveChapter(chapter)
setActiveChapter(chapter, activePage) setActiveChapter(chapter, activePage)
} }
fun setActiveChapter(chapter: Chapter, currentPage: Int) { fun setActiveChapter(chapter: ReaderChapter, currentPage: Int) {
val numPages = chapter.pages.size val numPages = chapter.pages!!.size
if (page_seekbar.rotation != 180f) { if (page_seekbar.rotation != 180f) {
right_page_text.text = "$numPages" right_page_text.text = "$numPages"
left_page_text.text = "${currentPage + 1}" left_page_text.text = "${currentPage + 1}"
@ -275,7 +268,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
chapter.name) chapter.name)
} }
fun onAppendChapter(chapter: Chapter) { fun onAppendChapter(chapter: ReaderChapter) {
viewer?.onPageListAppendReady(chapter) viewer?.onPageListAppendReady(chapter)
} }
@ -324,7 +317,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
viewer?.let { viewer?.let {
val activePage = it.getActivePage() val activePage = it.getActivePage()
if (activePage != null) { if (activePage != null) {
val requestedPage = activePage.chapter.pages[pageIndex] val requestedPage = activePage.chapter.pages!![pageIndex]
it.setActivePage(requestedPage) 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.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.download.DownloadManager 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.MangaSyncManager
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.RetryWithDelay
import eu.kanade.tachiyomi.util.SharedData import eu.kanade.tachiyomi.util.SharedData
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subjects.PublishSubject import uy.kohesive.injekt.injectLazy
import timber.log.Timber
import java.io.File import java.io.File
import java.util.* import java.util.*
import javax.inject.Inject
/**
* Presenter of [ReaderActivity].
*/
class ReaderPresenter : BasePresenter<ReaderActivity>() { class ReaderPresenter : BasePresenter<ReaderActivity>() {
@Inject lateinit var prefs: PreferencesHelper /**
@Inject lateinit var db: DatabaseHelper * Preferences.
@Inject lateinit var downloadManager: DownloadManager */
@Inject lateinit var syncManager: MangaSyncManager val prefs: PreferencesHelper by injectLazy()
@Inject lateinit var sourceManager: SourceManager
@Inject lateinit var chapterCache: ChapterCache
/**
* 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 lateinit var manga: Manga
private set private set
lateinit var chapter: Chapter /**
* Active chapter.
*/
lateinit var chapter: ReaderChapter
private set 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 * Next chapter of the active.
private var nextChapter: Chapter? = null */
private var previousChapter: Chapter? = null 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 var mangaSyncList: List<MangaSync>? = null
private val retryPageSubject by lazy { PublishSubject.create<Page>() } /**
private val pageInitializerSubject by lazy { PublishSubject.create<Chapter>() } * Chapter loader whose job is to obtain the chapter list and initialize every page.
*/
val isSeamlessMode by lazy { prefs.seamlessMode() } 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 var appenderSubscription: Subscription? = null
private val PREPARE_READER = 1 /**
private val GET_PAGE_LIST = 2 * Subscription for retrieving the adjacent chapters to the current one.
private val GET_ADJACENT_CHAPTERS = 3 */
private val GET_MANGA_SYNC = 4 private var adjacentChaptersSubscription: Subscription? = null
private val PRELOAD_NEXT_CHAPTER = 5
private val MANGA_KEY = "manga_key" companion object {
private val CHAPTER_KEY = "chapter_key" /**
private val PAGE_KEY = "page_key" * Id of the restartable that loads the active chapter.
*/
private const val LOAD_ACTIVE_CHAPTER = 1
}
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@ -75,306 +136,287 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
if (savedState == null) { if (savedState == null) {
val event = SharedData.get(ReaderEvent::class.java) ?: return val event = SharedData.get(ReaderEvent::class.java) ?: return
manga = event.manga manga = event.manga
chapter = event.chapter chapter = event.chapter.toModel()
} else { } else {
manga = savedState.getSerializable(MANGA_KEY) as Manga manga = savedState.getSerializable(ReaderPresenter::manga.name) as Manga
chapter = savedState.getSerializable(CHAPTER_KEY) as Chapter chapter = savedState.getSerializable(ReaderPresenter::chapter.name) as ReaderChapter
requestedPage = savedState.getInt(PAGE_KEY)
} }
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, restartableLatestCache(LOAD_ACTIVE_CHAPTER,
{ Observable.just(manga) }, { loadChapterObservable(chapter) },
{ view, manga -> view.onMangaOpen(manga) }) { view, chapter -> view.onChapterReady(this.chapter) },
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) },
{ view, error -> view.onChapterError(error) }) { view, error -> view.onChapterError(error) })
if (savedState == null) { if (savedState == null) {
start(PREPARE_READER)
loadChapter(chapter) loadChapter(chapter)
if (prefs.autoUpdateMangaSync()) {
start(GET_MANGA_SYNC)
}
} }
} }
override fun onSave(state: Bundle) { override fun onSave(state: Bundle) {
chapter.requestedPage = chapter.last_page_read
onChapterLeft() onChapterLeft()
state.putSerializable(MANGA_KEY, manga) state.putSerializable(ReaderPresenter::manga.name, manga)
state.putSerializable(CHAPTER_KEY, chapter) state.putSerializable(ReaderPresenter::chapter.name, chapter)
state.putSerializable(PAGE_KEY, currentPage?.pageNumber ?: 0)
super.onSave(state) super.onSave(state)
} }
private fun initializeSubjects() { override fun onDestroy() {
// Listen for pages initialization events loader.cleanup()
add(pageInitializerSubject.observeOn(Schedulers.io()) super.onDestroy()
.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())
} }
// Returns the page list of a chapter /**
private fun getPageListObservable(chapter: Chapter): Observable<Chapter> { * Converts a chapter to a [ReaderChapter] if needed.
val observable: Observable<List<Page>> = if (chapter.isDownloaded) */
// Fetch the page list from disk private fun Chapter.toModel(): ReaderChapter {
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!) if (this is ReaderChapter) return this
else return ReaderChapter(this)
// 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
}
} }
private fun getAdjacentChaptersObservable(): Observable<Pair<Chapter, Chapter>> { /**
val strategy = getAdjacentChaptersStrategy() * Returns an observable that loads the given chapter, discarding any previous work.
return Observable.zip(strategy.first, strategy.second) { prev, next -> Pair(prev, next) } *
.doOnNext { pair -> * @param chapter the now active chapter.
previousChapter = pair.first */
nextChapter = pair.second private fun loadChapterObservable(chapter: ReaderChapter): Observable<ReaderChapter> {
} loader.restart()
.observeOn(AndroidSchedulers.mainThread()) return loader.loadChapter(chapter)
}
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) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnCompleted { stopPreloadingNextChapter() }
} }
private fun getMangaSyncObservable(): Observable<List<MangaSync>> { /**
return db.getMangasSync(manga).asRxObservable() * Obtains the adjacent chapters of the given one in a background thread, and notifies the view
.take(1) * when they are known.
.doOnNext { mangaSyncList = it } *
* @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) { * Returns the previous and next chapters of the given one in a [Pair] according to the sorting
if (isSeamlessMode) { * strategy set for the manga.
if (appenderSubscription != null) *
remove(appenderSubscription) * @param chapter the current active chapter.
} else { */
stopPreloadingNextChapter() 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 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 the chapter is partially read, set the starting page to the last the user read
if (!chapter.read && chapter.last_page_read != 0) // otherwise use the requested page.
this.requestedPage = chapter.last_page_read chapter.requestedPage = if (!chapter.read) chapter.last_page_read else requestedPage
else
this.requestedPage = requestedPage
// Reset next and previous chapter. They have to be fetched again // Reset next and previous chapter. They have to be fetched again
nextChapter = null nextChapter = null
previousChapter = null prevChapter = null
start(GET_PAGE_LIST) start(LOAD_ACTIVE_CHAPTER)
start(GET_ADJACENT_CHAPTERS) 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() onChapterLeft()
this.chapter = chapter this.chapter = chapter
nextChapter = null nextChapter = null
previousChapter = null prevChapter = null
start(GET_ADJACENT_CHAPTERS) getAdjacentChapters(chapter)
} }
/**
* Appends the next chapter to the reader, if possible.
*/
fun appendNextChapter() { fun appendNextChapter() {
if (nextChapter == null) appenderSubscription?.let { remove(it) }
return
if (appenderSubscription != null) val nextChapter = nextChapter ?: return
remove(appenderSubscription)
nextChapter?.let { appenderSubscription = loader.loadChapter(nextChapter)
if (appenderSubscription != null) .subscribeOn(Schedulers.io())
remove(appenderSubscription) .retryWhen(RetryWithDelay(1, { 3000 }))
.observeOn(AndroidSchedulers.mainThread())
it.status = if (isChapterDownloaded(it)) Download.DOWNLOADED else Download.NOT_DOWNLOADED .subscribeLatestCache({ view, chapter ->
view.onAppendChapter(chapter)
appenderSubscription = getPageListObservable(it).subscribeOn(Schedulers.io()) }, { view, error ->
.observeOn(AndroidSchedulers.mainThread()) view.onChapterAppendError()
.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)
} }
/**
* Retries a page that failed to load due to network error or corruption.
*
* @param page the page that failed.
*/
fun retryPage(page: Page?) { fun retryPage(page: Page?) {
if (page != null) { if (page != null && source is OnlineSource) {
page.status = Page.QUEUE page.status = Page.QUEUE
if (page.imagePath != null) { if (page.imagePath != null) {
val file = File(page.imagePath) val file = File(page.imagePath)
chapterCache.removeFileFromCache(file.name) 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() { fun onChapterLeft() {
val pages = chapter.pages ?: return val pages = chapter.pages ?: return
// Get the last page read // Reference these locally because they are needed later from another thread.
var activePageNumber = chapter.last_page_read val chapter = chapter
val prevChapter = prevChapter
// Just in case, avoid out of index exceptions Observable
if (activePageNumber >= pages.size) { .fromCallable {
activePageNumber = pages.size - 1 if (!chapter.isDownloaded) {
} source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
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)
} }
} 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()
} }
} .subscribeOn(Schedulers.io())
}
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) }
.subscribe() .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 * Delete selected chapter
*
* @param chapter chapter that is selected * @param chapter chapter that is selected
* *
* @param manga manga that belongs to chapter * @param manga manga that belongs to chapter
*/ */
fun deleteChapter(chapter: Chapter, manga: Manga) { fun deleteChapter(chapter: ReaderChapter, manga: Manga) {
val source = sourceManager.get(manga.source)!! chapter.isDownloaded = false
chapter.pages?.forEach { it.status == Page.QUEUE }
downloadManager.deleteChapter(source, manga, chapter) 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 * Returns the chapter to be marked as last read in sync services or 0 if no update required.
// 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
fun getMangaSyncChapterToUpdate(): Int { fun getMangaSyncChapterToUpdate(): Int {
if (chapter.pages == null || mangaSyncList == null || mangaSyncList!!.isEmpty()) if (chapter.pages == null || mangaSyncList == null || mangaSyncList!!.isEmpty())
return 0 return 0
var lastChapterReadLocal = 0 var lastChapterReadLocal = 0
// If the current chapter has been read, we check with this one
if (chapter.read) if (chapter.read)
lastChapterReadLocal = Math.floor(chapter.chapter_number.toDouble()).toInt() lastChapterReadLocal = Math.floor(chapter.chapter_number.toDouble()).toInt()
else if (previousChapter != null && previousChapter!!.read) // If not, we check if the previous chapter has been read
lastChapterReadLocal = Math.floor(previousChapter!!.chapter_number.toDouble()).toInt() 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 var hasToUpdate = false
for (mangaSync in mangaSyncList!!) { for (mangaSync in mangaSyncList!!) {
@ -387,6 +429,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
return if (hasToUpdate) lastChapterReadLocal else 0 return if (hasToUpdate) lastChapterReadLocal else 0
} }
/**
* Starts the service that updates the last chapter read in sync services
*/
fun updateMangaSyncLastChapterRead() { fun updateMangaSyncLastChapterRead() {
for (mangaSync in mangaSyncList ?: emptyList()) { for (mangaSync in mangaSyncList ?: emptyList()) {
val service = syncManager.getService(mangaSync.sync_id) ?: continue 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 { fun loadNextChapter(): Boolean {
nextChapter?.let { nextChapter?.let {
onChapterLeft() onChapterLeft()
@ -405,44 +455,42 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
return false 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 { fun loadPreviousChapter(): Boolean {
previousChapter?.let { prevChapter?.let {
onChapterLeft() onChapterLeft()
loadChapter(it, 0) loadChapter(it, if (it.read) -1 else 0)
return true return true
} }
return false return false
} }
/**
* Returns true if there's a next chapter.
*/
fun hasNextChapter(): Boolean { fun hasNextChapter(): Boolean {
return nextChapter != null return nextChapter != null
} }
/**
* Returns true if there's a previous chapter.
*/
fun hasPreviousChapter(): Boolean { fun hasPreviousChapter(): Boolean {
return previousChapter != null return prevChapter != 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) }
}
}
}
} }
/**
* Updates the viewer for this manga.
*
* @param viewer the id of the viewer to set.
*/
fun updateMangaViewer(viewer: Int) { fun updateMangaViewer(viewer: Int) {
manga.viewer = viewer manga.viewer = viewer
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
} }
} }

View File

@ -1,11 +1,11 @@
package eu.kanade.tachiyomi.ui.reader.viewer.base package eu.kanade.tachiyomi.ui.reader.viewer.base
import com.davemorrissey.labs.subscaleview.decoder.* 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.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
import java.util.* import java.util.*
/** /**
@ -29,7 +29,7 @@ abstract class BaseReader : BaseFragment() {
/** /**
* List of chapters added in the reader. * 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. * 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() { fun updatePageNumber() {
val activePage = getActivePage() val activePage = getActivePage()
if (activePage != null) { 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) { fun onPageChanged(position: Int) {
val oldPage = pages[currentPage] val oldPage = pages[currentPage]
val newPage = pages[position] val newPage = pages[position]
newPage.chapter.last_page_read = newPage.pageNumber readerActivity.presenter.onPageChanged(newPage)
if (readerActivity.presenter.isSeamlessMode) { val oldChapter = oldPage.chapter
val oldChapter = oldPage.chapter val newChapter = newPage.chapter
val newChapter = newPage.chapter
// Active chapter has changed. // Active chapter has changed.
if (oldChapter.id != newChapter.id) { if (oldChapter.id != newChapter.id) {
readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber) 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()
}
} }
// 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 currentPage = position
updatePageNumber() updatePageNumber()
} }
@ -144,10 +143,10 @@ abstract class BaseReader : BaseFragment() {
* @param chapter the chapter to set. * @param chapter the chapter to set.
* @param currentPage the initial page to display. * @param currentPage the initial page to display.
*/ */
fun onPageListReady(chapter: Chapter, currentPage: Page) { fun onPageListReady(chapter: ReaderChapter, currentPage: Page) {
if (!chapters.contains(chapter)) { if (!chapters.contains(chapter)) {
// if we reset the loaded page we also need to reset the loaded chapters // if we reset the loaded page we also need to reset the loaded chapters
chapters = ArrayList<Chapter>() chapters.clear()
chapters.add(chapter) chapters.add(chapter)
pages = ArrayList(chapter.pages) pages = ArrayList(chapter.pages)
onChapterSet(chapter, currentPage) onChapterSet(chapter, currentPage)
@ -162,11 +161,11 @@ abstract class BaseReader : BaseFragment() {
* *
* @param chapter the chapter to append. * @param chapter the chapter to append.
*/ */
fun onPageListAppendReady(chapter: Chapter) { fun onPageListAppendReady(chapter: ReaderChapter) {
if (!chapters.contains(chapter)) { if (!chapters.contains(chapter)) {
hasRequestedNextChapter = false hasRequestedNextChapter = false
chapters.add(chapter) chapters.add(chapter)
pages.addAll(chapter.pages) pages.addAll(chapter.pages!!)
onChapterAppended(chapter) onChapterAppended(chapter)
} }
} }
@ -184,14 +183,14 @@ abstract class BaseReader : BaseFragment() {
* @param chapter the chapter set. * @param chapter the chapter set.
* @param currentPage the initial page to display. * @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]. * Called when a chapter is appended in [BaseReader].
* *
* @param chapter the chapter appended. * @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...). * 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
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import eu.kanade.tachiyomi.R 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.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.base.BaseReader
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader
@ -181,7 +181,7 @@ abstract class PagerReader : BaseReader() {
* @param chapter the chapter set. * @param chapter the chapter set.
* @param currentPage the initial page to display. * @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 this.currentPage = getPageIndex(currentPage) // we might have a new page object
// Make sure the view is already initialized. // Make sure the view is already initialized.
@ -195,7 +195,7 @@ abstract class PagerReader : BaseReader() {
* *
* @param chapter the chapter appended. * @param chapter the chapter appended.
*/ */
override fun onChapterAppended(chapter: Chapter) { override fun onChapterAppended(chapter: ReaderChapter) {
// Make sure the view is already initialized. // Make sure the view is already initialized.
if (view != null) { if (view != null) {
adapter.pages = pages adapter.pages = pages

View File

@ -6,8 +6,8 @@ import android.view.*
import android.view.GestureDetector.SimpleOnGestureListener import android.view.GestureDetector.SimpleOnGestureListener
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 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.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.base.BaseReader
import eu.kanade.tachiyomi.widget.PreCachingLayoutManager import eu.kanade.tachiyomi.widget.PreCachingLayoutManager
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
@ -147,7 +147,7 @@ class WebtoonReader : BaseReader() {
* @param chapter the chapter set. * @param chapter the chapter set.
* @param currentPage the initial page to display. * @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 // Restoring current page is not supported. It's getting weird scrolling jumps
// this.currentPage = currentPage; // this.currentPage = currentPage;
@ -162,11 +162,11 @@ class WebtoonReader : BaseReader() {
* *
* @param chapter the chapter appended. * @param chapter the chapter appended.
*/ */
override fun onChapterAppended(chapter: Chapter) { override fun onChapterAppended(chapter: ReaderChapter) {
// Make sure the view is already initialized. // Make sure the view is already initialized.
if (view != null) { if (view != null) {
val insertStart = pages.size - chapter.pages.size val insertStart = pages.size - chapter.pages!!.size
adapter.notifyItemRangeInserted(insertStart, 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_custom_brightness_value_key">pref_custom_brightness_value_key</string>
<string name="pref_reader_theme_key">pref_reader_theme_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_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_volume_keys_key">reader_volume_keys</string>
<string name="pref_read_with_tapping_key">reader_tap</string> <string name="pref_read_with_tapping_key">reader_tap</string>
<string name="pref_reencode_key">reencode_image</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_enable_transitions">Enable transitions</string>
<string name="pref_show_page_number">Show page number</string> <string name="pref_show_page_number">Show page number</string>
<string name="pref_custom_brightness">Use custom brightness</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_keep_screen_on">Keep screen on</string>
<string name="pref_reader_navigation">Navigation</string> <string name="pref_reader_navigation">Navigation</string>
<string name="pref_read_with_volume_keys">Volume keys</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:key="@string/pref_keep_screen_on_key"
android:defaultValue="true" /> android:defaultValue="true" />
<SwitchPreferenceCompat
android:title="@string/pref_seamless_mode"
android:key="@string/pref_seamless_mode_key"
android:defaultValue="true" />
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_reader_navigation"> android:title="@string/pref_reader_navigation">