Dynamic chapters loading

This commit is contained in:
Koitharu
2020-02-27 19:51:29 +02:00
parent 60567757ae
commit 9adf19b898
12 changed files with 241 additions and 23 deletions

View File

@@ -52,7 +52,7 @@ androidExtensions {
experimental = true experimental = true
} }
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'

Binary file not shown.

View File

@@ -6,6 +6,7 @@ import androidx.room.Room
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
import coil.util.CoilUtils import coil.util.CoilUtils
import com.itkacher.okhttpprofiler.OkHttpProfilerInterceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
@@ -80,11 +81,15 @@ class KotatsuApp : Application() {
}) })
} }
private fun okHttp() = OkHttpClient.Builder() private fun okHttp() = OkHttpClient.Builder().apply {
.connectTimeout(20, TimeUnit.SECONDS) connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS) writeTimeout(20, TimeUnit.SECONDS)
.cookieJar(cookieJar) cookieJar(cookieJar)
if (BuildConfig.DEBUG) {
addInterceptor(OkHttpProfilerInterceptor())
}
}
private fun mangaDb() = Room.databaseBuilder( private fun mangaDb() = Room.databaseBuilder(
applicationContext, applicationContext,

View File

@@ -6,10 +6,13 @@ import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.ui.common.BaseFragment import org.koitharu.kotatsu.ui.common.BaseFragment
import java.util.*
abstract class BaseReaderFragment(@LayoutRes contentLayoutId: Int) : BaseFragment(contentLayoutId), abstract class BaseReaderFragment(@LayoutRes contentLayoutId: Int) : BaseFragment(contentLayoutId),
ReaderView { ReaderView {
private val chaptersMap = ArrayDeque<Pair<Long, Int>>() as Deque<Pair<Long, Int>>
protected val lastState protected val lastState
get() = (activity as? ReaderActivity)?.state get() = (activity as? ReaderActivity)?.state
@@ -42,4 +45,85 @@ abstract class BaseReaderFragment(@LayoutRes contentLayoutId: Int) : BaseFragmen
override fun onInitReader(mode: ReaderMode) = Unit override fun onInitReader(mode: ReaderMode) = Unit
override fun onChaptersLoader(chapters: List<MangaChapter>) = Unit override fun onChaptersLoader(chapters: List<MangaChapter>) = Unit
final override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>) {
when {
chaptersMap.isEmpty() -> {
chaptersMap.push(chapterId to pages.size)
onPagesLoaded(chapterId, pages, Action.REPLACE)
}
shouldAppend(chapterId) -> {
chaptersMap.addLast(chapterId to pages.size)
onPagesLoaded(chapterId, pages, Action.APPEND)
}
shouldPrepend(chapterId) -> {
chaptersMap.addFirst(chapterId to pages.size)
onPagesLoaded(chapterId, pages, Action.PREPEND)
}
else -> {
chaptersMap.clear()
chaptersMap.push(chapterId to pages.size)
onPagesLoaded(chapterId, pages, Action.REPLACE)
}
}
}
private fun shouldAppend(chapterId: Long): Boolean {
val chapters = lastState?.manga?.chapters ?: return false
val lastChapterId = chaptersMap.peekLast()?.first ?: return false
val indexOfCurrent = chapters.indexOfLast { x -> x.id == lastChapterId }
val indexOfNext = chapters.indexOfLast { x -> x.id == chapterId }
return indexOfCurrent != -1 && indexOfNext != -1 && indexOfCurrent + 1 == indexOfNext
}
private fun shouldPrepend(chapterId: Long): Boolean {
val chapters = lastState?.manga?.chapters ?: return false
val firstChapterId = chaptersMap.peekFirst()?.first ?: return false
val indexOfCurrent = chapters.indexOfFirst { x -> x.id == firstChapterId }
val indexOfPrev = chapters.indexOfFirst { x -> x.id == chapterId }
return indexOfCurrent != -1 && indexOfPrev != -1 && indexOfCurrent + 1 == indexOfPrev
}
protected fun getNextChapterId(): Long {
val lastChapterId = chaptersMap.peekLast()?.first ?: return 0
val chapters = lastState?.manga?.chapters ?: return 0
val indexOfCurrent = chapters.indexOfLast { x -> x.id == lastChapterId }
return if (indexOfCurrent == -1) {
0
} else {
chapters.getOrNull(indexOfCurrent + 1)?.id ?: 0
}
}
protected fun getPrevChapterId(): Long {
val firstChapterId = chaptersMap.peekFirst()?.first ?: return 0
val chapters = lastState?.manga?.chapters ?: return 0
val indexOfCurrent = chapters.indexOfFirst { x -> x.id == firstChapterId }
return if (indexOfCurrent == -1) {
0
} else {
chapters.getOrNull(indexOfCurrent - 1)?.id ?: 0
}
}
protected fun notifyPageChanged(page: Int) {
var i = page
val chapters = lastState?.manga?.chapters ?: return
val chapter = chaptersMap.firstOrNull { x ->
i -= x.second
i <= 0
} ?: return
(activity as? ReaderListener)?.onPageChanged(
chapter = chapters.find { x -> x.id == chapter.first } ?: return,
page = i + chapter.second,
total = chapter.second
)
}
protected abstract fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>, action: Action)
protected enum class Action {
REPLACE, PREPEND, APPEND
}
} }

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.ui.reader
interface OnBoundsScrollListener {
fun onScrolledToStart()
fun onScrolledToEnd()
}

