From 9adf19b8987d4a4d4f6f59d9b93ff384164e3ce2 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 27 Feb 2020 19:51:29 +0200 Subject: [PATCH] Dynamic chapters loading --- app/build.gradle | 2 +- app/libs/okhttpprofiler-1.0.7.aar | Bin 0 -> 9203 bytes .../java/org/koitharu/kotatsu/KotatsuApp.kt | 15 ++-- .../kotatsu/ui/reader/BaseReaderFragment.kt | 84 ++++++++++++++++++ .../ui/reader/OnBoundsScrollListener.kt | 8 ++ .../koitharu/kotatsu/ui/reader/PageLoader.kt | 5 ++ .../kotatsu/ui/reader/ReaderActivity.kt | 11 ++- .../kotatsu/ui/reader/ReaderListener.kt | 8 ++ .../standard/PagerPaginationListener.kt | 26 ++++++ .../reader/standard/StandardReaderFragment.kt | 39 ++++++-- .../reader/wetoon/ListPaginationListener.kt | 29 ++++++ .../ui/reader/wetoon/WebtoonReaderFragment.kt | 37 ++++++-- 12 files changed, 241 insertions(+), 23 deletions(-) create mode 100644 app/libs/okhttpprofiler-1.0.7.aar create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/OnBoundsScrollListener.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderListener.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PagerPaginationListener.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/ListPaginationListener.kt diff --git a/app/build.gradle b/app/build.gradle index b08f1898a..61eff687a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/libs/okhttpprofiler-1.0.7.aar b/app/libs/okhttpprofiler-1.0.7.aar new file mode 100644 index 0000000000000000000000000000000000000000..53c8c5cc4a5a86a3c685f228f4c122d9c826be80 GIT binary patch literal 9203 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 ZzltbG!+iXE0sH5R`}2C)KUWYC{|AAk5tRS{ literal 0 HcmV?d00001 diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index cb3f6a643..3ed054989 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -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, diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/BaseReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/BaseReaderFragment.kt index 7d7f623b6..4e5bef523 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/BaseReaderFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/BaseReaderFragment.kt @@ -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>() as Deque> + 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) = Unit + + final override fun onPagesLoaded(chapterId: Long, pages: List) { + 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, action: Action) + + protected enum class Action { + REPLACE, PREPEND, APPEND + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/OnBoundsScrollListener.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/OnBoundsScrollListener.kt new file mode 100644 index 000000000..df7c683f6 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/OnBoundsScrollListener.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.ui.reader + +interface OnBoundsScrollListener { + + fun onScrolledToStart() + + fun onScrolledToEnd() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt index 56f49803e..1d6fb6f71 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt @@ -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) } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderActivity.kt index 130827b1d..135d316c1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderActivity.kt @@ -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) diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderListener.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderListener.kt new file mode 100644 index 000000000..7ff2a1eeb --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderListener.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PagerPaginationListener.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PagerPaginationListener.kt new file mode 100644 index 000000000..e3227db94 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PagerPaginationListener.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/StandardReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/StandardReaderFragment.kt index 02e756375..e4d2d6697 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/StandardReaderFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/StandardReaderFragment.kt @@ -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) { - adapter?.let { - it.replaceData(pages) - lastState?.let { state -> - if (chapterId == state.chapterId) { - pager.setCurrentItem(state.page, false) + override fun onPagesLoaded(chapterId: Long, pages: List, 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 diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/ListPaginationListener.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/ListPaginationListener.kt new file mode 100644 index 000000000..d6bd3b9b1 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/ListPaginationListener.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonReaderFragment.kt index 951e0ee3c..ecd74be75 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonReaderFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonReaderFragment.kt @@ -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) { - adapter?.let { - it.replaceData(pages) - lastState?.let { state -> - if (chapterId == state.chapterId) { - recyclerView.firstItem = state.page + override fun onPagesLoaded(chapterId: Long, pages: List, 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) } }