Refactor reader
parent
71a5801a0c
commit
904d12f611
@ -0,0 +1,95 @@
|
|||||||
|
package org.koitharu.kotatsu.reader
|
||||||
|
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.utils.GridTouchHelper
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
class ReaderControlDelegate(
|
||||||
|
private val scope: LifecycleCoroutineScope,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
private val listener: OnInteractionListener
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var isTapSwitchEnabled: Boolean = true
|
||||||
|
private var isVolumeKeysSwitchEnabled: Boolean = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
settings.observe()
|
||||||
|
.filter { it == AppSettings.KEY_READER_SWITCHERS }
|
||||||
|
.map { settings.readerPageSwitch }
|
||||||
|
.onStart { emit(settings.readerPageSwitch) }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.onEach {
|
||||||
|
isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in it
|
||||||
|
isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in it
|
||||||
|
}.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onGridTouch(area: Int) {
|
||||||
|
when (area) {
|
||||||
|
GridTouchHelper.AREA_CENTER -> {
|
||||||
|
listener.toggleUiVisibility()
|
||||||
|
}
|
||||||
|
GridTouchHelper.AREA_TOP,
|
||||||
|
GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) {
|
||||||
|
listener.switchPageBy(-1)
|
||||||
|
}
|
||||||
|
GridTouchHelper.AREA_BOTTOM,
|
||||||
|
GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) {
|
||||||
|
listener.switchPageBy(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean = when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_VOLUME_UP -> if (isVolumeKeysSwitchEnabled) {
|
||||||
|
listener.switchPageBy(-1)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_VOLUME_DOWN -> if (isVolumeKeysSwitchEnabled) {
|
||||||
|
listener.switchPageBy(1)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_SPACE,
|
||||||
|
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||||
|
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||||
|
listener.switchPageBy(1)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_PAGE_UP,
|
||||||
|
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP,
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||||
|
listener.switchPageBy(-1)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||||
|
listener.toggleUiVisibility()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
return (isVolumeKeysSwitchEnabled &&
|
||||||
|
(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP))
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnInteractionListener {
|
||||||
|
|
||||||
|
fun switchPageBy(delta: Int)
|
||||||
|
|
||||||
|
fun toggleUiVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
|
||||||
|
data class ReaderContent(
|
||||||
|
val pages: List<ReaderPage>,
|
||||||
|
val state: ReaderState?
|
||||||
|
)
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
|
||||||
|
|
||||||
interface ReaderListener {
|
|
||||||
|
|
||||||
fun onPageChanged(chapter: MangaChapter, page: Int)
|
|
||||||
|
|
||||||
fun saveState(chapterId: Long, page: Int, scroll: Int)
|
|
||||||
|
|
||||||
fun onLoadingStateChanged(isLoading: Boolean)
|
|
||||||
|
|
||||||
fun onError(error: Throwable)
|
|
||||||
}
|
|
||||||
@ -1,21 +1,29 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ReaderState(
|
data class ReaderState(
|
||||||
val manga: Manga,
|
|
||||||
val chapterId: Long,
|
val chapterId: Long,
|
||||||
val page: Int,
|
val page: Int,
|
||||||
val scroll: Int
|
val scroll: Int
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
companion object {
|
||||||
val chapter: MangaChapter? by lazy {
|
|
||||||
manga.chapters?.find { it.id == chapterId }
|
fun from(history: MangaHistory) = ReaderState(
|
||||||
|
chapterId = history.chapterId,
|
||||||
|
page = history.page,
|
||||||
|
scroll = history.scroll
|
||||||
|
)
|
||||||
|
|
||||||
|
fun initial(manga: Manga) = ReaderState(
|
||||||
|
chapterId = manga.chapters?.firstOrNull()?.id ?: error("Cannot find first chapter"),
|
||||||
|
page = 0,
|
||||||
|
scroll = 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,267 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.base
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.collection.LongSparseArray
|
|
||||||
import androidx.core.view.postDelayed
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderListener
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
|
||||||
import org.koitharu.kotatsu.utils.ext.associateByLong
|
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
|
||||||
|
|
||||||
abstract class AbstractReader<B : ViewBinding> : BaseFragment<B>(), OnBoundsScrollListener {
|
|
||||||
|
|
||||||
protected lateinit var manga: Manga
|
|
||||||
private set
|
|
||||||
private lateinit var chapters: LongSparseArray<MangaChapter>
|
|
||||||
protected val loader by lazy(LazyThreadSafetyMode.NONE) {
|
|
||||||
PageLoader()
|
|
||||||
}
|
|
||||||
protected val pages = ArrayDeque<ReaderPage>()
|
|
||||||
protected var readerAdapter: BaseReaderAdapter? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
val itemsCount: Int
|
|
||||||
get() = readerAdapter?.itemCount ?: 0
|
|
||||||
|
|
||||||
val hasItems: Boolean
|
|
||||||
get() = itemsCount != 0
|
|
||||||
|
|
||||||
val currentPage: MangaPage?
|
|
||||||
get() = pages.getOrNull(getCurrentItem())?.toMangaPage()
|
|
||||||
|
|
||||||
private var readerListener: ReaderListener? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
manga = requireNotNull(requireArguments().getParcelable<ReaderState>(ARG_STATE)).manga
|
|
||||||
chapters = requireNotNull(manga.chapters).associateByLong { it.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
readerAdapter = onCreateAdapter(pages)
|
|
||||||
@Suppress("RemoveExplicitTypeArguments")
|
|
||||||
val state = savedInstanceState?.getParcelable<ReaderState>(ARG_STATE)
|
|
||||||
?: requireArguments().getParcelable<ReaderState>(ARG_STATE)!!
|
|
||||||
loadChapter(state.chapterId) {
|
|
||||||
pages.clear()
|
|
||||||
it.mapIndexedTo(pages) { i, p ->
|
|
||||||
ReaderPage.from(p, i, state.chapterId)
|
|
||||||
}
|
|
||||||
readerAdapter?.notifyDataSetChanged()
|
|
||||||
setCurrentItem(state.page, false)
|
|
||||||
if (state.scroll != 0) {
|
|
||||||
restorePageScroll(state.page, state.scroll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
readerListener = activity as? ReaderListener
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
readerListener = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
val page = pages.getOrNull(getCurrentItem()) ?: return
|
|
||||||
outState.putParcelable(
|
|
||||||
ARG_STATE, ReaderState(
|
|
||||||
manga = manga,
|
|
||||||
chapterId = page.chapterId,
|
|
||||||
page = page.index,
|
|
||||||
scroll = getCurrentPageScroll()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScrolledToStart() {
|
|
||||||
val chapterId = getFirstPage()?.chapterId ?: return
|
|
||||||
val index = manga.chapters?.indexOfFirst { it.id == chapterId } ?: return
|
|
||||||
val prevChapterId = manga.chapters!!.getOrNull(index - 1)?.id ?: return
|
|
||||||
loadChapter(prevChapterId) {
|
|
||||||
pages.addAll(0, it.mapIndexed { i, p ->
|
|
||||||
ReaderPage.from(p, i, prevChapterId)
|
|
||||||
})
|
|
||||||
readerAdapter?.notifyItemsPrepended(it.size)
|
|
||||||
view?.postDelayed(500) {
|
|
||||||
trimEnd()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScrolledToEnd() {
|
|
||||||
val chapterId = getLastPage()?.chapterId ?: return
|
|
||||||
val index = manga.chapters?.indexOfLast { it.id == chapterId } ?: return
|
|
||||||
val nextChapterId = manga.chapters!!.getOrNull(index + 1)?.id ?: return
|
|
||||||
loadChapter(nextChapterId) {
|
|
||||||
pages.addAll(it.mapIndexed { i, p ->
|
|
||||||
ReaderPage.from(p, i, nextChapterId)
|
|
||||||
})
|
|
||||||
readerAdapter?.notifyItemsAppended(it.size)
|
|
||||||
view?.postDelayed(500) {
|
|
||||||
trimStart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
readerAdapter = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
loader.dispose()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
open fun recreateAdapter() {
|
|
||||||
readerAdapter = onCreateAdapter(pages)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPages(): List<MangaPage>? {
|
|
||||||
val chapterId = (pages.getOrNull(getCurrentItem()) ?: return null).chapterId
|
|
||||||
// TODO optimize
|
|
||||||
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
saveState()
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadChapter(chapterId: Long, callback: suspend (List<MangaPage>) -> Unit) {
|
|
||||||
viewLifecycleScope.launch {
|
|
||||||
readerListener?.onLoadingStateChanged(isLoading = true)
|
|
||||||
try {
|
|
||||||
val pages = withContext(Dispatchers.Default) {
|
|
||||||
val chapter = chapters.get(chapterId)
|
|
||||||
?: throw RuntimeException("Chapter $chapterId not found")
|
|
||||||
val repo = manga.source.repository
|
|
||||||
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(position: Int) {
|
|
||||||
val page = pages.getOrNull(position) ?: return
|
|
||||||
val chapter = chapters.get(page.chapterId) ?: return
|
|
||||||
readerListener?.onPageChanged(
|
|
||||||
chapter = chapter,
|
|
||||||
page = page.index
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveState() {
|
|
||||||
val page = pages.getOrNull(getCurrentItem()) ?: return
|
|
||||||
readerListener?.saveState(page.chapterId, page.index, getCurrentPageScroll())
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun switchPageBy(delta: Int) {
|
|
||||||
setCurrentItem(getCurrentItem() + delta, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateState(chapterId: Long = 0, pageId: Long = 0) {
|
|
||||||
val currentChapterId = pages.getOrNull(getCurrentItem())?.chapterId ?: 0L
|
|
||||||
if (chapterId != 0L && chapterId != currentChapterId) {
|
|
||||||
pages.clear()
|
|
||||||
readerAdapter?.notifyDataSetChanged()
|
|
||||||
loadChapter(chapterId) {
|
|
||||||
pages.clear()
|
|
||||||
it.mapIndexedTo(pages) { i, p ->
|
|
||||||
ReaderPage.from(p, i, chapterId)
|
|
||||||
}
|
|
||||||
readerAdapter?.notifyDataSetChanged()
|
|
||||||
setCurrentItem(
|
|
||||||
if (pageId == 0L) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
it.indexOfFirst { x -> x.id == pageId }.coerceAtLeast(0)
|
|
||||||
}, false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var index = 0
|
|
||||||
if (pageId != 0L) {
|
|
||||||
index = pages.indexOfFirst {
|
|
||||||
it.chapterId == currentChapterId && it.id == pageId
|
|
||||||
}
|
|
||||||
if (index == -1) { // try to find chapter at least
|
|
||||||
index = pages.indexOfFirst {
|
|
||||||
it.chapterId == currentChapterId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (index == -1) {
|
|
||||||
index = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setCurrentItem(index, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun getLastPage() = pages.lastOrNull()
|
|
||||||
|
|
||||||
protected open fun getFirstPage() = pages.firstOrNull()
|
|
||||||
|
|
||||||
protected abstract fun getCurrentItem(): Int
|
|
||||||
|
|
||||||
protected abstract fun getCurrentPageScroll(): Int
|
|
||||||
|
|
||||||
protected abstract fun restorePageScroll(position: Int, scroll: Int)
|
|
||||||
|
|
||||||
protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean)
|
|
||||||
|
|
||||||
protected abstract fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter
|
|
||||||
|
|
||||||
protected companion object {
|
|
||||||
|
|
||||||
const val ARG_STATE = "state"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.base
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
|
||||||
|
|
||||||
abstract class BaseReaderAdapter(protected val pages: List<ReaderPage>) :
|
|
||||||
RecyclerView.Adapter<BaseViewHolder<ReaderPage, Unit, *>>() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
setHasStableIds(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BaseViewHolder<ReaderPage, Unit, *>, position: Int) {
|
|
||||||
val item = pages[position]
|
|
||||||
holder.bind(item, Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getItem(position: Int) = pages[position]
|
|
||||||
|
|
||||||
open fun notifyItemsAppended(count: Int) {
|
|
||||||
notifyItemRangeInserted(pages.size - count, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun notifyItemsPrepended(count: Int) {
|
|
||||||
notifyItemRangeInserted(0, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun notifyItemsRemovedStart(count: Int) {
|
|
||||||
notifyItemRangeRemoved(0, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
open 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<ReaderPage, Unit, *> {
|
|
||||||
return onCreateViewHolder(parent).also(this::onViewHolderCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun onViewHolderCreated(holder: BaseViewHolder<ReaderPage, Unit, *>) = Unit
|
|
||||||
|
|
||||||
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<ReaderPage, Unit, *>
|
|
||||||
}
|
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||||
|
|
||||||
|
abstract class BasePageHolder<B : ViewBinding>(
|
||||||
|
protected val binding: B,
|
||||||
|
loader: PageLoader,
|
||||||
|
settings: AppSettings
|
||||||
|
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
|
||||||
|
|
||||||
|
protected val delegate = PageHolderDelegate(loader, settings, this)
|
||||||
|
|
||||||
|
val context: Context
|
||||||
|
get() = itemView.context
|
||||||
|
|
||||||
|
var boundData: ReaderPage? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun requireData(): ReaderPage {
|
||||||
|
return checkNotNull(boundData) { "Calling requireData() before bind()" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(data: ReaderPage) {
|
||||||
|
boundData = data
|
||||||
|
onBind(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun onBind(data: ReaderPage)
|
||||||
|
|
||||||
|
protected open fun onRecycled() = Unit
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
|
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||||
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
|
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||||
|
|
||||||
|
abstract class BaseReader<B : ViewBinding> : BaseFragment<B>() {
|
||||||
|
|
||||||
|
protected val viewModel by sharedViewModel<ReaderViewModel>()
|
||||||
|
protected val loader by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
PageLoader(lifecycleScope, get(), get())
|
||||||
|
}
|
||||||
|
private var lastReaderState: ReaderState? = null
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
lastReaderState = savedInstanceState?.getParcelable(KEY_STATE) ?: lastReaderState
|
||||||
|
|
||||||
|
viewModel.content.observe(viewLifecycleOwner) {
|
||||||
|
onPagesChanged(it.pages, lastReaderState ?: it.state)
|
||||||
|
lastReaderState = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
lastReaderState = getCurrentState()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
getCurrentState()?.let {
|
||||||
|
lastReaderState = it
|
||||||
|
}
|
||||||
|
outState.putParcelable(KEY_STATE, lastReaderState)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun switchPageBy(delta: Int)
|
||||||
|
|
||||||
|
abstract fun switchPageTo(position: Int, smooth: Boolean)
|
||||||
|
|
||||||
|
abstract fun getCurrentState(): ReaderState?
|
||||||
|
|
||||||
|
protected abstract fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val KEY_STATE = "state"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
|
||||||
|
private val loader: PageLoader,
|
||||||
|
private val settings: AppSettings
|
||||||
|
) : RecyclerView.Adapter<H>() {
|
||||||
|
|
||||||
|
private val differ = AsyncListDiffer(this, DiffCallback())
|
||||||
|
|
||||||
|
init {
|
||||||
|
setHasStableIds(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: H, position: Int) {
|
||||||
|
holder.bind(differ.currentList[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getItem(position: Int): ReaderPage = differ.currentList[position]
|
||||||
|
|
||||||
|
open fun getItemOrNull(position: Int) = differ.currentList.getOrNull(position)
|
||||||
|
|
||||||
|
override fun getItemId(position: Int) = differ.currentList[position].id
|
||||||
|
|
||||||
|
final override fun getItemCount() = differ.currentList.size
|
||||||
|
|
||||||
|
final override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): H = onCreateViewHolder(parent, loader, settings).also(this::onViewHolderCreated)
|
||||||
|
|
||||||
|
fun setItems(items: List<ReaderPage>, callback: Runnable) {
|
||||||
|
differ.submitList(items, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine<Unit> { cont ->
|
||||||
|
differ.submitList(items) {
|
||||||
|
cont.resume(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun onViewHolderCreated(holder: H) = Unit
|
||||||
|
|
||||||
|
protected abstract fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
loader: PageLoader,
|
||||||
|
settings: AppSettings
|
||||||
|
): H
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<ReaderPage>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: ReaderPage, newItem: ReaderPage): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ReaderPage, newItem: ReaderPage): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.base
|
package org.koitharu.kotatsu.reader.ui.pager
|
||||||
|
|
||||||
interface OnBoundsScrollListener {
|
interface OnBoundsScrollListener {
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.base
|
package org.koitharu.kotatsu.reader.ui.pager
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager
|
||||||
|
|
||||||
|
data class ReaderUiState(
|
||||||
|
val mangaName: String?,
|
||||||
|
val chapterName: String?,
|
||||||
|
val chapterNumber: Int,
|
||||||
|
val chaptersTotal: Int
|
||||||
|
)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.reversed
|
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
@ -1,13 +1,18 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.reversed
|
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||||
|
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.view.ViewGroup
|
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.standard.PageHolder
|
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
|
||||||
|
|
||||||
class ReversedPageHolder(parent: ViewGroup, loader: PageLoader) : PageHolder(parent, loader) {
|
class ReversedPageHolder(
|
||||||
|
binding: ItemPageBinding,
|
||||||
|
loader: PageLoader,
|
||||||
|
settings: AppSettings
|
||||||
|
) : PageHolder(binding, loader, settings) {
|
||||||
|
|
||||||
override fun onImageShowing(zoom: ZoomMode) {
|
override fun onImageShowing(zoom: ZoomMode) {
|
||||||
with(binding.ssiv) {
|
with(binding.ssiv) {
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||||
|
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||||
|
|
||||||
|
class ReversedPagesAdapter(
|
||||||
|
loader: PageLoader,
|
||||||
|
settings: AppSettings
|
||||||
|
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
loader: PageLoader,
|
||||||
|
settings: AppSettings
|
||||||
|
) = ReversedPageHolder(
|
||||||
|
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||||
|
loader = loader,
|
||||||
|
settings = settings
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||||
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||||
|
|
||||||
|
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||||
|
|
||||||
|
private var pagerAdapter: ReversedPagesAdapter? = null
|
||||||
|
|
||||||
|
override fun onInflateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?
|
||||||
|
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
pagerAdapter = ReversedPagesAdapter(loader, get())
|
||||||
|
with(binding.pager) {
|
||||||
|
adapter = pagerAdapter
|
||||||
|
offscreenPageLimit = 2
|
||||||
|
doOnPageChanged(::notifyPageChanged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
pagerAdapter = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun switchPageBy(delta: Int) {
|
||||||
|
with(binding.pager) {
|
||||||
|
setCurrentItem(currentItem - delta, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun switchPageTo(position: Int, smooth: Boolean) {
|
||||||
|
binding.pager.setCurrentItem(reversed(position), smooth)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
|
||||||
|
pagerAdapter?.setItems(pages.asReversed()) {
|
||||||
|
if (pendingState != null) {
|
||||||
|
val position = pages.indexOfFirst {
|
||||||
|
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
||||||
|
}
|
||||||
|
if (position == -1) return@setItems
|
||||||
|
binding.pager.setCurrentItem(position, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentState(): ReaderState? = bindingOrNull()?.run {
|
||||||
|
val adapter = pager.adapter as? BaseReaderAdapter<*>
|
||||||
|
val page = adapter?.getItemOrNull(pager.currentItem) ?: return@run null
|
||||||
|
ReaderState(
|
||||||
|
chapterId = page.chapterId,
|
||||||
|
page = page.index,
|
||||||
|
scroll = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyPageChanged(page: Int) {
|
||||||
|
viewModel.onCurrentPageChanged(reversed(page))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reversed(position: Int): Int {
|
||||||
|
return ((pagerAdapter?.itemCount ?: 0) - position - 1).coerceAtLeast(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.standard
|
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
@ -1,35 +1,32 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.standard
|
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||||
|
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.base.PageHolderDelegate
|
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
|
||||||
open class PageHolder(parent: ViewGroup, loader: PageLoader) :
|
open class PageHolder(
|
||||||
BaseViewHolder<ReaderPage, Unit, ItemPageBinding>(
|
binding: ItemPageBinding,
|
||||||
ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
loader: PageLoader,
|
||||||
), PageHolderDelegate.Callback, View.OnClickListener {
|
settings: AppSettings
|
||||||
|
) : BasePageHolder<ItemPageBinding>(binding, loader, settings), View.OnClickListener {
|
||||||
private val delegate = PageHolderDelegate(loader, this)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.ssiv.setOnImageEventListener(delegate)
|
binding.ssiv.setOnImageEventListener(delegate)
|
||||||
binding.buttonRetry.setOnClickListener(this)
|
binding.buttonRetry.setOnClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(data: ReaderPage, extra: Unit) {
|
override fun onBind(data: ReaderPage) {
|
||||||
delegate.onBind(data.toMangaPage())
|
delegate.onBind(data.toMangaPage())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.standard
|
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import org.koitharu.kotatsu.reader.ui.base.OnBoundsScrollListener
|
import org.koitharu.kotatsu.reader.ui.pager.OnBoundsScrollListener
|
||||||
|
|
||||||
class PagerPaginationListener(
|
class PagerPaginationListener(
|
||||||
private val adapter: RecyclerView.Adapter<*>,
|
private val adapter: RecyclerView.Adapter<*>,
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
||||||
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||||
|
import org.koitharu.kotatsu.utils.ext.swapAdapter
|
||||||
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
|
|
||||||
|
class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||||
|
|
||||||
|
private var pagesAdapter: PagesAdapter? = null
|
||||||
|
|
||||||
|
override fun onInflateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?
|
||||||
|
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
pagesAdapter = PagesAdapter(loader, get())
|
||||||
|
with(binding.pager) {
|
||||||
|
adapter = pagesAdapter
|
||||||
|
offscreenPageLimit = 2
|
||||||
|
doOnPageChanged(::notifyPageChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.readerAnimation.observe(viewLifecycleOwner) {
|
||||||
|
val transformer = if (it) PageAnimTransformer() else null
|
||||||
|
binding.pager.setPageTransformer(transformer)
|
||||||
|
}
|
||||||
|
viewModel.onZoomChanged.observe(viewLifecycleOwner) {
|
||||||
|
pagesAdapter = PagesAdapter(loader, get())
|
||||||
|
binding.pager.swapAdapter(pagesAdapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
pagesAdapter = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
|
||||||
|
viewLifecycleScope.launchWhenCreated {
|
||||||
|
val items = async {
|
||||||
|
pagesAdapter?.setItems(pages)
|
||||||
|
}
|
||||||
|
if (pendingState != null) {
|
||||||
|
val position = pages.indexOfFirst {
|
||||||
|
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
||||||
|
}
|
||||||
|
items.await() ?: return@launchWhenCreated
|
||||||
|
if (position != -1) {
|
||||||
|
binding.pager.setCurrentItem(position, false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun switchPageBy(delta: Int) {
|
||||||
|
with(binding.pager) {
|
||||||
|
setCurrentItem(currentItem + delta, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun switchPageTo(position: Int, smooth: Boolean) {
|
||||||
|
binding.pager.setCurrentItem(position, smooth)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentState(): ReaderState? = bindingOrNull()?.run {
|
||||||
|
val adapter = pager.adapter as? BaseReaderAdapter<*>
|
||||||
|
val page = adapter?.getItemOrNull(pager.currentItem) ?: return@run null
|
||||||
|
ReaderState(
|
||||||
|
chapterId = page.chapterId,
|
||||||
|
page = page.index,
|
||||||
|
scroll = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyPageChanged(page: Int) {
|
||||||
|
viewModel.onCurrentPageChanged(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||||
|
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||||
|
|
||||||
|
class PagesAdapter(
|
||||||
|
loader: PageLoader,
|
||||||
|
settings: AppSettings
|
||||||
|
) : BaseReaderAdapter<PageHolder>(loader, settings) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
loader: PageLoader,
|
||||||
|
settings: AppSettings
|
||||||
|
) = PageHolder(
|
||||||
|
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||||
|
loader = loader,
|
||||||
|
settings = settings
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.koitharu.kotatsu.reader.ui.base.OnBoundsScrollListener
|
import org.koitharu.kotatsu.reader.ui.pager.OnBoundsScrollListener
|
||||||
|
|
||||||
class ListPaginationListener(
|
class ListPaginationListener(
|
||||||
private val offset: Int,
|
private val offset: Int,
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||||
|
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||||
|
|
||||||
|
class WebtoonAdapter(
|
||||||
|
loader: PageLoader,
|
||||||
|
settings: AppSettings
|
||||||
|
) : BaseReaderAdapter<WebtoonHolder>(loader, settings) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
loader: PageLoader,
|
||||||
|
settings: AppSettings
|
||||||
|
) = WebtoonHolder(
|
||||||
|
binding = ItemPageWebtoonBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
loader = loader,
|
||||||
|
settings = settings
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
|
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
||||||
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged
|
||||||
|
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
|
||||||
|
import org.koitharu.kotatsu.utils.ext.firstItem
|
||||||
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
|
|
||||||
|
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
||||||
|
|
||||||
|
private val scrollInterpolator = AccelerateDecelerateInterpolator()
|
||||||
|
private var webtoonAdapter: WebtoonAdapter? = null
|
||||||
|
|
||||||
|
override fun onInflateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?
|
||||||
|
) = FragmentReaderWebtoonBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
webtoonAdapter = WebtoonAdapter(loader, get())
|
||||||
|
with(binding.recyclerView) {
|
||||||
|
setHasFixedSize(true)
|
||||||
|
adapter = webtoonAdapter
|
||||||
|
doOnCurrentItemChanged(::notifyPageChanged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
webtoonAdapter = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) {
|
||||||
|
viewLifecycleScope.launchWhenCreated {
|
||||||
|
val setItems = async { webtoonAdapter?.setItems(pages) }
|
||||||
|
if (pendingState != null) {
|
||||||
|
val position = pages.indexOfFirst {
|
||||||
|
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
||||||
|
}
|
||||||
|
setItems.await() ?: return@launchWhenCreated
|
||||||
|
if (position != -1) {
|
||||||
|
binding.recyclerView.firstItem = position
|
||||||
|
// TODO check
|
||||||
|
(binding.recyclerView.findViewHolderForAdapterPosition(position) as? WebtoonHolder)
|
||||||
|
?.restoreScroll(pendingState.scroll)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setItems.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentState(): ReaderState? = bindingOrNull()?.run {
|
||||||
|
val currentItem = recyclerView.findCenterViewPosition()
|
||||||
|
val adapter = recyclerView.adapter as? BaseReaderAdapter<*>
|
||||||
|
val page = adapter?.getItemOrNull(currentItem) ?: return@run null
|
||||||
|
ReaderState(
|
||||||
|
chapterId = page.chapterId,
|
||||||
|
page = page.index,
|
||||||
|
scroll = (recyclerView.findViewHolderForAdapterPosition(currentItem) as? WebtoonHolder)
|
||||||
|
?.getScrollY() ?: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyPageChanged(page: Int) {
|
||||||
|
viewModel.onCurrentPageChanged(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun switchPageBy(delta: Int) {
|
||||||
|
binding.recyclerView.smoothScrollBy(
|
||||||
|
0,
|
||||||
|
(binding.recyclerView.height * 0.9).toInt() * delta,
|
||||||
|
scrollInterpolator
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun switchPageTo(position: Int, smooth: Boolean) {
|
||||||
|
if (smooth) {
|
||||||
|
binding.recyclerView.smoothScrollToPosition(position)
|
||||||
|
} else {
|
||||||
|
binding.recyclerView.firstItem = position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
package org.koitharu.kotatsu.reader.ui.pager.wetoon
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@ -1,45 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.reversed
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
|
||||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
|
||||||
|
|
||||||
class ReversedPagesAdapter(
|
|
||||||
pages: List<ReaderPage>,
|
|
||||||
private val loader: PageLoader
|
|
||||||
) : BaseReaderAdapter(pages) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup) = ReversedPageHolder(parent, loader)
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BaseViewHolder<ReaderPage, Unit, *>, position: Int) {
|
|
||||||
super.onBindViewHolder(holder, reversed(position))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): ReaderPage {
|
|
||||||
return super.getItem(reversed(position))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return super.getItemId(reversed(position))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun notifyItemsAppended(count: Int) {
|
|
||||||
super.notifyItemsPrepended(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun notifyItemsPrepended(count: Int) {
|
|
||||||
super.notifyItemsAppended(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun notifyItemsRemovedStart(count: Int) {
|
|
||||||
super.notifyItemsRemovedEnd(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun notifyItemsRemovedEnd(count: Int) {
|
|
||||||
super.notifyItemsRemovedStart(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reversed(position: Int) = pages.size - position - 1
|
|
||||||
}
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.reversed
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.AbstractReader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
|
||||||
import org.koitharu.kotatsu.reader.ui.standard.PageAnimTransformer
|
|
||||||
import org.koitharu.kotatsu.reader.ui.standard.PagerPaginationListener
|
|
||||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
|
||||||
import org.koitharu.kotatsu.utils.ext.swapAdapter
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
|
|
||||||
class ReversedReaderFragment : AbstractReader<FragmentReaderStandardBinding>(),
|
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
|
|
||||||
private var paginationListener: PagerPaginationListener? = null
|
|
||||||
private val settings by inject<AppSettings>()
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?
|
|
||||||
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
paginationListener = PagerPaginationListener(readerAdapter!!, 2, this)
|
|
||||||
with(binding.pager) {
|
|
||||||
adapter = readerAdapter
|
|
||||||
if (settings.readerAnimation) {
|
|
||||||
setPageTransformer(ReversedPageAnimTransformer())
|
|
||||||
}
|
|
||||||
offscreenPageLimit = 2
|
|
||||||
registerOnPageChangeCallback(paginationListener!!)
|
|
||||||
doOnPageChanged {
|
|
||||||
notifyPageChanged(reversed(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
settings.subscribe(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
settings.unsubscribe(this)
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
paginationListener = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
|
|
||||||
return ReversedPagesAdapter(dataSet, loader)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun recreateAdapter() {
|
|
||||||
super.recreateAdapter()
|
|
||||||
binding.pager.swapAdapter(readerAdapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentItem() = reversed(binding.pager.currentItem)
|
|
||||||
|
|
||||||
override fun setCurrentItem(position: Int, isSmooth: Boolean) {
|
|
||||||
binding.pager.setCurrentItem(reversed(position), isSmooth)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentPageScroll() = 0
|
|
||||||
|
|
||||||
override fun restorePageScroll(position: Int, scroll: Int) = Unit
|
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
|
||||||
when (key) {
|
|
||||||
AppSettings.KEY_READER_ANIMATION -> {
|
|
||||||
if (settings.readerAnimation) {
|
|
||||||
binding.pager.setPageTransformer(PageAnimTransformer())
|
|
||||||
} else {
|
|
||||||
binding.pager.setPageTransformer(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLastPage() = pages.firstOrNull()
|
|
||||||
|
|
||||||
override fun getFirstPage() = pages.lastOrNull()
|
|
||||||
|
|
||||||
private fun reversed(position: Int) = (itemsCount - position - 1).coerceAtLeast(0)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun newInstance(state: ReaderState) = ReversedReaderFragment().withArgs(1) {
|
|
||||||
putParcelable(ARG_STATE, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.standard
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.AbstractReader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
|
||||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
|
||||||
import org.koitharu.kotatsu.utils.ext.swapAdapter
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
|
|
||||||
class PagerReaderFragment : AbstractReader<FragmentReaderStandardBinding>(),
|
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
|
|
||||||
private var paginationListener: PagerPaginationListener? = null
|
|
||||||
private val settings by inject<AppSettings>()
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?
|
|
||||||
) = FragmentReaderStandardBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
paginationListener = PagerPaginationListener(readerAdapter!!, 2, this)
|
|
||||||
with(binding.pager) {
|
|
||||||
adapter = readerAdapter
|
|
||||||
if (settings.readerAnimation) {
|
|
||||||
setPageTransformer(PageAnimTransformer())
|
|
||||||
}
|
|
||||||
offscreenPageLimit = 2
|
|
||||||
registerOnPageChangeCallback(paginationListener!!)
|
|
||||||
doOnPageChanged(::notifyPageChanged)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
settings.subscribe(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
settings.unsubscribe(this)
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
paginationListener = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
|
|
||||||
return PagesAdapter(dataSet, loader)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun recreateAdapter() {
|
|
||||||
super.recreateAdapter()
|
|
||||||
binding.pager.swapAdapter(readerAdapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentItem() = binding.pager.currentItem
|
|
||||||
|
|
||||||
override fun setCurrentItem(position: Int, isSmooth: Boolean) {
|
|
||||||
binding.pager.setCurrentItem(position, isSmooth)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentPageScroll() = 0
|
|
||||||
|
|
||||||
override fun restorePageScroll(position: Int, scroll: Int) = Unit
|
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
|
||||||
when (key) {
|
|
||||||
AppSettings.KEY_READER_ANIMATION -> {
|
|
||||||
if (settings.readerAnimation) {
|
|
||||||
binding.pager.setPageTransformer(PageAnimTransformer())
|
|
||||||
} else {
|
|
||||||
binding.pager.setPageTransformer(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun newInstance(state: ReaderState) = PagerReaderFragment().withArgs(1) {
|
|
||||||
putParcelable(ARG_STATE, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.standard
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
|
||||||
|
|
||||||
class PagesAdapter(
|
|
||||||
pages: List<ReaderPage>,
|
|
||||||
private val loader: PageLoader
|
|
||||||
) : BaseReaderAdapter(pages) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup) = PageHolder(parent, loader)
|
|
||||||
}
|
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui.thumbnails
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
|
||||||
|
data class PageThumbnail(
|
||||||
|
val number: Int,
|
||||||
|
val isCurrent: Boolean,
|
||||||
|
val repository: MangaRepository,
|
||||||
|
val page: MangaPage
|
||||||
|
)
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
|
||||||
|
|
||||||
class WebtoonAdapter(
|
|
||||||
pages: List<ReaderPage>,
|
|
||||||
private val loader: PageLoader
|
|
||||||
) : BaseReaderAdapter(pages) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup) = WebtoonHolder(parent, loader)
|
|
||||||
}
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.wetoon
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.AbstractReader
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.BaseReaderAdapter
|
|
||||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
|
||||||
import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged
|
|
||||||
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
|
|
||||||
import org.koitharu.kotatsu.utils.ext.firstItem
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
|
|
||||||
class WebtoonReaderFragment : AbstractReader<FragmentReaderWebtoonBinding>() {
|
|
||||||
|
|
||||||
private val scrollInterpolator = AccelerateDecelerateInterpolator()
|
|
||||||
private var paginationListener: ListPaginationListener? = null
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?
|
|
||||||
) = FragmentReaderWebtoonBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
paginationListener = ListPaginationListener(2, this)
|
|
||||||
with(binding.recyclerView) {
|
|
||||||
setHasFixedSize(true)
|
|
||||||
adapter = readerAdapter
|
|
||||||
addOnScrollListener(paginationListener!!)
|
|
||||||
doOnCurrentItemChanged(::notifyPageChanged)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
|
|
||||||
return WebtoonAdapter(dataSet, loader)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun recreateAdapter() {
|
|
||||||
super.recreateAdapter()
|
|
||||||
binding.recyclerView.swapAdapter(readerAdapter, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
paginationListener = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentItem(): Int {
|
|
||||||
return binding.recyclerView.findCenterViewPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setCurrentItem(position: Int, isSmooth: Boolean) {
|
|
||||||
if (isSmooth) {
|
|
||||||
binding.recyclerView.smoothScrollToPosition(position)
|
|
||||||
} else {
|
|
||||||
binding.recyclerView.firstItem = position
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun switchPageBy(delta: Int) {
|
|
||||||
binding.recyclerView.smoothScrollBy(
|
|
||||||
0,
|
|
||||||
(binding.recyclerView.height * 0.9).toInt() * delta,
|
|
||||||
scrollInterpolator
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentPageScroll(): Int {
|
|
||||||
return (binding.recyclerView.findViewHolderForAdapterPosition(getCurrentItem()) as? WebtoonHolder)
|
|
||||||
?.getScrollY() ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun restorePageScroll(position: Int, scroll: Int) {
|
|
||||||
binding.recyclerView.post {
|
|
||||||
val holder = binding.recyclerView.findViewHolderForAdapterPosition(position) ?: return@post
|
|
||||||
(holder as WebtoonHolder).restoreScroll(scroll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun newInstance(state: ReaderState) = WebtoonReaderFragment().withArgs(1) {
|
|
||||||
putParcelable(ARG_STATE, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package org.koitharu.kotatsu.utils
|
||||||
|
|
||||||
|
fun interface BufferedObserver<T> {
|
||||||
|
|
||||||
|
fun onChanged(t: T, previous: T?)
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package org.koitharu.kotatsu.utils
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class FlowLiveEvent<T>(
|
||||||
|
private val source: Flow<T>,
|
||||||
|
private val context: CoroutineContext
|
||||||
|
) : LiveData<T>() {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(
|
||||||
|
Dispatchers.Main.immediate + context + SupervisorJob(context[Job])
|
||||||
|
)
|
||||||
|
private val pending = AtomicBoolean(false)
|
||||||
|
private var collectJob: Job? = null
|
||||||
|
|
||||||
|
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
|
||||||
|
super.observe(owner) {
|
||||||
|
if (pending.compareAndSet(true, false)) {
|
||||||
|
observer.onChanged(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActive() {
|
||||||
|
super.onActive()
|
||||||
|
if (collectJob == null) {
|
||||||
|
collectJob = source.onEach {
|
||||||
|
setValue(it)
|
||||||
|
}.launchIn(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInactive() {
|
||||||
|
collectJob?.cancel()
|
||||||
|
collectJob = null
|
||||||
|
super.onInactive()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(value: T) {
|
||||||
|
pending.set(true)
|
||||||
|
super.setValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue