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 000000000..53c8c5cc4 Binary files /dev/null and b/app/libs/okhttpprofiler-1.0.7.aar differ 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) } }