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
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
|
||||
@Parcelize
|
||||
data class ReaderState(
|
||||
val manga: Manga,
|
||||
val chapterId: Long,
|
||||
val page: Int,
|
||||
val scroll: Int
|
||||
) : Parcelable {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val chapter: MangaChapter? by lazy {
|
||||
manga.chapters?.find { it.id == chapterId }
|
||||
companion object {
|
||||
|
||||
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 {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.reader.ui.base
|
||||
package org.koitharu.kotatsu.reader.ui.pager
|
||||
|
||||
import android.os.Parcelable
|
||||
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 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.view.ViewGroup
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
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.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) {
|
||||
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 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.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
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.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
import org.koitharu.kotatsu.reader.ui.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.base.PageHolderDelegate
|
||||
import org.koitharu.kotatsu.reader.ui.base.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
open class PageHolder(parent: ViewGroup, loader: PageLoader) :
|
||||
BaseViewHolder<ReaderPage, Unit, ItemPageBinding>(
|
||||
ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
), PageHolderDelegate.Callback, View.OnClickListener {
|
||||
|
||||
private val delegate = PageHolderDelegate(loader, this)
|
||||
open class PageHolder(
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: AppSettings
|
||||
) : BasePageHolder<ItemPageBinding>(binding, loader, settings), View.OnClickListener {
|
||||
|
||||
init {
|
||||
binding.ssiv.setOnImageEventListener(delegate)
|
||||
binding.buttonRetry.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onBind(data: ReaderPage, extra: Unit) {
|
||||
override fun onBind(data: ReaderPage) {
|
||||
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.viewpager2.widget.ViewPager2
|
||||
import org.koitharu.kotatsu.reader.ui.base.OnBoundsScrollListener
|
||||
import org.koitharu.kotatsu.reader.ui.pager.OnBoundsScrollListener
|
||||
|
||||
class PagerPaginationListener(
|
||||
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.RecyclerView
|
||||
import org.koitharu.kotatsu.reader.ui.base.OnBoundsScrollListener
|
||||
import org.koitharu.kotatsu.reader.ui.pager.OnBoundsScrollListener
|
||||
|
||||
class ListPaginationListener(
|
||||
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.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.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.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