View File

@@ -45,6 +45,11 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
.cacheControl(CacheUtils.CONTROL_DISABLED) .cacheControl(CacheUtils.CONTROL_DISABLED)
.build() .build()
okHttp.newCall(request).await().use { response -> okHttp.newCall(request).await().use { response ->
val body = response.body!!
val type = body.contentType()
check(type?.type == "image") {
"Unexpected content type ${type?.type}/${type?.subtype}"
}
cache.put(url) { out -> cache.put(url) { out ->
response.body!!.byteStream().copyTo(out) response.body!!.byteStream().copyTo(out)
} }

View File

@@ -34,7 +34,8 @@ import org.koitharu.kotatsu.utils.anim.Motion
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnChapterChangeListener, class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnChapterChangeListener,
GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback { GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback,
ReaderListener {
private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance) private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance)
@@ -246,6 +247,14 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
} }
} }
override fun onPageChanged(chapter: MangaChapter, page: Int, total: Int) {
title = chapter.name
state.manga.chapters?.run {
supportActionBar?.subtitle =
getString(R.string.chapter_d_of_d, chapter.number, size)
}
}
private fun showWaitWhileLoading() { private fun showWaitWhileLoading() {
Toast.makeText(this, R.string.wait_for_loading_finish, Toast.LENGTH_SHORT).apply { Toast.makeText(this, R.string.wait_for_loading_finish, Toast.LENGTH_SHORT).apply {
setGravity(Gravity.CENTER, 0, 0) setGravity(Gravity.CENTER, 0, 0)

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.ui.reader
import org.koitharu.kotatsu.core.model.MangaChapter
interface ReaderListener {
fun onPageChanged(chapter: MangaChapter, page: Int, total: Int)
}

View File

@@ -0,0 +1,26 @@
package org.koitharu.kotatsu.ui.reader.standard
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import org.koitharu.kotatsu.ui.reader.OnBoundsScrollListener
class PagerPaginationListener(
private val adapter: RecyclerView.Adapter<*>,
private val offset: Int,
private val listener: OnBoundsScrollListener
) : ViewPager2.OnPageChangeCallback() {
private var lastItemCountStart = 0
private var lastItemCountEnd = 0
override fun onPageSelected(position: Int) {
val itemCount = adapter.itemCount
if (position <= offset && itemCount != lastItemCountStart) {
lastItemCountStart = itemCount
listener.onScrolledToStart()
} else if (position >= itemCount - offset && itemCount != lastItemCountEnd) {
lastItemCountEnd = itemCount
listener.onScrolledToEnd()
}
}
}

View File

@@ -7,15 +7,17 @@ import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.reader.BaseReaderFragment import org.koitharu.kotatsu.ui.reader.BaseReaderFragment
import org.koitharu.kotatsu.ui.reader.OnBoundsScrollListener
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.ReaderPresenter import org.koitharu.kotatsu.ui.reader.ReaderPresenter
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_standard) { class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_standard),
OnBoundsScrollListener {
private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance) private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance)
private var adapter: PagesAdapter? = null private var adapter: PagesAdapter? = null
private var isBusy: Boolean = true
private lateinit var loader: PageLoader private lateinit var loader: PageLoader
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -28,6 +30,8 @@ class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_stand
adapter = PagesAdapter(loader) adapter = PagesAdapter(loader)
pager.adapter = adapter pager.adapter = adapter
pager.offscreenPageLimit = 2 pager.offscreenPageLimit = 2
pager.registerOnPageChangeCallback(PagerPaginationListener(adapter!!, 2, this))
pager.doOnPageChanged(::notifyPageChanged)
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -35,16 +39,19 @@ class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_stand
super.onDestroyView() super.onDestroyView()
} }
override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>) { override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>, action: Action) {
adapter?.let { when (action) {
it.replaceData(pages) Action.REPLACE -> adapter?.let {
lastState?.let { state -> it.replaceData(pages)
if (chapterId == state.chapterId) { lastState?.let { state ->
pager.setCurrentItem(state.page, false) if (chapterId == state.chapterId) {
pager.setCurrentItem(state.page, false)
}
} }
} }
Action.PREPEND -> adapter?.prependData(pages)
Action.APPEND -> adapter?.appendData(pages)
} }
isBusy = false
} }
override fun onDestroy() { override fun onDestroy() {
@@ -52,6 +59,20 @@ class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_stand
super.onDestroy() super.onDestroy()
} }
override fun onScrolledToStart() {
val prevChapterId = getPrevChapterId()
if (prevChapterId != 0L) {
presenter.loadChapter(lastState?.manga ?: return, prevChapterId)
}
}
override fun onScrolledToEnd() {
val nextChapterId = getNextChapterId()
if (nextChapterId != 0L) {
presenter.loadChapter(lastState?.manga ?: return, nextChapterId)
}
}
override val hasItems: Boolean override val hasItems: Boolean
get() = adapter?.hasItems == true get() = adapter?.hasItems == true

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.ui.reader.wetoon
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.ui.reader.OnBoundsScrollListener
class ListPaginationListener(
private val offset: Int,
private val listener: OnBoundsScrollListener
) : RecyclerView.OnScrollListener() {
private var lastItemCountStart = 0
private var lastItemCountEnd = 0
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val itemCount = recyclerView.adapter?.itemCount ?: return
val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
if (firstVisiblePosition <= offset && itemCount != lastItemCountStart) {
lastItemCountStart = itemCount
listener.onScrolledToStart()
} else if (lastVisiblePosition >= itemCount - offset && itemCount != lastItemCountEnd) {
lastItemCountEnd = itemCount
listener.onScrolledToEnd()
}
}
}

