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

@@ -6,6 +6,7 @@ import androidx.room.Room
import coil.Coil
import coil.ImageLoader
import coil.util.CoilUtils
import com.itkacher.okhttpprofiler.OkHttpProfilerInterceptor
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
@@ -80,11 +81,15 @@ class KotatsuApp : Application() {
})
}
private fun okHttp() = OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.cookieJar(cookieJar)
private fun okHttp() = OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(cookieJar)
if (BuildConfig.DEBUG) {
addInterceptor(OkHttpProfilerInterceptor())
}
}
private fun mangaDb() = Room.databaseBuilder(
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.prefs.ReaderMode
import org.koitharu.kotatsu.ui.common.BaseFragment
import java.util.*
abstract class BaseReaderFragment(@LayoutRes contentLayoutId: Int) : BaseFragment(contentLayoutId),
ReaderView {
private val chaptersMap = ArrayDeque<Pair<Long, Int>>() as Deque<Pair<Long, Int>>
protected val lastState
get() = (activity as? ReaderActivity)?.state
@@ -42,4 +45,85 @@ abstract class BaseReaderFragment(@LayoutRes contentLayoutId: Int) : BaseFragmen
override fun onInitReader(mode: ReaderMode) = 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)
.build()
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 ->
response.body!!.byteStream().copyTo(out)
}

View File

@@ -34,7 +34,8 @@ import org.koitharu.kotatsu.utils.anim.Motion
import org.koitharu.kotatsu.utils.ext.*
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)
@@ -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() {
Toast.makeText(this, R.string.wait_for_loading_finish, Toast.LENGTH_SHORT).apply {
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.core.model.MangaPage
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.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 var adapter: PagesAdapter? = null
private var isBusy: Boolean = true
private lateinit var loader: PageLoader
override fun onCreate(savedInstanceState: Bundle?) {
@@ -28,6 +30,8 @@ class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_stand
adapter = PagesAdapter(loader)
pager.adapter = adapter
pager.offscreenPageLimit = 2
pager.registerOnPageChangeCallback(PagerPaginationListener(adapter!!, 2, this))
pager.doOnPageChanged(::notifyPageChanged)
}
override fun onDestroyView() {
@@ -35,16 +39,19 @@ class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_stand
super.onDestroyView()
}
override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>) {
adapter?.let {
it.replaceData(pages)
lastState?.let { state ->
if (chapterId == state.chapterId) {
pager.setCurrentItem(state.page, false)
override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>, action: Action) {
when (action) {
Action.REPLACE -> adapter?.let {
it.replaceData(pages)
lastState?.let { state ->
if (chapterId == state.chapterId) {
pager.setCurrentItem(state.page, false)
}
}
}
Action.PREPEND -> adapter?.prependData(pages)
Action.APPEND -> adapter?.appendData(pages)
}
isBusy = false
}
override fun onDestroy() {
@@ -52,6 +59,20 @@ class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_stand
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
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.core.model.MangaPage
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.ReaderPresenter
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)
@@ -27,16 +29,37 @@ class WebtoonReaderFragment : BaseReaderFragment(R.layout.fragment_reader_webtoo
super.onViewCreated(view, savedInstanceState)
adapter = WebtoonAdapter(loader)
recyclerView.adapter = adapter
recyclerView.addOnScrollListener(ListPaginationListener(2, this))
}
override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>) {
adapter?.let {
it.replaceData(pages)
lastState?.let { state ->
if (chapterId == state.chapterId) {
recyclerView.firstItem = state.page
override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>, action: Action) {
when(action) {
Action.REPLACE -> {
adapter?.let {
it.replaceData(pages)
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)
}
}