Dynamic chapters loading
This commit is contained in:
@@ -52,7 +52,7 @@ androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
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.kotlinx:kotlinx-coroutines-core:1.3.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
|
||||
|
||||
BIN
app/libs/okhttpprofiler-1.0.7.aar
Normal file
BIN
app/libs/okhttpprofiler-1.0.7.aar
Normal file
Binary file not shown.
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.ui.reader
|
||||
|
||||
interface OnBoundsScrollListener {
|
||||
|
||||
fun onScrolledToStart()
|
||||
|
||||
fun onScrolledToEnd()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user