View File

@@ -7,11 +7,13 @@ import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.reader.BaseReaderFragment import org.koitharu.kotatsu.ui.reader.BaseReaderFragment
import org.koitharu.kotatsu.ui.reader.OnBoundsScrollListener
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.ReaderPresenter import org.koitharu.kotatsu.ui.reader.ReaderPresenter
import org.koitharu.kotatsu.utils.ext.firstItem import org.koitharu.kotatsu.utils.ext.firstItem
class WebtoonReaderFragment : BaseReaderFragment(R.layout.fragment_reader_webtoon) { class WebtoonReaderFragment : BaseReaderFragment(R.layout.fragment_reader_webtoon),
OnBoundsScrollListener {
private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance) private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance)
@@ -27,16 +29,37 @@ class WebtoonReaderFragment : BaseReaderFragment(R.layout.fragment_reader_webtoo
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapter = WebtoonAdapter(loader) adapter = WebtoonAdapter(loader)
recyclerView.adapter = adapter recyclerView.adapter = adapter
recyclerView.addOnScrollListener(ListPaginationListener(2, this))
} }
override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>) { override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>, action: Action) {
adapter?.let { when(action) {
it.replaceData(pages) Action.REPLACE -> {
lastState?.let { state -> adapter?.let {
if (chapterId == state.chapterId) { it.replaceData(pages)
recyclerView.firstItem = state.page lastState?.let { state ->
if (chapterId == state.chapterId) {
recyclerView.firstItem = state.page
}
}
} }
} }
Action.PREPEND -> adapter?.prependData(pages)
Action.APPEND -> adapter?.appendData(pages)
}
}
override fun onScrolledToStart() {
val prevChapterId = getPrevChapterId()
if (prevChapterId != 0L) {
presenter.loadChapter(lastState?.manga ?: return, prevChapterId)
}
}
override fun onScrolledToEnd() {
val nextChapterId = getNextChapterId()
if (nextChapterId != 0L) {
presenter.loadChapter(lastState?.manga ?: return, nextChapterId)
} }
} }