Remove presenter from reader fragments

This commit is contained in:
Koitharu
2020-03-09 09:57:25 +02:00
parent 4a4c7108cb
commit fec3481d27
18 changed files with 718 additions and 464 deletions

View File

@@ -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'

View File

@@ -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
)
}
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
)
}
}
}
}
}

View File

@@ -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?)

View File

@@ -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"
}
}

View File

@@ -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>
}

View File

@@ -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 }
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.reader
package org.koitharu.kotatsu.ui.reader.base
interface OnBoundsScrollListener {

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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