Remove presenter from reader fragments
This commit is contained in:
@@ -84,7 +84,7 @@ dependencies {
|
||||
implementation 'com.squareup.okio:okio:2.4.3'
|
||||
implementation 'org.jsoup:jsoup:1.12.2'
|
||||
|
||||
implementation 'org.koin:koin-android:2.1.1'
|
||||
implementation 'org.koin:koin-android:2.1.3'
|
||||
implementation 'io.coil-kt:coil:0.9.5'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
||||
implementation 'com.tomclaw.cache:cache:1.0'
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
package org.koitharu.kotatsu.ui.reader
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.LayoutRes
|
||||
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
|
||||
|
||||
abstract val hasItems: Boolean
|
||||
|
||||
protected abstract val currentPageIndex: Int
|
||||
|
||||
abstract val pages: List<MangaPage>
|
||||
|
||||
abstract fun setCurrentPage(index: Int, smooth: Boolean)
|
||||
|
||||
val currentPage get() = pages.getOrNull(currentPageIndex)
|
||||
|
||||
/**
|
||||
* Handled by activity
|
||||
*/
|
||||
override fun onLoadingStateChanged(isLoading: Boolean) = Unit
|
||||
|
||||
/**
|
||||
* Handled by activity
|
||||
*/
|
||||
override fun onError(e: Throwable) = Unit
|
||||
|
||||
/**
|
||||
* Handled by activity
|
||||
*/
|
||||
override fun onPageSaved(uri: Uri?) = Unit
|
||||
|
||||
|
||||
override fun onInitReader(mode: ReaderMode) = Unit
|
||||
|
||||
override fun onChaptersLoader(chapters: List<MangaChapter>) = Unit
|
||||
|
||||
override fun onDestroyView() {
|
||||
chaptersMap.clear()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>, action: ReaderAction) {
|
||||
when (action) {
|
||||
ReaderAction.REPLACE -> {
|
||||
chaptersMap.clear()
|
||||
chaptersMap.add(chapterId to pages.size)
|
||||
}
|
||||
ReaderAction.PREPEND -> chaptersMap.addFirst(chapterId to pages.size)
|
||||
ReaderAction.APPEND -> chaptersMap.addLast(chapterId to pages.size)
|
||||
}
|
||||
}
|
||||
|
||||
open fun switchPageBy(delta: Int) {
|
||||
setCurrentPage(currentPageIndex + delta, true)
|
||||
}
|
||||
|
||||
fun findCurrentPageIndex(chapterId: Long): Int {
|
||||
var offset = 0
|
||||
for ((id, count) in chaptersMap) {
|
||||
if (id == chapterId) {
|
||||
return currentPageIndex - offset
|
||||
}
|
||||
offset += count
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun findChapterOffset(chapterId: Long): Int {
|
||||
var offset = 0
|
||||
for ((id, count) in chaptersMap) {
|
||||
if (id == chapterId) {
|
||||
return offset
|
||||
}
|
||||
offset += count
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun getPages(chapterId: Long): List<MangaPage>? {
|
||||
var offset = 0
|
||||
for ((id, count) in chaptersMap) {
|
||||
if (id == chapterId) {
|
||||
return pages.subList(offset, offset + count - 1)
|
||||
}
|
||||
offset += count
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@@ -24,7 +25,8 @@ import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.ui.common.BaseFullscreenActivity
|
||||
import org.koitharu.kotatsu.ui.reader.standard.StandardReaderFragment
|
||||
import org.koitharu.kotatsu.ui.reader.base.AbstractReader
|
||||
import org.koitharu.kotatsu.ui.reader.standard.PagerReaderFragment
|
||||
import org.koitharu.kotatsu.ui.reader.thumbnails.OnPageSelectListener
|
||||
import org.koitharu.kotatsu.ui.reader.thumbnails.PagesThumbnailsSheet
|
||||
import org.koitharu.kotatsu.ui.reader.wetoon.WebtoonReaderFragment
|
||||
@@ -37,7 +39,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback,
|
||||
ReaderListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance)
|
||||
private val presenter by moxyPresenter(factory = ::ReaderPresenter)
|
||||
private val settings by inject<AppSettings>()
|
||||
|
||||
lateinit var state: ReaderState
|
||||
@@ -48,7 +50,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
private var isVolumeKeysSwitchEnabled = false
|
||||
|
||||
private val reader
|
||||
get() = supportFragmentManager.findFragmentById(R.id.container) as? BaseReaderFragment
|
||||
get() = supportFragmentManager.findFragmentById(R.id.container) as? AbstractReader
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -81,24 +83,33 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
loadSettings()
|
||||
|
||||
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
|
||||
presenter.loadChapter(state.manga, state.chapterId, ReaderAction.REPLACE)
|
||||
presenter.init(state.manga)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInitReader(mode: ReaderMode) {
|
||||
if (reader == null) {
|
||||
setReader(mode)
|
||||
override fun onInitReader(manga: Manga, mode: ReaderMode) {
|
||||
val currentReader = reader
|
||||
when (mode) {
|
||||
ReaderMode.WEBTOON -> if (currentReader !is WebtoonReaderFragment) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.container, WebtoonReaderFragment.newInstance(state))
|
||||
}
|
||||
}
|
||||
else -> if (currentReader !is PagerReaderFragment) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.container, PagerReaderFragment.newInstance(state))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>, action: ReaderAction) = Unit
|
||||
|
||||
override fun onPause() {
|
||||
reader?.let {
|
||||
state = state.copy(page = it.findCurrentPageIndex(state.chapterId))
|
||||
presenter.saveState(state)
|
||||
toolbar_bottom.menu.findItem(R.id.action_reader_mode).setIcon(
|
||||
when (mode) {
|
||||
ReaderMode.WEBTOON -> R.drawable.ic_script
|
||||
else -> R.drawable.ic_book_page
|
||||
}
|
||||
)
|
||||
appbar_top.postDelayed(1000) {
|
||||
setUiIsVisible(false)
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -120,7 +131,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
R.id.action_reader_mode -> {
|
||||
ReaderConfigDialog.show(
|
||||
supportFragmentManager, when (reader) {
|
||||
is StandardReaderFragment -> ReaderMode.STANDARD
|
||||
is PagerReaderFragment -> ReaderMode.STANDARD
|
||||
is WebtoonReaderFragment -> ReaderMode.WEBTOON
|
||||
else -> ReaderMode.UNKNOWN
|
||||
}
|
||||
@@ -141,7 +152,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
}
|
||||
R.id.action_pages_thumbs -> {
|
||||
if (reader?.hasItems == true) {
|
||||
val pages = reader?.getPages(state.chapterId)
|
||||
val pages = reader?.getPages()
|
||||
if (pages != null) {
|
||||
PagesThumbnailsSheet.show(
|
||||
supportFragmentManager, pages,
|
||||
@@ -173,6 +184,11 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun saveState(chapterId: Long, page: Int) {
|
||||
state = state.copy(chapterId = chapterId, page = page)
|
||||
ReaderPresenter.saveState(state)
|
||||
}
|
||||
|
||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
val hasPages = reader?.hasItems == true
|
||||
layout_loading.isVisible = isLoading && !hasPages
|
||||
@@ -268,29 +284,16 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
chapterId = chapter.id,
|
||||
page = 0
|
||||
)
|
||||
presenter.loadChapter(state.manga, chapter.id, ReaderAction.REPLACE)
|
||||
reader?.updateState(chapterId = chapter.id)
|
||||
}
|
||||
|
||||
override fun onPageSelected(page: MangaPage) {
|
||||
reader?.let {
|
||||
val index = it.pages.indexOfFirst { x -> x.id == page.id }
|
||||
if (index != -1) {
|
||||
it.setCurrentPage(index, false)
|
||||
}
|
||||
}
|
||||
reader?.updateState(pageId = page.id)
|
||||
}
|
||||
|
||||
override fun onReaderModeChanged(mode: ReaderMode) {
|
||||
reader?.let {
|
||||
state = state.copy(page = it.findCurrentPageIndex(state.chapterId))
|
||||
}
|
||||
presenter.saveState(state, mode)
|
||||
setReader(mode)
|
||||
setUiIsVisible(false)
|
||||
}
|
||||
|
||||
override fun onChaptersLoader(chapters: List<MangaChapter>) {
|
||||
state = state.copy(manga = state.manga.copy(chapters = chapters))
|
||||
//TODO save state
|
||||
presenter.setMode(state.manga, mode)
|
||||
}
|
||||
|
||||
override fun onPageSaved(uri: Uri?) {
|
||||
@@ -305,14 +308,10 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
}
|
||||
|
||||
override fun onPageChanged(chapter: MangaChapter, page: Int, total: Int) {
|
||||
if (chapter.id != state.chapterId) {
|
||||
title = chapter.name
|
||||
state = state.copy(chapterId = chapter.id)
|
||||
presenter.saveState(state)
|
||||
state.manga.chapters?.run {
|
||||
supportActionBar?.subtitle =
|
||||
getString(R.string.chapter_d_of_d, chapter.number, size)
|
||||
}
|
||||
title = chapter.name
|
||||
state.manga.chapters?.run {
|
||||
supportActionBar?.subtitle =
|
||||
getString(R.string.chapter_d_of_d, chapter.number, size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,25 +339,6 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
|
||||
}
|
||||
}
|
||||
|
||||
private fun setReader(mode: ReaderMode) {
|
||||
val currentReader = reader
|
||||
when (mode) {
|
||||
ReaderMode.WEBTOON -> if (currentReader !is WebtoonReaderFragment) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.container, WebtoonReaderFragment())
|
||||
}
|
||||
}
|
||||
else -> if (currentReader !is StandardReaderFragment) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.container, StandardReaderFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
toolbar_bottom.menu.findItem(R.id.action_reader_mode).setIcon(when(mode) {
|
||||
ReaderMode.WEBTOON -> R.drawable.ic_script
|
||||
else -> R.drawable.ic_book_page
|
||||
})
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
settings.readerPageSwitch.let {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.koitharu.kotatsu.ui.reader
|
||||
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.ui.common.BaseMvpView
|
||||
|
||||
interface ReaderListener {
|
||||
interface ReaderListener : BaseMvpView {
|
||||
|
||||
fun onPageChanged(chapter: MangaChapter, page: Int, total: Int)
|
||||
|
||||
fun saveState(chapterId: Long, page: Int)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import moxy.InjectViewState
|
||||
import moxy.presenterScope
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koin.core.get
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
@@ -25,50 +26,31 @@ import org.koitharu.kotatsu.utils.ext.mimeType
|
||||
@InjectViewState
|
||||
class ReaderPresenter : BasePresenter<ReaderView>() {
|
||||
|
||||
private var loaderJob: Job? = null
|
||||
private var isInitialized = false
|
||||
|
||||
fun loadChapter(manga: Manga, chapterId: Long, action: ReaderAction) {
|
||||
loaderJob?.cancel()
|
||||
loaderJob = presenterScope.launch {
|
||||
fun init(manga: Manga) {
|
||||
presenterScope.launch {
|
||||
viewState.onLoadingStateChanged(isLoading = true)
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val mode = withContext(Dispatchers.IO) {
|
||||
val repo = MangaProviderFactory.create(manga.source)
|
||||
val chapter = (manga.chapters ?: repo.getDetails(manga).chapters?.also {
|
||||
withContext(Dispatchers.Main) {
|
||||
viewState.onChaptersLoader(it)
|
||||
val chapter =
|
||||
(manga.chapters ?: throw RuntimeException("Chapters is null")).random()
|
||||
val prefs = MangaDataRepository()
|
||||
var mode = prefs.getReaderMode(manga.id)
|
||||
if (mode == null) {
|
||||
val pages = repo.getPages(chapter)
|
||||
mode = MangaUtils.determineReaderMode(pages)
|
||||
if (mode != null) {
|
||||
prefs.savePreferences(
|
||||
mangaId = manga.id,
|
||||
mode = mode
|
||||
)
|
||||
}
|
||||
})?.find { it.id == chapterId }
|
||||
?: throw RuntimeException("Chapter ${chapterId} not found")
|
||||
val pages = repo.getPages(chapter)
|
||||
if (!isInitialized) {
|
||||
val prefs = MangaDataRepository()
|
||||
var mode = prefs.getReaderMode(manga.id)
|
||||
if (mode == null) {
|
||||
mode = MangaUtils.determineReaderMode(pages)
|
||||
if (mode != null) {
|
||||
prefs.savePreferences(
|
||||
mangaId = manga.id,
|
||||
mode = mode
|
||||
)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
viewState.onInitReader(mode ?: ReaderMode.UNKNOWN)
|
||||
}
|
||||
isInitialized = true
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
viewState.onPagesLoaded(chapterId, pages, action)
|
||||
}
|
||||
mode ?: ReaderMode.UNKNOWN
|
||||
}
|
||||
} catch (e: CancellationException){
|
||||
Log.w(null, "Loader job cancelled", e)
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
viewState.onInitReader(manga, mode)
|
||||
} catch (_: CancellationException) {
|
||||
} catch (e: Throwable) {
|
||||
viewState.onError(e)
|
||||
} finally {
|
||||
viewState.onLoadingStateChanged(isLoading = false)
|
||||
@@ -76,20 +58,14 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
|
||||
}
|
||||
}
|
||||
|
||||
fun saveState(state: ReaderState, mode: ReaderMode? = null) {
|
||||
fun setMode(manga: Manga, mode: ReaderMode) {
|
||||
presenterScope.launch(Dispatchers.IO) {
|
||||
HistoryRepository().addOrUpdate(
|
||||
manga = state.manga,
|
||||
chapterId = state.chapterId,
|
||||
page = state.page
|
||||
MangaDataRepository().savePreferences(
|
||||
mangaId = manga.id,
|
||||
mode = mode
|
||||
)
|
||||
if (mode != null) {
|
||||
MangaDataRepository().savePreferences(
|
||||
mangaId = state.manga.id,
|
||||
mode = mode
|
||||
)
|
||||
}
|
||||
}
|
||||
viewState.onInitReader(manga, mode)
|
||||
}
|
||||
|
||||
fun savePage(resolver: ContentResolver, page: MangaPage) {
|
||||
@@ -101,7 +77,7 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
val uri = getKoin().get<OkHttpClient>().newCall(request).await().use { response ->
|
||||
val uri = get<OkHttpClient>().newCall(request).await().use { response ->
|
||||
val fileName =
|
||||
URLUtil.guessFileName(url, response.contentDisposition, response.mimeType)
|
||||
MediaStoreCompat.insertImage(resolver, fileName) {
|
||||
@@ -120,20 +96,17 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
instance = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private var instance: ReaderPresenter? = null
|
||||
|
||||
fun getInstance(): ReaderPresenter = instance ?: synchronized(this) {
|
||||
ReaderPresenter().also {
|
||||
instance = it
|
||||
fun saveState(state: ReaderState) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
HistoryRepository().addOrUpdate(
|
||||
manga = state.manga,
|
||||
chapterId = state.chapterId,
|
||||
page = state.page
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,14 @@ package org.koitharu.kotatsu.ui.reader
|
||||
import android.net.Uri
|
||||
import moxy.viewstate.strategy.alias.AddToEndSingle
|
||||
import moxy.viewstate.strategy.alias.OneExecution
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.ui.common.BaseMvpView
|
||||
|
||||
interface ReaderView : BaseMvpView {
|
||||
|
||||
@AddToEndSingle
|
||||
fun onInitReader(mode: ReaderMode)
|
||||
|
||||
@AddToEndSingle
|
||||
fun onChaptersLoader(chapters: List<MangaChapter>)
|
||||
|
||||
@AddToEndSingle
|
||||
fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>, action: ReaderAction)
|
||||
fun onInitReader(manga: Manga, mode: ReaderMode)
|
||||
|
||||
@OneExecution
|
||||
fun onPageSaved(uri: Uri?)
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package org.koitharu.kotatsu.ui.reader.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.model.MangaState
|
||||
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
||||
import org.koitharu.kotatsu.ui.common.BaseFragment
|
||||
import org.koitharu.kotatsu.ui.reader.*
|
||||
|
||||
abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayoutId),
|
||||
OnBoundsScrollListener {
|
||||
|
||||
protected lateinit var manga: Manga
|
||||
protected lateinit var loader: PageLoader
|
||||
private set
|
||||
private lateinit var pages: GroupedList<Long, MangaPage>
|
||||
protected var adapter: BaseReaderAdapter<*>? = null
|
||||
private set
|
||||
|
||||
val hasItems: Boolean
|
||||
get() = itemsCount != 0
|
||||
|
||||
val currentPage: MangaPage?
|
||||
get() = pages.getOrNull(getCurrentItem())
|
||||
|
||||
protected val readerListener: ReaderListener?
|
||||
get() = activity as? ReaderListener
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
pages = GroupedList()
|
||||
manga = requireArguments().getParcelable<ReaderState>(ARG_STATE)!!.manga
|
||||
loader = PageLoader()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = onCreateAdapter(pages)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
@Suppress("RemoveExplicitTypeArguments")
|
||||
val state = savedInstanceState?.getParcelable<ReaderState>(ARG_STATE)
|
||||
?: requireArguments().getParcelable<ReaderState>(ARG_STATE)!!
|
||||
loadChapter(state.chapterId) {
|
||||
pages.clear()
|
||||
pages.addLast(state.chapterId, it)
|
||||
adapter?.notifyDataSetChanged()
|
||||
setCurrentItem(state.page, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelable(
|
||||
ARG_STATE, ReaderState(
|
||||
manga = manga,
|
||||
chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return,
|
||||
page = pages.getRelativeIndex(getCurrentItem())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onScrolledToStart() {
|
||||
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return
|
||||
val index = manga.chapters?.indexOfFirst { it.id == chapterId } ?: return
|
||||
val prevChapterId = manga.chapters!!.getOrNull(index - 1)?.id ?: return
|
||||
loadChapter(prevChapterId) {
|
||||
pages.addFirst(prevChapterId, it)
|
||||
adapter?.notifyItemsPrepended(it.size)
|
||||
view?.postDelayed(500) {
|
||||
trimEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return
|
||||
val index = manga.chapters?.indexOfFirst { it.id == chapterId } ?: return
|
||||
val nextChapterId = manga.chapters!!.getOrNull(index + 1)?.id ?: return
|
||||
loadChapter(nextChapterId) {
|
||||
pages.addLast(nextChapterId, it)
|
||||
adapter?.notifyItemsAppended(it.size)
|
||||
view?.postDelayed(500) {
|
||||
trimStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
loader.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
fun getPages() = pages.findGroupByIndex(getCurrentItem())?.let {
|
||||
pages.getGroup(it)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
saveState()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun loadChapter(chapterId: Long, callback: suspend (List<MangaPage>) -> Unit) {
|
||||
lifecycleScope.launch {
|
||||
readerListener?.onLoadingStateChanged(isLoading = true)
|
||||
try {
|
||||
val pages = withContext(Dispatchers.IO) {
|
||||
val chapter = manga.chapters?.find { it.id == chapterId }
|
||||
?: throw RuntimeException("Chapter $chapterId not found")
|
||||
val repo = MangaProviderFactory.create(manga.source)
|
||||
repo.getPages(chapter)
|
||||
}
|
||||
callback(pages)
|
||||
} catch (_: CancellationException) {
|
||||
} catch (e: Throwable) {
|
||||
readerListener?.onError(e)
|
||||
} finally {
|
||||
readerListener?.onLoadingStateChanged(isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun trimStart() {
|
||||
var removed = 0
|
||||
while (pages.groupCount > 3 && pages.size > 8) {
|
||||
removed += pages.removeFirst().size
|
||||
}
|
||||
if (removed != 0) {
|
||||
adapter?.notifyItemsRemovedStart(removed)
|
||||
Log.i(TAG, "Removed $removed pages from start")
|
||||
}
|
||||
}
|
||||
|
||||
private fun trimEnd() {
|
||||
var removed = 0
|
||||
while (pages.groupCount > 3 && pages.size > 8) {
|
||||
removed += pages.removeLast().size
|
||||
}
|
||||
if (removed != 0) {
|
||||
adapter?.notifyItemsRemovedEnd(removed)
|
||||
Log.i(TAG, "Removed $removed pages from end")
|
||||
}
|
||||
}
|
||||
|
||||
protected fun notifyPageChanged(page: Int) {
|
||||
val chapters = manga.chapters ?: return
|
||||
val chapterId = pages.findGroupByIndex(page) ?: return
|
||||
val chapter = chapters.find { it.id == chapterId } ?: return
|
||||
readerListener?.onPageChanged(
|
||||
chapter = chapter,
|
||||
page = page - pages.getGroupOffset(chapterId),
|
||||
total = pages.getGroup(chapterId)?.size ?: return
|
||||
)
|
||||
}
|
||||
|
||||
protected fun saveState() {
|
||||
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return
|
||||
val page = pages.getRelativeIndex(getCurrentItem())
|
||||
if (page != -1) {
|
||||
readerListener?.saveState(chapterId, page)
|
||||
}
|
||||
Log.i(TAG, "saveState(chapterId=$chapterId, page=$page)")
|
||||
}
|
||||
|
||||
open fun switchPageBy(delta: Int) {
|
||||
setCurrentItem(getCurrentItem() + delta, true)
|
||||
}
|
||||
|
||||
fun updateState(chapterId: Long = 0, pageId: Long = 0) {
|
||||
val currentChapterId = pages.findGroupByIndex(getCurrentItem())
|
||||
if (chapterId != 0L && chapterId != currentChapterId) {
|
||||
pages.clear()
|
||||
adapter?.notifyDataSetChanged()
|
||||
loadChapter(chapterId) {
|
||||
pages.clear()
|
||||
pages.addLast(chapterId, it)
|
||||
adapter?.notifyDataSetChanged()
|
||||
setCurrentItem(
|
||||
if (pageId == 0L) {
|
||||
0
|
||||
} else {
|
||||
it.indexOfFirst { it.id == pageId }.coerceAtLeast(0)
|
||||
}, false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setCurrentItem(
|
||||
if (pageId == 0L) {
|
||||
0
|
||||
} else {
|
||||
val chapterPages = pages.getGroup(currentChapterId ?: return) ?: return
|
||||
chapterPages.indexOfFirst { it.id == pageId }
|
||||
.coerceAtLeast(0) + pages.getGroupOffset(currentChapterId)
|
||||
}, false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
abstract val itemsCount: Int
|
||||
|
||||
protected abstract fun getCurrentItem(): Int
|
||||
|
||||
protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean)
|
||||
|
||||
protected abstract fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter<*>
|
||||
|
||||
protected companion object {
|
||||
|
||||
const val ARG_STATE = "state"
|
||||
private const val TAG = "AbstractReader"
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.koitharu.kotatsu.ui.reader.base
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||
|
||||
abstract class BaseReaderAdapter<E>(private val pages: GroupedList<Long, MangaPage>) :
|
||||
RecyclerView.Adapter<BaseViewHolder<MangaPage, E>>() {
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder<MangaPage, E>, position: Int) {
|
||||
val item = pages[position]
|
||||
holder.bind(item, getExtra(item, position))
|
||||
}
|
||||
|
||||
fun getItem(position: Int) = pages[position]
|
||||
|
||||
fun notifyItemsAppended(count: Int) {
|
||||
notifyItemRangeInserted(pages.size - count, count)
|
||||
}
|
||||
|
||||
fun notifyItemsPrepended(count: Int) {
|
||||
notifyItemRangeInserted(0, count)
|
||||
}
|
||||
|
||||
fun notifyItemsRemovedStart(count: Int) {
|
||||
notifyItemRangeRemoved(0, count)
|
||||
}
|
||||
|
||||
fun notifyItemsRemovedEnd(count: Int) {
|
||||
notifyItemRangeRemoved(pages.size - count, count)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int) = pages[position].id
|
||||
|
||||
final override fun getItemCount() = pages.size
|
||||
|
||||
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<MangaPage, E> {
|
||||
return onCreateViewHolder(parent).also(this::onViewHolderCreated)
|
||||
}
|
||||
|
||||
protected abstract fun getExtra(item: MangaPage, position: Int): E
|
||||
|
||||
protected open fun onViewHolderCreated(holder: BaseViewHolder<MangaPage, E>) = Unit
|
||||
|
||||
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<MangaPage, E>
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package org.koitharu.kotatsu.ui.reader.base
|
||||
|
||||
import java.util.*
|
||||
|
||||
class GroupedList<K, T> {
|
||||
|
||||
private val data = LinkedList<Pair<K, List<T>>>()
|
||||
|
||||
private var intSize: Int = -1
|
||||
private var lruGroup: List<T>? = null
|
||||
private var lruGroupKey: K? = null
|
||||
private var lruGroupFirstIndex = -1
|
||||
|
||||
val size: Int
|
||||
get() {
|
||||
if (intSize < 0) {
|
||||
computeSize()
|
||||
}
|
||||
return intSize
|
||||
}
|
||||
|
||||
val groupCount: Int
|
||||
get() = data.size
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = size == 0
|
||||
|
||||
val isNotEmpty: Boolean
|
||||
get() = size != 0
|
||||
|
||||
operator fun get(index: Int): T {
|
||||
if (index >= lruGroupFirstIndex) {
|
||||
val relIndex = index - lruGroupFirstIndex
|
||||
lruGroup?.let {
|
||||
if (relIndex in it.indices) {
|
||||
return it[relIndex]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intSize < 0 || index < intSize shr 1) {
|
||||
var firstIndex = 0
|
||||
for (entry in data.iterator()) {
|
||||
if (index < firstIndex + entry.second.size && index >= firstIndex) {
|
||||
lruGroup = entry.second
|
||||
lruGroupKey = entry.first
|
||||
lruGroupFirstIndex = firstIndex
|
||||
return entry.second[index - firstIndex]
|
||||
}
|
||||
firstIndex += entry.second.size
|
||||
}
|
||||
} else {
|
||||
var lastIndex = intSize
|
||||
for (entry in data.descendingIterator()) {
|
||||
if (index < lastIndex && index >= lastIndex - entry.second.size) {
|
||||
lruGroup = entry.second
|
||||
lruGroupKey = entry.first
|
||||
lruGroupFirstIndex = lastIndex - entry.second.size
|
||||
return entry.second.get(index - lruGroupFirstIndex)
|
||||
}
|
||||
lastIndex -= entry.second.size
|
||||
}
|
||||
}
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
|
||||
fun getOrNull(index: Int) = try {
|
||||
get(index)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
null
|
||||
}
|
||||
|
||||
fun getLastKey() = data.peekLast()?.first
|
||||
|
||||
fun getFirstKey() = data.peekFirst()?.first
|
||||
|
||||
fun getGroup(key: K): List<T>? {
|
||||
if (key == lruGroupKey && lruGroup != null) {
|
||||
return lruGroup
|
||||
} else {
|
||||
for(entry in data) {
|
||||
if (entry.first == key) {
|
||||
return entry.second
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getRelativeIndex(absIndex: Int): Int {
|
||||
if (absIndex >= lruGroupFirstIndex) {
|
||||
val relIndex = absIndex - lruGroupFirstIndex
|
||||
lruGroup?.let {
|
||||
if (relIndex in it.indices) {
|
||||
return relIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intSize < 0 || absIndex < intSize shr 1) {
|
||||
var firstIndex = 0
|
||||
for (entry in data.iterator()) {
|
||||
if (absIndex < firstIndex + entry.second.size && absIndex >= firstIndex) {
|
||||
return absIndex - firstIndex
|
||||
}
|
||||
firstIndex += entry.second.size
|
||||
}
|
||||
} else {
|
||||
var lastIndex = intSize
|
||||
for (entry in data.descendingIterator()) {
|
||||
if (absIndex < lastIndex && absIndex >= lastIndex - entry.second.size) {
|
||||
return absIndex - lruGroupFirstIndex
|
||||
}
|
||||
lastIndex -= entry.second.size
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun findGroupByIndex(absIndex: Int): K? {
|
||||
if (absIndex >= lruGroupFirstIndex && lruGroupKey != null) {
|
||||
val relIndex = absIndex - lruGroupFirstIndex
|
||||
lruGroup?.let {
|
||||
if (relIndex in it.indices) {
|
||||
return lruGroupKey
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intSize < 0 || absIndex < intSize shr 1) {
|
||||
var firstIndex = 0
|
||||
for (entry in data.iterator()) {
|
||||
if (absIndex < firstIndex + entry.second.size && absIndex >= firstIndex) {
|
||||
return entry.first
|
||||
}
|
||||
firstIndex += entry.second.size
|
||||
}
|
||||
} else {
|
||||
var lastIndex = intSize
|
||||
for (entry in data.descendingIterator()) {
|
||||
if (absIndex < lastIndex && absIndex >= lastIndex - entry.second.size) {
|
||||
return entry.first
|
||||
}
|
||||
lastIndex -= entry.second.size
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getGroupOffset(key: K): Int {
|
||||
if (lruGroupKey == key && lruGroupFirstIndex >= 0) {
|
||||
return lruGroupFirstIndex
|
||||
}
|
||||
var offset = 0
|
||||
for (entry in data) {
|
||||
if (entry.first == key) {
|
||||
return offset
|
||||
}
|
||||
offset += entry.second.size
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun indexOf(item: T): Int {
|
||||
var offset = 0
|
||||
for ((_, list) in data) {
|
||||
for ((i, x) in list.withIndex()) {
|
||||
if (x == item) {
|
||||
return i + offset
|
||||
}
|
||||
}
|
||||
offset += list.size
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun addLast(key: K, items: List<T>) {
|
||||
data.addLast(key to items.toList())
|
||||
if (intSize < 0) {
|
||||
computeSize()
|
||||
} else {
|
||||
intSize += items.size
|
||||
}
|
||||
}
|
||||
|
||||
fun addFirst(key: K, items: List<T>) {
|
||||
data.addFirst(key to items.toList())
|
||||
if (lruGroupFirstIndex >= 0) {
|
||||
lruGroupFirstIndex += items.size
|
||||
}
|
||||
if (intSize < 0) {
|
||||
computeSize()
|
||||
} else {
|
||||
intSize += items.size
|
||||
}
|
||||
}
|
||||
|
||||
fun removeLast(): List<T> {
|
||||
val item = data.removeLast()
|
||||
if (intSize < 0) {
|
||||
computeSize()
|
||||
} else {
|
||||
intSize -= item.second.size
|
||||
}
|
||||
return item.second
|
||||
}
|
||||
|
||||
fun removeFirst(): List<T> {
|
||||
val item = data.removeFirst()
|
||||
if (intSize < 0) {
|
||||
computeSize()
|
||||
} else {
|
||||
intSize -= item.second.size
|
||||
}
|
||||
if (lruGroupFirstIndex >= 0) {
|
||||
lruGroupFirstIndex -= item.second.size
|
||||
}
|
||||
return item.second
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
data.clear()
|
||||
intSize = 0
|
||||
lruGroupFirstIndex = -1
|
||||
lruGroup = null
|
||||
lruGroupKey = null
|
||||
}
|
||||
|
||||
private fun computeSize() {
|
||||
intSize = data.sumBy { it.second.size }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.ui.reader
|
||||
package org.koitharu.kotatsu.ui.reader.base
|
||||
|
||||
interface OnBoundsScrollListener {
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.ui.reader.standard
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import org.koitharu.kotatsu.ui.reader.OnBoundsScrollListener
|
||||
import org.koitharu.kotatsu.ui.reader.base.OnBoundsScrollListener
|
||||
|
||||
class PagerPaginationListener(
|
||||
private val adapter: RecyclerView.Adapter<*>,
|
||||
@@ -10,16 +10,19 @@ class PagerPaginationListener(
|
||||
private val listener: OnBoundsScrollListener
|
||||
) : ViewPager2.OnPageChangeCallback() {
|
||||
|
||||
private var lastItemCountStart = 0
|
||||
private var lastItemCountEnd = 0
|
||||
private var firstItemId: Long = 0
|
||||
private var lastItemId: Long = 0
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
val itemCount = adapter.itemCount
|
||||
if (position <= offset && itemCount != lastItemCountStart) {
|
||||
lastItemCountStart = itemCount
|
||||
if (itemCount == 0) {
|
||||
return
|
||||
}
|
||||
if (position <= offset && adapter.getItemId(0) != firstItemId) {
|
||||
firstItemId = adapter.getItemId(0)
|
||||
listener.onScrolledToStart()
|
||||
} else if (position >= itemCount - offset && itemCount != lastItemCountEnd) {
|
||||
lastItemCountEnd = itemCount
|
||||
} else if (position >= itemCount - offset && adapter.getItemId(itemCount - 1) != lastItemId) {
|
||||
lastItemId = adapter.getItemId(itemCount - 1)
|
||||
listener.onScrolledToEnd()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.koitharu.kotatsu.ui.reader.standard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.fragment_reader_standard.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.reader.base.AbstractReader
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.GroupedList
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class PagerReaderFragment() : AbstractReader(R.layout.fragment_reader_standard) {
|
||||
|
||||
private var paginationListener: PagerPaginationListener? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
paginationListener = PagerPaginationListener(adapter!!, 2, this)
|
||||
pager.adapter = adapter
|
||||
pager.offscreenPageLimit = 2
|
||||
pager.registerOnPageChangeCallback(paginationListener!!)
|
||||
pager.doOnPageChanged(::notifyPageChanged)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
paginationListener = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter<*> {
|
||||
return PagesAdapter(dataSet, loader)
|
||||
}
|
||||
|
||||
override val itemsCount: Int
|
||||
get() = adapter?.itemCount ?: 0
|
||||
|
||||
override fun getCurrentItem() = pager.currentItem
|
||||
|
||||
override fun setCurrentItem(position: Int, isSmooth: Boolean) {
|
||||
pager.setCurrentItem(position, isSmooth)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(state: ReaderState) = PagerReaderFragment().withArgs(1) {
|
||||
putParcelable(ARG_STATE, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,16 @@ package org.koitharu.kotatsu.ui.reader.standard
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.GroupedList
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
|
||||
class PagesAdapter(private val loader: PageLoader) : BaseRecyclerAdapter<MangaPage, Unit>() {
|
||||
class PagesAdapter(
|
||||
pages: GroupedList<Long, MangaPage>,
|
||||
private val loader: PageLoader
|
||||
) : BaseReaderAdapter<Unit>(pages) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup) =
|
||||
PageHolder(parent, loader)
|
||||
|
||||
override fun onGetItemId(item: MangaPage) = item.id
|
||||
override fun onCreateViewHolder(parent: ViewGroup) = PageHolder(parent, loader)
|
||||
|
||||
override fun getExtra(item: MangaPage, position: Int) = Unit
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package org.koitharu.kotatsu.ui.reader.standard
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.fragment_reader_standard.*
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.reader.*
|
||||
import org.koitharu.kotatsu.utils.ext.callOnPageChaneListeners
|
||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||
|
||||
class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_standard),
|
||||
OnBoundsScrollListener {
|
||||
|
||||
private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance)
|
||||
|
||||
private var adapter: PagesAdapter? = null
|
||||
private lateinit var loader: PageLoader
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
loader = PageLoader()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = PagesAdapter(loader)
|
||||
pager.adapter = adapter
|
||||
pager.offscreenPageLimit = 2
|
||||
pager.registerOnPageChangeCallback(PagerPaginationListener(adapter!!, 2, this))
|
||||
pager.doOnPageChanged(::notifyPageChanged)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>, action: ReaderAction) {
|
||||
super.onPagesLoaded(chapterId, pages, action)
|
||||
when (action) {
|
||||
ReaderAction.REPLACE -> adapter?.let {
|
||||
it.replaceData(pages)
|
||||
lastState?.let { state ->
|
||||
if (chapterId == state.chapterId) {
|
||||
pager.setCurrentItem(findChapterOffset(chapterId) + state.page, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
ReaderAction.PREPEND -> adapter?.run {
|
||||
val pos = pager.currentItem
|
||||
prependData(pages)
|
||||
pager.setCurrentItem(pos + pages.size, false)
|
||||
}
|
||||
ReaderAction.APPEND -> adapter?.appendData(pages)
|
||||
}
|
||||
pager.callOnPageChaneListeners()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
loader.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onScrolledToStart() {
|
||||
val prevChapterId = getPrevChapterId()
|
||||
if (prevChapterId != 0L) {
|
||||
presenter.loadChapter(lastState?.manga ?: return, prevChapterId, ReaderAction.PREPEND)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
val nextChapterId = getNextChapterId()
|
||||
if (nextChapterId != 0L) {
|
||||
presenter.loadChapter(lastState?.manga ?: return, nextChapterId, ReaderAction.APPEND)
|
||||
}
|
||||
}
|
||||
|
||||
override val hasItems: Boolean
|
||||
get() = adapter?.hasItems == true
|
||||
|
||||
override val currentPageIndex: Int
|
||||
get() = pager.currentItem
|
||||
|
||||
override val pages: List<MangaPage>
|
||||
get() = adapter?.items.orEmpty()
|
||||
|
||||
override fun setCurrentPage(index: Int, smooth: Boolean) {
|
||||
pager.setCurrentItem(index, smooth)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val SCROLL_OFFSET = 2
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,31 @@ package org.koitharu.kotatsu.ui.reader.wetoon
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.ui.reader.OnBoundsScrollListener
|
||||
import org.koitharu.kotatsu.ui.reader.base.OnBoundsScrollListener
|
||||
|
||||
class ListPaginationListener(
|
||||
private val offset: Int,
|
||||
private val listener: OnBoundsScrollListener
|
||||
) : RecyclerView.OnScrollListener() {
|
||||
|
||||
private var lastItemCountStart = 0
|
||||
private var lastItemCountEnd = 0
|
||||
private var firstItemId: Long = 0
|
||||
private var lastItemId: Long = 0
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
val itemCount = recyclerView.adapter?.itemCount ?: return
|
||||
val adapter = recyclerView.adapter ?: 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
|
||||
val itemCount = adapter.itemCount
|
||||
if (itemCount == 0) {
|
||||
return
|
||||
}
|
||||
if (lastVisiblePosition >= itemCount - offset && adapter.getItemId(itemCount - 1) != lastItemId) {
|
||||
lastItemId = adapter.getItemId(itemCount - 1)
|
||||
listener.onScrolledToEnd()
|
||||
} else if (firstVisiblePosition <= offset && adapter.getItemId(0) != firstItemId) {
|
||||
firstItemId = adapter.getItemId(0)
|
||||
listener.onScrolledToStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,18 @@ package org.koitharu.kotatsu.ui.reader.wetoon
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.GroupedList
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
|
||||
class WebtoonAdapter(private val loader: PageLoader) : BaseRecyclerAdapter<MangaPage, Int>() {
|
||||
class WebtoonAdapter(
|
||||
pages: GroupedList<Long, MangaPage>,
|
||||
private val loader: PageLoader
|
||||
) : BaseReaderAdapter<Int>(pages) {
|
||||
|
||||
var pageGravity: Int = Gravity.TOP
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup) =
|
||||
WebtoonHolder(parent, loader)
|
||||
|
||||
override fun onGetItemId(item: MangaPage) = item.id
|
||||
override fun onCreateViewHolder(parent: ViewGroup) = WebtoonHolder(parent, loader)
|
||||
|
||||
override fun getExtra(item: MangaPage, position: Int) = pageGravity
|
||||
}
|
||||
@@ -3,94 +3,66 @@ package org.koitharu.kotatsu.ui.reader.wetoon
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.fragment_reader_webtoon.*
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.reader.*
|
||||
import org.koitharu.kotatsu.utils.ext.callOnScrollListeners
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||
import org.koitharu.kotatsu.ui.reader.base.AbstractReader
|
||||
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.base.GroupedList
|
||||
import org.koitharu.kotatsu.ui.reader.standard.PagerReaderFragment
|
||||
import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged
|
||||
import org.koitharu.kotatsu.utils.ext.findMiddleVisibleItemPosition
|
||||
import org.koitharu.kotatsu.utils.ext.firstItem
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class WebtoonReaderFragment : BaseReaderFragment(R.layout.fragment_reader_webtoon),
|
||||
OnBoundsScrollListener {
|
||||
class WebtoonReaderFragment : AbstractReader(R.layout.fragment_reader_webtoon) {
|
||||
|
||||
private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance)
|
||||
|
||||
private var adapter: WebtoonAdapter? = null
|
||||
private lateinit var loader: PageLoader
|
||||
private val scrollInterpolator = AccelerateDecelerateInterpolator()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
loader = PageLoader()
|
||||
}
|
||||
protected var paginationListener: ListPaginationListener? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = WebtoonAdapter(loader)
|
||||
paginationListener = ListPaginationListener(2, this)
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(ListPaginationListener(2, this))
|
||||
recyclerView.addOnScrollListener(paginationListener!!)
|
||||
recyclerView.doOnCurrentItemChanged(::notifyPageChanged)
|
||||
}
|
||||
|
||||
override fun onPagesLoaded(chapterId: Long, pages: List<MangaPage>, action: ReaderAction) {
|
||||
super.onPagesLoaded(chapterId, pages, action)
|
||||
when(action) {
|
||||
ReaderAction.REPLACE -> {
|
||||
adapter?.let {
|
||||
it.replaceData(pages)
|
||||
lastState?.let { state ->
|
||||
if (chapterId == state.chapterId) {
|
||||
recyclerView.firstItem = state.page
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ReaderAction.PREPEND -> adapter?.prependData(pages)
|
||||
ReaderAction.APPEND -> adapter?.appendData(pages)
|
||||
}
|
||||
recyclerView.callOnScrollListeners()
|
||||
override fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter<*> {
|
||||
return WebtoonAdapter(dataSet, loader)
|
||||
}
|
||||
|
||||
override fun onScrolledToStart() {
|
||||
val prevChapterId = getPrevChapterId()
|
||||
if (prevChapterId != 0L) {
|
||||
presenter.loadChapter(lastState?.manga ?: return, prevChapterId, ReaderAction.PREPEND)
|
||||
}
|
||||
override fun onDestroyView() {
|
||||
paginationListener = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
val nextChapterId = getNextChapterId()
|
||||
if (nextChapterId != 0L) {
|
||||
presenter.loadChapter(lastState?.manga ?: return, nextChapterId, ReaderAction.APPEND)
|
||||
}
|
||||
override val itemsCount: Int
|
||||
get() = adapter?.itemCount ?: 0
|
||||
|
||||
override fun getCurrentItem(): Int {
|
||||
return (recyclerView.layoutManager as LinearLayoutManager).findMiddleVisibleItemPosition()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
loader.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override val hasItems: Boolean
|
||||
get() = adapter?.hasItems == true
|
||||
|
||||
override val currentPageIndex: Int
|
||||
get() = recyclerView.firstItem
|
||||
|
||||
override val pages: List<MangaPage>
|
||||
get() = adapter?.items.orEmpty()
|
||||
|
||||
override fun setCurrentPage(index: Int, smooth: Boolean) {
|
||||
if (smooth) {
|
||||
recyclerView.smoothScrollToPosition(index)
|
||||
override fun setCurrentItem(position: Int, isSmooth: Boolean) {
|
||||
if (isSmooth) {
|
||||
recyclerView.smoothScrollToPosition(position)
|
||||
} else {
|
||||
recyclerView.firstItem = index
|
||||
recyclerView.firstItem = position
|
||||
}
|
||||
}
|
||||
|
||||
override fun switchPageBy(delta: Int) {
|
||||
recyclerView.smoothScrollBy(0, (recyclerView.height * 0.9).toInt() * delta, scrollInterpolator)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(state: ReaderState) = WebtoonReaderFragment().withArgs(1) {
|
||||
putParcelable(ARG_STATE, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Wed Feb 26 19:30:06 EET 2020
|
||||
#Sun Mar 08 18:35:17 EET 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
|
||||
|
||||
Reference in New Issue
Block a user