Use ArrayDeque in reader

pull/26/head
Koitharu 6 years ago
parent a885709ba9
commit 72fdc7796f

@ -63,8 +63,8 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
implementation 'androidx.core:core-ktx:1.5.0-alpha04' implementation 'androidx.core:core-ktx:1.5.0-alpha04'
implementation 'androidx.activity:activity-ktx:1.2.0-beta01' implementation 'androidx.activity:activity-ktx:1.2.0-beta01'

@ -5,7 +5,6 @@ import android.view.View
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.fragment_details.* import kotlinx.android.synthetic.main.fragment_details.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -73,7 +72,7 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
) )
} }
manga.url.toUri().toFileOrNull()?.let { f -> manga.url.toUri().toFileOrNull()?.let { f ->
lifecycleScope.launch { viewLifecycleScope.launch {
val size = withContext(Dispatchers.IO) { val size = withContext(Dispatchers.IO) {
f.length() f.length()
} }

@ -27,7 +27,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
private val convertLock = Mutex() private val convertLock = Mutex()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.Main.immediate + job get() = job + Dispatchers.Main.immediate
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
suspend fun loadFile(url: String, force: Boolean): File { suspend fun loadFile(url: String, force: Boolean): File {
@ -88,7 +88,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
} }
override fun dispose() { override fun dispose() {
coroutineContext.cancel() job.cancelChildren()
tasks.clear() tasks.clear()
} }
} }

@ -14,14 +14,14 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.graphics.Insets
import androidx.core.view.postDelayed import androidx.core.view.*
import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -53,7 +53,7 @@ import org.koitharu.kotatsu.utils.ext.*
class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnChapterChangeListener, class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnChapterChangeListener,
GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback, GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback,
ReaderListener, SharedPreferences.OnSharedPreferenceChangeListener, ReaderListener, SharedPreferences.OnSharedPreferenceChangeListener,
View.OnApplyWindowInsetsListener, ActivityResultCallback<Boolean> { ActivityResultCallback<Boolean>, OnApplyWindowInsetsListener {
private val presenter by moxyPresenter(factory = ::ReaderPresenter) private val presenter by moxyPresenter(factory = ::ReaderPresenter)
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
@ -93,7 +93,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
getString(R.string.chapter_d_of_d, state.chapter?.number ?: 0, size) getString(R.string.chapter_d_of_d, state.chapter?.number ?: 0, size)
} }
rootLayout.setOnApplyWindowInsetsListener(this) ViewCompat.setOnApplyWindowInsetsListener(rootLayout, this)
settings.subscribe(this) settings.subscribe(this)
loadSettings() loadSettings()
@ -105,10 +105,8 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) { if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
presenter.init(state.manga) presenter.init(state.manga)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
GlobalScope.launch { GlobalScope.launch(Dispatchers.Main + IgnoreErrors) {
safe { MangaShortcut(state.manga).addAppShortcut(applicationContext)
MangaShortcut(state.manga).addAppShortcut(applicationContext)
}
} }
} }
} }
@ -191,7 +189,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
R.id.action_pages_thumbs -> { R.id.action_pages_thumbs -> {
if (reader?.hasItems == true) { if (reader?.hasItems == true) {
val pages = reader?.getPages() val pages = reader?.getPages()
if (pages != null) { if (!pages.isNullOrEmpty()) {
PagesThumbnailsSheet.show( PagesThumbnailsSheet.show(
supportFragmentManager, pages, supportFragmentManager, pages,
state.chapter?.name ?: title?.toString().orEmpty() state.chapter?.name ?: title?.toString().orEmpty()
@ -363,7 +361,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
} }
} }
override fun onPageChanged(chapter: MangaChapter, page: Int, total: Int) { override fun onPageChanged(chapter: MangaChapter, page: Int) {
title = chapter.name title = chapter.name
state.manga.chapters?.run { state.manga.chapters?.run {
supportActionBar?.subtitle = supportActionBar?.subtitle =
@ -395,18 +393,21 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
} }
} }
override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets { override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
appbar_top.updatePadding( appbar_top.updatePadding(
top = insets.systemWindowInsetTop, top = systemBars.top,
right = insets.systemWindowInsetRight, right = systemBars.right,
left = insets.systemWindowInsetLeft left = systemBars.left
) )
appbar_bottom.updatePadding( appbar_bottom.updatePadding(
bottom = insets.systemWindowInsetBottom, bottom = systemBars.bottom,
right = insets.systemWindowInsetRight, right = systemBars.right,
left = insets.systemWindowInsetLeft left = systemBars.left
) )
return insets.consumeSystemWindowInsets() return WindowInsetsCompat.Builder(insets)
.setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)
.build()
} }
private fun loadSettings() { private fun loadSettings() {

@ -5,7 +5,7 @@ import org.koitharu.kotatsu.ui.base.BaseMvpView
interface ReaderListener : BaseMvpView { interface ReaderListener : BaseMvpView {
fun onPageChanged(chapter: MangaChapter, page: Int, total: Int) fun onPageChanged(chapter: MangaChapter, page: Int)
fun saveState(chapterId: Long, page: Int, scroll: Int) fun saveState(chapterId: Long, page: Int, scroll: Int)
} }

@ -1,45 +1,52 @@
package org.koitharu.kotatsu.ui.reader.base package org.koitharu.kotatsu.ui.reader.base
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View import android.view.View
import androidx.collection.LongSparseArray
import androidx.core.view.postDelayed import androidx.core.view.postDelayed
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
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.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.base.BaseFragment import org.koitharu.kotatsu.ui.base.BaseFragment
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.ReaderListener import org.koitharu.kotatsu.ui.reader.ReaderListener
import org.koitharu.kotatsu.ui.reader.ReaderState import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.utils.ext.associateByLong
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayoutId), abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayoutId),
OnBoundsScrollListener { OnBoundsScrollListener {
protected lateinit var manga: Manga protected lateinit var manga: Manga
protected lateinit var loader: PageLoader
private set private set
private lateinit var pages: GroupedList<Long, MangaPage> private lateinit var chapters: LongSparseArray<MangaChapter>
protected val loader by lazy(LazyThreadSafetyMode.NONE) {
PageLoader()
}
protected val pages = ArrayDeque<ReaderPage>()
protected var adapter: BaseReaderAdapter? = null protected var adapter: BaseReaderAdapter? = null
private set private set
val itemsCount: Int
get() = adapter?.itemCount ?: 0
val hasItems: Boolean val hasItems: Boolean
get() = itemsCount != 0 get() = itemsCount != 0
val currentPage: MangaPage? val currentPage: MangaPage?
get() = pages.getOrNull(getCurrentItem()) get() = pages.getOrNull(getCurrentItem())?.toMangaPage()
protected val readerListener: ReaderListener? private var readerListener: ReaderListener? = null
get() = activity as? ReaderListener
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
pages = GroupedList() manga = requireNotNull(requireArguments().getParcelable<ReaderState>(ARG_STATE)).manga
manga = requireArguments().getParcelable<ReaderState>(ARG_STATE)!!.manga chapters = requireNotNull(manga.chapters).associateByLong { it.id }
loader = PageLoader()
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -50,7 +57,9 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
?: requireArguments().getParcelable<ReaderState>(ARG_STATE)!! ?: requireArguments().getParcelable<ReaderState>(ARG_STATE)!!
loadChapter(state.chapterId) { loadChapter(state.chapterId) {
pages.clear() pages.clear()
pages.addLast(state.chapterId, it) it.mapIndexedTo(pages) { i, p ->
ReaderPage.from(p, i, state.chapterId)
}
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
setCurrentItem(state.page, false) setCurrentItem(state.page, false)
if (state.scroll != 0) { if (state.scroll != 0) {
@ -59,24 +68,37 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
} }
} }
override fun onAttach(context: Context) {
super.onAttach(context)
readerListener = activity as? ReaderListener
}
override fun onDetach() {
readerListener = null
super.onDetach()
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
val page = pages.getOrNull(getCurrentItem()) ?: return
outState.putParcelable( outState.putParcelable(
ARG_STATE, ReaderState( ARG_STATE, ReaderState(
manga = manga, manga = manga,
chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return, chapterId = page.chapterId,
page = pages.getRelativeIndex(getCurrentItem()), page = page.index,
scroll = getCurrentPageScroll() scroll = getCurrentPageScroll()
) )
) )
} }
override fun onScrolledToStart() { override fun onScrolledToStart() {
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return val chapterId = getFirstPage()?.chapterId ?: return
val index = manga.chapters?.indexOfFirst { it.id == chapterId } ?: return val index = manga.chapters?.indexOfFirst { it.id == chapterId } ?: return
val prevChapterId = manga.chapters!!.getOrNull(index - 1)?.id ?: return val prevChapterId = manga.chapters!!.getOrNull(index - 1)?.id ?: return
loadChapter(prevChapterId) { loadChapter(prevChapterId) {
pages.addFirst(prevChapterId, it) pages.addAll(0, it.mapIndexed { i, p ->
ReaderPage.from(p, i, prevChapterId)
})
adapter?.notifyItemsPrepended(it.size) adapter?.notifyItemsPrepended(it.size)
view?.postDelayed(500) { view?.postDelayed(500) {
trimEnd() trimEnd()
@ -85,11 +107,13 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
} }
override fun onScrolledToEnd() { override fun onScrolledToEnd() {
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return val chapterId = getLastPage()?.chapterId ?: return
val index = manga.chapters?.indexOfFirst { it.id == chapterId } ?: return val index = manga.chapters?.indexOfLast { it.id == chapterId } ?: return
val nextChapterId = manga.chapters!!.getOrNull(index + 1)?.id ?: return val nextChapterId = manga.chapters!!.getOrNull(index + 1)?.id ?: return
loadChapter(nextChapterId) { loadChapter(nextChapterId) {
pages.addLast(nextChapterId, it) pages.addAll(it.mapIndexed { i, p ->
ReaderPage.from(p, i, nextChapterId)
})
adapter?.notifyItemsAppended(it.size) adapter?.notifyItemsAppended(it.size)
view?.postDelayed(500) { view?.postDelayed(500) {
trimStart() trimStart()
@ -107,8 +131,10 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
super.onDestroy() super.onDestroy()
} }
fun getPages() = pages.findGroupByIndex(getCurrentItem())?.let { fun getPages(): List<MangaPage>? {
pages.getGroup(it) val chapterId = (pages.getOrNull(getCurrentItem()) ?: return null).chapterId
// TODO optimize
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
} }
override fun onPause() { override fun onPause() {
@ -117,11 +143,11 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
} }
private fun loadChapter(chapterId: Long, callback: suspend (List<MangaPage>) -> Unit) { private fun loadChapter(chapterId: Long, callback: suspend (List<MangaPage>) -> Unit) {
lifecycleScope.launch { viewLifecycleScope.launch {
readerListener?.onLoadingStateChanged(isLoading = true) readerListener?.onLoadingStateChanged(isLoading = true)
try { try {
val pages = withContext(Dispatchers.IO) { val pages = withContext(Dispatchers.Default) {
val chapter = manga.chapters?.find { it.id == chapterId } val chapter = chapters.get(chapterId)
?: throw RuntimeException("Chapter $chapterId not found") ?: throw RuntimeException("Chapter $chapterId not found")
val repo = manga.source.repository val repo = manga.source.repository
repo.getPages(chapter) repo.getPages(chapter)
@ -137,45 +163,39 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
} }
private fun trimStart() { private fun trimStart() {
var removed = 0 /*var removed = 0
while (pages.groupCount > 3 && pages.size > 8) { while (pages.groupCount > 3 && pages.size > 8) {
removed += pages.removeFirst().size removed += pages.removeFirst().size
} }
if (removed != 0) { if (removed != 0) {
adapter?.notifyItemsRemovedStart(removed) adapter?.notifyItemsRemovedStart(removed)
Log.i(TAG, "Removed $removed pages from start") Log.i(TAG, "Removed $removed pages from start")
} }*/
} }
private fun trimEnd() { private fun trimEnd() {
var removed = 0 /*var removed = 0
while (pages.groupCount > 3 && pages.size > 8) { while (pages.groupCount > 3 && pages.size > 8) {
removed += pages.removeLast().size removed += pages.removeLast().size
} }
if (removed != 0) { if (removed != 0) {
adapter?.notifyItemsRemovedEnd(removed) adapter?.notifyItemsRemovedEnd(removed)
Log.i(TAG, "Removed $removed pages from end") Log.i(TAG, "Removed $removed pages from end")
} }*/
} }
protected fun notifyPageChanged(page: Int) { protected fun notifyPageChanged(position: Int) {
val chapters = manga.chapters ?: return val page = pages.getOrNull(position) ?: return
val chapterId = pages.findGroupByIndex(page) ?: return val chapter = chapters.get(page.chapterId) ?: return
val chapter = chapters.find { it.id == chapterId } ?: return
readerListener?.onPageChanged( readerListener?.onPageChanged(
chapter = chapter, chapter = chapter,
page = page - pages.getGroupOffset(chapterId), page = page.index
total = pages.getGroup(chapterId)?.size ?: return
) )
} }
protected fun saveState() { private fun saveState() {
val chapterId = pages.findGroupByIndex(getCurrentItem()) ?: return val page = pages.getOrNull(getCurrentItem()) ?: return
val page = pages.getRelativeIndex(getCurrentItem()) readerListener?.saveState(page.chapterId, page.index, getCurrentPageScroll())
if (page != -1) {
readerListener?.saveState(chapterId, page, getCurrentPageScroll())
}
Log.i(TAG, "saveState(chapterId=$chapterId, page=$page)")
} }
open fun switchPageBy(delta: Int) { open fun switchPageBy(delta: Int) {
@ -183,13 +203,15 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
} }
fun updateState(chapterId: Long = 0, pageId: Long = 0) { fun updateState(chapterId: Long = 0, pageId: Long = 0) {
val currentChapterId = pages.findGroupByIndex(getCurrentItem()) val currentChapterId = pages.getOrNull(getCurrentItem())?.chapterId ?: 0L
if (chapterId != 0L && chapterId != currentChapterId) { if (chapterId != 0L && chapterId != currentChapterId) {
pages.clear() pages.clear()
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
loadChapter(chapterId) { loadChapter(chapterId) {
pages.clear() pages.clear()
pages.addLast(chapterId, it) it.mapIndexedTo(pages) { i, p ->
ReaderPage.from(p, i, chapterId)
}
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
setCurrentItem( setCurrentItem(
if (pageId == 0L) { if (pageId == 0L) {
@ -200,19 +222,27 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
) )
} }
} else { } else {
setCurrentItem( var index = 0
if (pageId == 0L) { if (pageId != 0L) {
0 index = pages.indexOfFirst {
} else { it.chapterId == currentChapterId && it.id == pageId
val chapterPages = pages.getGroup(currentChapterId ?: return) ?: return }
chapterPages.indexOfFirst { it.id == pageId } if (index == -1) { // try to find chapter at least
.coerceAtLeast(0) + pages.getGroupOffset(currentChapterId) index = pages.indexOfFirst {
}, false it.chapterId == currentChapterId
) }
}
if (index == -1) {
index = 0
}
}
setCurrentItem(index, false)
} }
} }
abstract val itemsCount: Int protected open fun getLastPage() = pages.lastOrNull()
protected open fun getFirstPage() = pages.firstOrNull()
protected abstract fun getCurrentItem(): Int protected abstract fun getCurrentItem(): Int
@ -222,12 +252,10 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean) protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean)
protected abstract fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter protected abstract fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter
protected companion object { protected companion object {
const val ARG_STATE = "state" const val ARG_STATE = "state"
private const val TAG = "AbstractReader"
} }
} }

@ -2,18 +2,17 @@ package org.koitharu.kotatsu.ui.reader.base
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder import org.koitharu.kotatsu.ui.base.list.BaseViewHolder
abstract class BaseReaderAdapter(protected val pages: GroupedList<Long, MangaPage>) : abstract class BaseReaderAdapter(protected val pages: List<ReaderPage>) :
RecyclerView.Adapter<BaseViewHolder<MangaPage, Unit>>() { RecyclerView.Adapter<BaseViewHolder<ReaderPage, Unit>>() {
init { init {
@Suppress("LeakingThis") @Suppress("LeakingThis")
setHasStableIds(true) setHasStableIds(true)
} }
override fun onBindViewHolder(holder: BaseViewHolder<MangaPage, Unit>, position: Int) { override fun onBindViewHolder(holder: BaseViewHolder<ReaderPage, Unit>, position: Int) {
val item = pages[position] val item = pages[position]
holder.bind(item, Unit) holder.bind(item, Unit)
} }
@ -36,18 +35,18 @@ abstract class BaseReaderAdapter(protected val pages: GroupedList<Long, MangaPag
notifyItemRangeRemoved(pages.size - count, count) notifyItemRangeRemoved(pages.size - count, count)
} }
open override fun getItemId(position: Int) = pages[position].id override fun getItemId(position: Int) = pages[position].id
final override fun getItemCount() = pages.size final override fun getItemCount() = pages.size
final override fun onCreateViewHolder( final override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): BaseViewHolder<MangaPage, Unit> { ): BaseViewHolder<ReaderPage, Unit> {
return onCreateViewHolder(parent).also(this::onViewHolderCreated) return onCreateViewHolder(parent).also(this::onViewHolderCreated)
} }
protected open fun onViewHolderCreated(holder: BaseViewHolder<MangaPage, Unit>) = Unit protected open fun onViewHolderCreated(holder: BaseViewHolder<ReaderPage, Unit>) = Unit
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<MangaPage, Unit> protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<ReaderPage, Unit>
} }

@ -1,229 +0,0 @@
package org.koitharu.kotatsu.ui.reader.base
import java.util.*
class GroupedList<K, T> {
private val data = LinkedList<Pair<K, List<T>>>()
private var intSize: Int = -1
private var lruGroup: List<T>? = null
private var lruGroupKey: K? = null
private var lruGroupFirstIndex = -1
val size: Int
get() {
if (intSize < 0) {
computeSize()
}
return intSize
}
val groupCount: Int
get() = data.size
val isEmpty: Boolean
get() = size == 0
val isNotEmpty: Boolean
get() = size != 0
operator fun get(index: Int): T {
if (index >= lruGroupFirstIndex) {
val relIndex = index - lruGroupFirstIndex
lruGroup?.let {
if (relIndex in it.indices) {
return it[relIndex]
}
}
}
if (intSize < 0 || index < intSize shr 1) {
var firstIndex = 0
for (entry in data.iterator()) {
if (index < firstIndex + entry.second.size && index >= firstIndex) {
lruGroup = entry.second
lruGroupKey = entry.first
lruGroupFirstIndex = firstIndex
return entry.second[index - firstIndex]
}
firstIndex += entry.second.size
}
} else {
var lastIndex = intSize
for (entry in data.descendingIterator()) {
if (index < lastIndex && index >= lastIndex - entry.second.size) {
lruGroup = entry.second
lruGroupKey = entry.first
lruGroupFirstIndex = lastIndex - entry.second.size
return entry.second[index - lruGroupFirstIndex]
}
lastIndex -= entry.second.size
}
}
throw IndexOutOfBoundsException()
}
fun getOrNull(index: Int) = try {
get(index)
} catch (e: IndexOutOfBoundsException) {
null
}
fun getLastKey() = data.peekLast()?.first
fun getFirstKey() = data.peekFirst()?.first
fun getGroup(key: K): List<T>? {
if (key == lruGroupKey && lruGroup != null) {
return lruGroup
} else {
for(entry in data) {
if (entry.first == key) {
return entry.second
}
}
}
return null
}
fun getRelativeIndex(absIndex: Int): Int {
if (absIndex >= lruGroupFirstIndex) {
val relIndex = absIndex - lruGroupFirstIndex
lruGroup?.let {
if (relIndex in it.indices) {
return relIndex
}
}
}
if (intSize < 0 || absIndex < intSize shr 1) {
var firstIndex = 0
for (entry in data.iterator()) {
if (absIndex < firstIndex + entry.second.size && absIndex >= firstIndex) {
return absIndex - firstIndex
}
firstIndex += entry.second.size
}
} else {
var lastIndex = intSize
for (entry in data.descendingIterator()) {
if (absIndex < lastIndex && absIndex >= lastIndex - entry.second.size) {
return absIndex - lruGroupFirstIndex
}
lastIndex -= entry.second.size
}
}
return -1
}
fun findGroupByIndex(absIndex: Int): K? {
if (absIndex >= lruGroupFirstIndex && lruGroupKey != null) {
val relIndex = absIndex - lruGroupFirstIndex
lruGroup?.let {
if (relIndex in it.indices) {
return lruGroupKey
}
}
}
if (intSize < 0 || absIndex < intSize shr 1) {
var firstIndex = 0
for (entry in data.iterator()) {
if (absIndex < firstIndex + entry.second.size && absIndex >= firstIndex) {
return entry.first
}
firstIndex += entry.second.size
}
} else {
var lastIndex = intSize
for (entry in data.descendingIterator()) {
if (absIndex < lastIndex && absIndex >= lastIndex - entry.second.size) {
return entry.first
}
lastIndex -= entry.second.size
}
}
return null
}
fun getGroupOffset(key: K): Int {
if (lruGroupKey == key && lruGroupFirstIndex >= 0) {
return lruGroupFirstIndex
}
var offset = 0
for (entry in data) {
if (entry.first == key) {
return offset
}
offset += entry.second.size
}
return -1
}
fun indexOf(item: T): Int {
var offset = 0
for ((_, list) in data) {
for ((i, x) in list.withIndex()) {
if (x == item) {
return i + offset
}
}
offset += list.size
}
return -1
}
fun addLast(key: K, items: List<T>) {
data.addLast(key to items.toList())
if (intSize < 0) {
computeSize()
} else {
intSize += items.size
}
}
fun addFirst(key: K, items: List<T>) {
data.addFirst(key to items.toList())
if (lruGroupFirstIndex >= 0) {
lruGroupFirstIndex += items.size
}
if (intSize < 0) {
computeSize()
} else {
intSize += items.size
}
}
fun removeLast(): List<T> {
val item = data.removeLast()
if (intSize < 0) {
computeSize()
} else {
intSize -= item.second.size
}
return item.second
}
fun removeFirst(): List<T> {
val item = data.removeFirst()
if (intSize < 0) {
computeSize()
} else {
intSize -= item.second.size
}
if (lruGroupFirstIndex >= 0) {
lruGroupFirstIndex -= item.second.size
}
return item.second
}
fun clear() {
data.clear()
intSize = 0
lruGroupFirstIndex = -1
lruGroup = null
lruGroupKey = null
}
private fun computeSize() {
intSize = data.sumBy { it.second.size }
}
}

@ -0,0 +1,36 @@
package org.koitharu.kotatsu.ui.reader.base
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.MangaSource
@Parcelize
data class ReaderPage(
val id: Long,
val url: String,
val preview: String?,
val chapterId: Long,
val index: Int,
val source: MangaSource
) : Parcelable {
fun toMangaPage() = MangaPage(
id = id,
url = url,
preview = preview,
source = source
)
companion object {
fun from(page: MangaPage, index: Int, chapterId: Long) = ReaderPage(
id = page.id,
url = page.url,
preview = page.preview,
chapterId = chapterId,
index = index,
source = page.source
)
}
}

@ -1,25 +1,24 @@
package org.koitharu.kotatsu.ui.reader.reversed package org.koitharu.kotatsu.ui.reader.reversed
import android.view.ViewGroup import android.view.ViewGroup
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder import org.koitharu.kotatsu.ui.base.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
import org.koitharu.kotatsu.ui.reader.base.GroupedList import org.koitharu.kotatsu.ui.reader.base.ReaderPage
import org.koitharu.kotatsu.ui.reader.standard.PageHolder import org.koitharu.kotatsu.ui.reader.standard.PageHolder
class ReversedPagesAdapter( class ReversedPagesAdapter(
pages: GroupedList<Long, MangaPage>, pages: List<ReaderPage>,
private val loader: PageLoader private val loader: PageLoader
) : BaseReaderAdapter(pages) { ) : BaseReaderAdapter(pages) {
override fun onCreateViewHolder(parent: ViewGroup) = PageHolder(parent, loader) override fun onCreateViewHolder(parent: ViewGroup) = PageHolder(parent, loader)
override fun onBindViewHolder(holder: BaseViewHolder<MangaPage, Unit>, position: Int) { override fun onBindViewHolder(holder: BaseViewHolder<ReaderPage, Unit>, position: Int) {
super.onBindViewHolder(holder, reversed(position)) super.onBindViewHolder(holder, reversed(position))
} }
override fun getItem(position: Int): MangaPage { override fun getItem(position: Int): ReaderPage {
return super.getItem(reversed(position)) return super.getItem(reversed(position))
} }

@ -7,12 +7,11 @@ import android.view.View
import kotlinx.android.synthetic.main.fragment_reader_standard.* import kotlinx.android.synthetic.main.fragment_reader_standard.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.ui.reader.ReaderState import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.reader.base.AbstractReader import org.koitharu.kotatsu.ui.reader.base.AbstractReader
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
import org.koitharu.kotatsu.ui.reader.base.GroupedList import org.koitharu.kotatsu.ui.reader.base.ReaderPage
import org.koitharu.kotatsu.ui.reader.standard.PageAnimTransformer import org.koitharu.kotatsu.ui.reader.standard.PageAnimTransformer
import org.koitharu.kotatsu.ui.reader.standard.PagerPaginationListener import org.koitharu.kotatsu.ui.reader.standard.PagerPaginationListener
import org.koitharu.kotatsu.utils.ext.doOnPageChanged import org.koitharu.kotatsu.utils.ext.doOnPageChanged
@ -53,13 +52,10 @@ class ReversedReaderFragment : AbstractReader(R.layout.fragment_reader_standard)
super.onDestroyView() super.onDestroyView()
} }
override fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter { override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
return ReversedPagesAdapter(dataSet, loader) return ReversedPagesAdapter(dataSet, loader)
} }
override val itemsCount: Int
get() = adapter?.itemCount ?: 0
override fun getCurrentItem() = reversed(pager.currentItem) override fun getCurrentItem() = reversed(pager.currentItem)
override fun setCurrentItem(position: Int, isSmooth: Boolean) { override fun setCurrentItem(position: Int, isSmooth: Boolean) {
@ -82,6 +78,10 @@ class ReversedReaderFragment : AbstractReader(R.layout.fragment_reader_standard)
} }
} }
override fun getLastPage() = pages.firstOrNull()
override fun getFirstPage() = pages.lastOrNull()
private fun reversed(position: Int) = (itemsCount - position - 1).coerceAtLeast(0) private fun reversed(position: Int) = (itemsCount - position - 1).coerceAtLeast(0)
companion object { companion object {

@ -7,14 +7,14 @@ import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import kotlinx.android.synthetic.main.item_page.* import kotlinx.android.synthetic.main.item_page.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder import org.koitharu.kotatsu.ui.base.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class PageHolder(parent: ViewGroup, loader: PageLoader) : class PageHolder(parent: ViewGroup, loader: PageLoader) :
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page), BaseViewHolder<ReaderPage, Unit>(parent, R.layout.item_page),
PageHolderDelegate.Callback, View.OnClickListener { PageHolderDelegate.Callback, View.OnClickListener {
private val delegate = PageHolderDelegate(loader, this) private val delegate = PageHolderDelegate(loader, this)
@ -24,8 +24,8 @@ class PageHolder(parent: ViewGroup, loader: PageLoader) :
button_retry.setOnClickListener(this) button_retry.setOnClickListener(this)
} }
override fun onBind(data: MangaPage, extra: Unit) { override fun onBind(data: ReaderPage, extra: Unit) {
delegate.onBind(data) delegate.onBind(data.toMangaPage())
} }
override fun onRecycled() { override fun onRecycled() {
@ -57,7 +57,7 @@ class PageHolder(parent: ViewGroup, loader: PageLoader) :
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.button_retry -> delegate.retry(boundData ?: return) R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return)
} }
} }

@ -7,12 +7,11 @@ import android.view.View
import kotlinx.android.synthetic.main.fragment_reader_standard.* import kotlinx.android.synthetic.main.fragment_reader_standard.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.ui.reader.ReaderState import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.reader.base.AbstractReader import org.koitharu.kotatsu.ui.reader.base.AbstractReader
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
import org.koitharu.kotatsu.ui.reader.base.GroupedList import org.koitharu.kotatsu.ui.reader.base.ReaderPage
import org.koitharu.kotatsu.utils.ext.doOnPageChanged import org.koitharu.kotatsu.utils.ext.doOnPageChanged
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
@ -49,13 +48,10 @@ class PagerReaderFragment : AbstractReader(R.layout.fragment_reader_standard),
super.onDestroyView() super.onDestroyView()
} }
override fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter { override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
return PagesAdapter(dataSet, loader) return PagesAdapter(dataSet, loader)
} }
override val itemsCount: Int
get() = adapter?.itemCount ?: 0
override fun getCurrentItem() = pager.currentItem override fun getCurrentItem() = pager.currentItem
override fun setCurrentItem(position: Int, isSmooth: Boolean) { override fun setCurrentItem(position: Int, isSmooth: Boolean) {

@ -1,13 +1,12 @@
package org.koitharu.kotatsu.ui.reader.standard package org.koitharu.kotatsu.ui.reader.standard
import android.view.ViewGroup import android.view.ViewGroup
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
import org.koitharu.kotatsu.ui.reader.base.GroupedList
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
class PagesAdapter( class PagesAdapter(
pages: GroupedList<Long, MangaPage>, pages: List<ReaderPage>,
private val loader: PageLoader private val loader: PageLoader
) : BaseReaderAdapter(pages) { ) : BaseReaderAdapter(pages) {

@ -1,13 +1,12 @@
package org.koitharu.kotatsu.ui.reader.wetoon package org.koitharu.kotatsu.ui.reader.wetoon
import android.view.ViewGroup import android.view.ViewGroup
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
import org.koitharu.kotatsu.ui.reader.base.GroupedList import org.koitharu.kotatsu.ui.reader.base.ReaderPage
class WebtoonAdapter( class WebtoonAdapter(
pages: GroupedList<Long, MangaPage>, pages: List<ReaderPage>,
private val loader: PageLoader private val loader: PageLoader
) : BaseReaderAdapter(pages) { ) : BaseReaderAdapter(pages) {

@ -8,15 +8,15 @@ import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.android.synthetic.main.item_page_webtoon.* import kotlinx.android.synthetic.main.item_page_webtoon.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.base.list.BaseViewHolder import org.koitharu.kotatsu.ui.base.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
import org.koitharu.kotatsu.ui.reader.base.ReaderPage
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) : class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page_webtoon), BaseViewHolder<ReaderPage, Unit>(parent, R.layout.item_page_webtoon),
PageHolderDelegate.Callback, View.OnClickListener { PageHolderDelegate.Callback, View.OnClickListener {
private val delegate = PageHolderDelegate(loader, this) private val delegate = PageHolderDelegate(loader, this)
@ -27,8 +27,8 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
button_retry.setOnClickListener(this) button_retry.setOnClickListener(this)
} }
override fun onBind(data: MangaPage, extra: Unit) { override fun onBind(data: ReaderPage, extra: Unit) {
delegate.onBind(data) delegate.onBind(data.toMangaPage())
} }
override fun onRecycled() { override fun onRecycled() {
@ -65,7 +65,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.button_retry -> delegate.retry(boundData ?: return) R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return)
} }
} }

@ -5,11 +5,10 @@ import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import kotlinx.android.synthetic.main.fragment_reader_webtoon.* import kotlinx.android.synthetic.main.fragment_reader_webtoon.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.reader.ReaderState import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.reader.base.AbstractReader import org.koitharu.kotatsu.ui.reader.base.AbstractReader
import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter import org.koitharu.kotatsu.ui.reader.base.BaseReaderAdapter
import org.koitharu.kotatsu.ui.reader.base.GroupedList import org.koitharu.kotatsu.ui.reader.base.ReaderPage
import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged import org.koitharu.kotatsu.utils.ext.doOnCurrentItemChanged
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
import org.koitharu.kotatsu.utils.ext.firstItem import org.koitharu.kotatsu.utils.ext.firstItem
@ -29,7 +28,7 @@ class WebtoonReaderFragment : AbstractReader(R.layout.fragment_reader_webtoon) {
recyclerView.doOnCurrentItemChanged(::notifyPageChanged) recyclerView.doOnCurrentItemChanged(::notifyPageChanged)
} }
override fun onCreateAdapter(dataSet: GroupedList<Long, MangaPage>): BaseReaderAdapter { override fun onCreateAdapter(dataSet: List<ReaderPage>): BaseReaderAdapter {
return WebtoonAdapter(dataSet, loader) return WebtoonAdapter(dataSet, loader)
} }
@ -38,9 +37,6 @@ class WebtoonReaderFragment : AbstractReader(R.layout.fragment_reader_webtoon) {
super.onDestroyView() super.onDestroyView()
} }
override val itemsCount: Int
get() = adapter?.itemCount ?: 0
override fun getCurrentItem(): Int { override fun getCurrentItem(): Int {
return recyclerView.findCenterViewPosition() return recyclerView.findCenterViewPosition()
} }

@ -1,6 +1,8 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import androidx.collection.ArrayMap
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.collection.LongSparseArray
fun <T> MutableCollection<T>.replaceWith(subject: Iterable<T>) { fun <T> MutableCollection<T>.replaceWith(subject: Iterable<T>) {
clear() clear()
@ -56,4 +58,12 @@ fun LongArray.toArraySet(): Set<Long> {
} }
} }
fun <K, V> List<Pair<K, V>>.toMutableMap(): MutableMap<K, V> = toMap(HashMap<K, V>(size)) fun <K, V> List<Pair<K, V>>.toMutableMap(): MutableMap<K, V> = toMap(ArrayMap(size))
inline fun <T> Collection<T>.associateByLong(selector: (T) -> Long): LongSparseArray<T> {
val result = LongSparseArray<T>(size)
for (item in this) {
result.put(selector(item), item)
}
return result
}
Loading…
Cancel
Save