Slider in reader #204
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.domain
|
||||||
|
|
||||||
|
import androidx.collection.LongSparseArray
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
|
||||||
|
class ChapterPages private constructor(private val pages: ArrayDeque<ReaderPage>) : List<ReaderPage> by pages {
|
||||||
|
|
||||||
|
// map chapterId to index in pages deque
|
||||||
|
private val indices = LongSparseArray<IntRange>()
|
||||||
|
|
||||||
|
constructor() : this(ArrayDeque())
|
||||||
|
|
||||||
|
val chaptersSize: Int
|
||||||
|
get() = indices.size()
|
||||||
|
|
||||||
|
fun removeFirst() {
|
||||||
|
val chapterId = pages.first().chapterId
|
||||||
|
indices.remove(chapterId)
|
||||||
|
var delta = 0
|
||||||
|
while (pages.first().chapterId == chapterId) {
|
||||||
|
pages.removeFirst()
|
||||||
|
delta--
|
||||||
|
}
|
||||||
|
shiftIndices(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeLast() {
|
||||||
|
val chapterId = pages.last().chapterId
|
||||||
|
indices.remove(chapterId)
|
||||||
|
while (pages.last().chapterId == chapterId) {
|
||||||
|
pages.removeLast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addLast(id: Long, newPages: List<ReaderPage>) {
|
||||||
|
indices.put(id, pages.size until (pages.size + newPages.size))
|
||||||
|
pages.addAll(newPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addFirst(id: Long, newPages: List<ReaderPage>) {
|
||||||
|
shiftIndices(newPages.size)
|
||||||
|
indices.put(id, newPages.indices)
|
||||||
|
pages.addAll(0, newPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
indices.clear()
|
||||||
|
pages.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun size(id: Long) = indices[id]?.run {
|
||||||
|
endInclusive - start + 1
|
||||||
|
} ?: 0
|
||||||
|
|
||||||
|
fun subList(id: Long): List<ReaderPage> {
|
||||||
|
val range = indices[id] ?: return emptyList()
|
||||||
|
return pages.subList(range.first, range.last + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shiftIndices(delta: Int) {
|
||||||
|
for (i in 0 until indices.size()) {
|
||||||
|
val range = indices.valueAt(i)
|
||||||
|
indices.setValueAt(i, range + delta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private operator fun IntRange.plus(delta: Int): IntRange {
|
||||||
|
return IntRange(start + delta, endInclusive + delta)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.domain
|
||||||
|
|
||||||
|
import android.util.LongSparseArray
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
|
||||||
|
private const val PAGES_TRIM_THRESHOLD = 120
|
||||||
|
|
||||||
|
class ChaptersLoader {
|
||||||
|
|
||||||
|
val chapters = LongSparseArray<MangaChapter>()
|
||||||
|
private val chapterPages = ChapterPages()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
suspend fun loadPrevNextChapter(manga: Manga, currentId: Long, isNext: Boolean) {
|
||||||
|
val chapters = manga.chapters ?: return
|
||||||
|
val predicate: (MangaChapter) -> Boolean = { it.id == currentId }
|
||||||
|
val index = if (isNext) chapters.indexOfFirst(predicate) else chapters.indexOfLast(predicate)
|
||||||
|
if (index == -1) return
|
||||||
|
val newChapter = chapters.getOrNull(if (isNext) index + 1 else index - 1) ?: return
|
||||||
|
val newPages = loadChapter(manga, newChapter.id)
|
||||||
|
mutex.withLock {
|
||||||
|
if (chapterPages.chaptersSize > 1) {
|
||||||
|
// trim pages
|
||||||
|
if (chapterPages.size > PAGES_TRIM_THRESHOLD) {
|
||||||
|
if (isNext) {
|
||||||
|
chapterPages.removeFirst()
|
||||||
|
} else {
|
||||||
|
chapterPages.removeLast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNext) {
|
||||||
|
chapterPages.addLast(newChapter.id, newPages)
|
||||||
|
} else {
|
||||||
|
chapterPages.addFirst(newChapter.id, newPages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadSingleChapter(manga: Manga, chapterId: Long) {
|
||||||
|
val pages = loadChapter(manga, chapterId)
|
||||||
|
mutex.withLock {
|
||||||
|
chapterPages.clear()
|
||||||
|
chapterPages.addLast(chapterId, pages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPages(chapterId: Long): List<ReaderPage> {
|
||||||
|
return chapterPages.subList(chapterId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPagesCount(chapterId: Long): Int {
|
||||||
|
return chapterPages.size(chapterId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun snapshot() = chapterPages.toList()
|
||||||
|
|
||||||
|
private suspend fun loadChapter(manga: Manga, chapterId: Long): List<ReaderPage> {
|
||||||
|
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
|
||||||
|
val repo = MangaRepository(manga.source)
|
||||||
|
return repo.getPages(chapter).mapIndexed { index, page ->
|
||||||
|
ReaderPage(page, index, chapterId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
|
import com.google.android.material.slider.LabelFormatter
|
||||||
|
import org.koitharu.kotatsu.parsers.util.format
|
||||||
|
|
||||||
|
class PageLabelFormatter : LabelFormatter {
|
||||||
|
|
||||||
|
override fun getFormattedValue(value: Float): String {
|
||||||
|
return (value + 1).format(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ import androidx.core.view.OnApplyWindowInsetsListener
|
|||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.transition.Slide
|
import androidx.transition.Slide
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
@@ -20,8 +19,6 @@ import androidx.transition.TransitionSet
|
|||||||
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.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
@@ -44,7 +41,6 @@ import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
|||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||||
import org.koitharu.kotatsu.utils.GridTouchHelper
|
import org.koitharu.kotatsu.utils.GridTouchHelper
|
||||||
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
|
|
||||||
import org.koitharu.kotatsu.utils.ShareHelper
|
import org.koitharu.kotatsu.utils.ShareHelper
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
import org.koitharu.kotatsu.utils.ext.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -68,7 +64,6 @@ class ReaderActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var touchHelper: GridTouchHelper
|
private lateinit var touchHelper: GridTouchHelper
|
||||||
private lateinit var orientationHelper: ScreenOrientationHelper
|
|
||||||
private lateinit var controlDelegate: ReaderControlDelegate
|
private lateinit var controlDelegate: ReaderControlDelegate
|
||||||
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
|
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
|
||||||
private var gestureInsets: Insets = Insets.NONE
|
private var gestureInsets: Insets = Insets.NONE
|
||||||
@@ -81,18 +76,12 @@ class ReaderActivity :
|
|||||||
readerManager = ReaderManager(supportFragmentManager, R.id.container)
|
readerManager = ReaderManager(supportFragmentManager, R.id.container)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
touchHelper = GridTouchHelper(this, this)
|
touchHelper = GridTouchHelper(this, this)
|
||||||
orientationHelper = ScreenOrientationHelper(this)
|
|
||||||
controlDelegate = ReaderControlDelegate(lifecycleScope, get(), this)
|
controlDelegate = ReaderControlDelegate(lifecycleScope, get(), this)
|
||||||
binding.toolbarBottom.inflateMenu(R.menu.opt_reader_bottom)
|
|
||||||
binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
|
binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
|
||||||
|
binding.slider.setLabelFormatter(PageLabelFormatter())
|
||||||
|
ReaderSliderListener(this, viewModel).attachToSlider(binding.slider)
|
||||||
insetsDelegate.interceptingWindowInsetsListener = this
|
insetsDelegate.interceptingWindowInsetsListener = this
|
||||||
|
|
||||||
orientationHelper.observeAutoOrientation()
|
|
||||||
.flowWithLifecycle(lifecycle)
|
|
||||||
.onEach {
|
|
||||||
binding.toolbarBottom.menu.findItem(R.id.action_screen_rotate).isVisible = !it
|
|
||||||
}.launchIn(lifecycleScope)
|
|
||||||
|
|
||||||
viewModel.onError.observe(this, this::onError)
|
viewModel.onError.observe(this, this::onError)
|
||||||
viewModel.readerMode.observe(this, this::onInitReader)
|
viewModel.readerMode.observe(this, this::onInitReader)
|
||||||
viewModel.onPageSaved.observe(this, this::onPageSaved)
|
viewModel.onPageSaved.observe(this, this::onPageSaved)
|
||||||
@@ -114,15 +103,6 @@ class ReaderActivity :
|
|||||||
if (readerManager.currentMode != mode) {
|
if (readerManager.currentMode != mode) {
|
||||||
readerManager.replace(mode)
|
readerManager.replace(mode)
|
||||||
}
|
}
|
||||||
val iconRes = when (mode) {
|
|
||||||
ReaderMode.WEBTOON -> R.drawable.ic_script
|
|
||||||
ReaderMode.REVERSED -> R.drawable.ic_read_reversed
|
|
||||||
ReaderMode.STANDARD -> R.drawable.ic_book_page
|
|
||||||
}
|
|
||||||
binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).run {
|
|
||||||
setIcon(iconRes)
|
|
||||||
setVisible(true)
|
|
||||||
}
|
|
||||||
if (binding.appbarTop.isVisible) {
|
if (binding.appbarTop.isVisible) {
|
||||||
lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1))
|
lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1))
|
||||||
}
|
}
|
||||||
@@ -136,7 +116,7 @@ class ReaderActivity :
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_reader_mode -> {
|
R.id.action_menu -> {
|
||||||
val currentMode = readerManager.currentMode ?: return false
|
val currentMode = readerManager.currentMode ?: return false
|
||||||
ReaderConfigDialog.show(supportFragmentManager, currentMode)
|
ReaderConfigDialog.show(supportFragmentManager, currentMode)
|
||||||
}
|
}
|
||||||
@@ -150,9 +130,6 @@ class ReaderActivity :
|
|||||||
viewModel.getCurrentState()?.chapterId ?: 0L
|
viewModel.getCurrentState()?.chapterId ?: 0L
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.action_screen_rotate -> {
|
|
||||||
orientationHelper.toggleOrientation()
|
|
||||||
}
|
|
||||||
R.id.action_pages_thumbs -> {
|
R.id.action_pages_thumbs -> {
|
||||||
val pages = viewModel.getCurrentChapterPages()
|
val pages = viewModel.getCurrentChapterPages()
|
||||||
if (!pages.isNullOrEmpty()) {
|
if (!pages.isNullOrEmpty()) {
|
||||||
@@ -166,12 +143,6 @@ class ReaderActivity :
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.action_save_page -> {
|
|
||||||
viewModel.getCurrentPage()?.also { page ->
|
|
||||||
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
|
|
||||||
viewModel.saveCurrentPage(page, savePageRequest)
|
|
||||||
} ?: return false
|
|
||||||
}
|
|
||||||
R.id.action_bookmark -> {
|
R.id.action_bookmark -> {
|
||||||
if (viewModel.isBookmarkAdded.value == true) {
|
if (viewModel.isBookmarkAdded.value == true) {
|
||||||
viewModel.removeBookmark()
|
viewModel.removeBookmark()
|
||||||
@@ -199,7 +170,6 @@ class ReaderActivity :
|
|||||||
val menu = binding.toolbarBottom.menu
|
val menu = binding.toolbarBottom.menu
|
||||||
menu.findItem(R.id.action_bookmark).isVisible = hasPages
|
menu.findItem(R.id.action_bookmark).isVisible = hasPages
|
||||||
menu.findItem(R.id.action_pages_thumbs).isVisible = hasPages
|
menu.findItem(R.id.action_pages_thumbs).isVisible = hasPages
|
||||||
menu.findItem(R.id.action_save_page).isVisible = hasPages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onError(e: Throwable) {
|
private fun onError(e: Throwable) {
|
||||||
@@ -351,6 +321,7 @@ class ReaderActivity :
|
|||||||
title = uiState?.chapterName ?: uiState?.mangaName ?: getString(R.string.loading_)
|
title = uiState?.chapterName ?: uiState?.mangaName ?: getString(R.string.loading_)
|
||||||
if (uiState == null) {
|
if (uiState == null) {
|
||||||
supportActionBar?.subtitle = null
|
supportActionBar?.subtitle = null
|
||||||
|
binding.slider.isVisible = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) {
|
supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) {
|
||||||
@@ -363,6 +334,13 @@ class ReaderActivity :
|
|||||||
binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
|
binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (uiState.totalPages > 0) {
|
||||||
|
binding.slider.value = uiState.currentPage.toFloat()
|
||||||
|
binding.slider.valueTo = uiState.totalPages.toFloat()
|
||||||
|
binding.slider.isVisible = true
|
||||||
|
} else {
|
||||||
|
binding.slider.isVisible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ErrorDialogListener(
|
private inner class ErrorDialogListener(
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
|
import com.google.android.material.slider.Slider
|
||||||
|
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
||||||
|
|
||||||
|
class ReaderSliderListener(
|
||||||
|
private val pageSelectListener: OnPageSelectListener,
|
||||||
|
private val viewModel: ReaderViewModel,
|
||||||
|
) : Slider.OnChangeListener, Slider.OnSliderTouchListener {
|
||||||
|
|
||||||
|
private var isChanged = false
|
||||||
|
|
||||||
|
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||||
|
if (fromUser) {
|
||||||
|
isChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
|
isChanged = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
|
if (isChanged) {
|
||||||
|
switchPageToIndex(slider.value.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun attachToSlider(slider: Slider) {
|
||||||
|
slider.addOnChangeListener(this)
|
||||||
|
slider.addOnSliderTouchListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchPageToIndex(index: Int) {
|
||||||
|
val pages = viewModel.getCurrentChapterPages()
|
||||||
|
val page = pages?.getOrNull(index) ?: return
|
||||||
|
pageSelectListener.onPageSelected(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.LongSparseArray
|
import android.util.LongSparseArray
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.annotation.AnyThread
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -24,17 +25,17 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.reader.data.filterChapters
|
import org.koitharu.kotatsu.reader.data.filterChapters
|
||||||
|
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||||
|
import org.koitharu.kotatsu.utils.ext.requireValue
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
private const val BOUNDS_PAGE_OFFSET = 2
|
private const val BOUNDS_PAGE_OFFSET = 2
|
||||||
private const val PAGES_TRIM_THRESHOLD = 120
|
|
||||||
private const val PREFETCH_LIMIT = 10
|
private const val PREFETCH_LIMIT = 10
|
||||||
|
|
||||||
class ReaderViewModel(
|
class ReaderViewModel(
|
||||||
@@ -53,25 +54,16 @@ class ReaderViewModel(
|
|||||||
private var bookmarkJob: Job? = null
|
private var bookmarkJob: Job? = null
|
||||||
private val currentState = MutableStateFlow(initialState)
|
private val currentState = MutableStateFlow(initialState)
|
||||||
private val mangaData = MutableStateFlow(intent.manga)
|
private val mangaData = MutableStateFlow(intent.manga)
|
||||||
private val chapters = LongSparseArray<MangaChapter>()
|
private val chapters: LongSparseArray<MangaChapter>
|
||||||
|
get() = chaptersLoader.chapters
|
||||||
|
|
||||||
val pageLoader = PageLoader()
|
val pageLoader = PageLoader()
|
||||||
|
private val chaptersLoader = ChaptersLoader()
|
||||||
|
|
||||||
val readerMode = MutableLiveData<ReaderMode>()
|
val readerMode = MutableLiveData<ReaderMode>()
|
||||||
val onPageSaved = SingleLiveEvent<Uri?>()
|
val onPageSaved = SingleLiveEvent<Uri?>()
|
||||||
val onShowToast = SingleLiveEvent<Int>()
|
val onShowToast = SingleLiveEvent<Int>()
|
||||||
val uiState: LiveData<ReaderUiState?> = combine(
|
val uiState = MutableLiveData<ReaderUiState?>(null)
|
||||||
mangaData,
|
|
||||||
currentState,
|
|
||||||
) { manga, state ->
|
|
||||||
val chapter = state?.chapterId?.let(chapters::get)
|
|
||||||
ReaderUiState(
|
|
||||||
mangaName = manga?.title,
|
|
||||||
chapterName = chapter?.name,
|
|
||||||
chapterNumber = chapter?.number ?: 0,
|
|
||||||
chaptersTotal = chapters.size()
|
|
||||||
)
|
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, null)
|
|
||||||
|
|
||||||
val content = MutableLiveData(ReaderContent(emptyList(), null))
|
val content = MutableLiveData(ReaderContent(emptyList(), null))
|
||||||
val manga: Manga?
|
val manga: Manga?
|
||||||
@@ -134,7 +126,6 @@ class ReaderViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO check performance
|
|
||||||
fun saveCurrentState(state: ReaderState? = null) {
|
fun saveCurrentState(state: ReaderState? = null) {
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
currentState.value = state
|
currentState.value = state
|
||||||
@@ -151,8 +142,7 @@ class ReaderViewModel(
|
|||||||
|
|
||||||
fun getCurrentChapterPages(): List<MangaPage>? {
|
fun getCurrentChapterPages(): List<MangaPage>? {
|
||||||
val chapterId = currentState.value?.chapterId ?: return null
|
val chapterId = currentState.value?.chapterId ?: return null
|
||||||
val pages = content.value?.pages ?: return null
|
return chaptersLoader.getPages(chapterId).map { it.toMangaPage() }
|
||||||
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveCurrentPage(
|
fun saveCurrentPage(
|
||||||
@@ -195,11 +185,12 @@ class ReaderViewModel(
|
|||||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
prevJob?.cancelAndJoin()
|
prevJob?.cancelAndJoin()
|
||||||
content.postValue(ReaderContent(emptyList(), null))
|
content.postValue(ReaderContent(emptyList(), null))
|
||||||
val newPages = loadChapter(id)
|
chaptersLoader.loadSingleChapter(mangaData.requireValue(), id)
|
||||||
content.postValue(ReaderContent(newPages, ReaderState(id, 0, 0)))
|
content.postValue(ReaderContent(chaptersLoader.snapshot(), ReaderState(id, 0, 0)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO move to background?
|
||||||
fun onCurrentPageChanged(position: Int) {
|
fun onCurrentPageChanged(position: Int) {
|
||||||
val pages = content.value?.pages ?: return
|
val pages = content.value?.pages ?: return
|
||||||
pages.getOrNull(position)?.let { page ->
|
pages.getOrNull(position)?.let { page ->
|
||||||
@@ -207,14 +198,15 @@ class ReaderViewModel(
|
|||||||
cs?.copy(chapterId = page.chapterId, page = page.index)
|
cs?.copy(chapterId = page.chapterId, page = page.index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
notifyStateChanged()
|
||||||
if (pages.isEmpty() || loadingJob?.isActive == true) {
|
if (pages.isEmpty() || loadingJob?.isActive == true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (position <= BOUNDS_PAGE_OFFSET) {
|
if (position <= BOUNDS_PAGE_OFFSET) {
|
||||||
loadPrevNextChapter(pages.first().chapterId, -1)
|
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
||||||
}
|
}
|
||||||
if (position >= pages.size - BOUNDS_PAGE_OFFSET) {
|
if (position >= pages.size - BOUNDS_PAGE_OFFSET) {
|
||||||
loadPrevNextChapter(pages.last().chapterId, 1)
|
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
||||||
}
|
}
|
||||||
if (pageLoader.isPrefetchApplicable()) {
|
if (pageLoader.isPrefetchApplicable()) {
|
||||||
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT))
|
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT))
|
||||||
@@ -279,55 +271,21 @@ class ReaderViewModel(
|
|||||||
mangaData.value = manga.filterChapters(branch)
|
mangaData.value = manga.filterChapters(branch)
|
||||||
readerMode.postValue(mode)
|
readerMode.postValue(mode)
|
||||||
|
|
||||||
val pages = loadChapter(requireNotNull(currentState.value).chapterId)
|
chaptersLoader.loadSingleChapter(manga, requireNotNull(currentState.value).chapterId)
|
||||||
// save state
|
// save state
|
||||||
currentState.value?.let {
|
currentState.value?.let {
|
||||||
val percent = computePercent(it.chapterId, it.page)
|
val percent = computePercent(it.chapterId, it.page)
|
||||||
historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent)
|
historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent)
|
||||||
}
|
}
|
||||||
|
notifyStateChanged()
|
||||||
content.postValue(ReaderContent(pages, currentState.value))
|
content.postValue(ReaderContent(chaptersLoader.snapshot(), currentState.value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
|
private fun loadPrevNextChapter(currentId: Long, isNext: Boolean) {
|
||||||
val manga = checkNotNull(mangaData.value) { "Manga is null" }
|
|
||||||
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
|
|
||||||
val repo = MangaRepository(manga.source)
|
|
||||||
return repo.getPages(chapter).mapIndexed { index, page ->
|
|
||||||
ReaderPage(page, index, chapterId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadPrevNextChapter(currentId: Long, delta: Int) {
|
|
||||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
val chapters = mangaData.value?.chapters ?: return@launchLoadingJob
|
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
|
||||||
val predicate: (MangaChapter) -> Boolean = { it.id == currentId }
|
content.postValue(ReaderContent(chaptersLoader.snapshot(), null))
|
||||||
val index =
|
|
||||||
if (delta < 0) chapters.indexOfLast(predicate) else chapters.indexOfFirst(predicate)
|
|
||||||
if (index == -1) return@launchLoadingJob
|
|
||||||
val newChapter = chapters.getOrNull(index + delta) ?: return@launchLoadingJob
|
|
||||||
val newPages = loadChapter(newChapter.id)
|
|
||||||
var currentPages = content.value?.pages ?: return@launchLoadingJob
|
|
||||||
// trim pages
|
|
||||||
if (currentPages.size > PAGES_TRIM_THRESHOLD) {
|
|
||||||
val firstChapterId = currentPages.first().chapterId
|
|
||||||
val lastChapterId = currentPages.last().chapterId
|
|
||||||
if (firstChapterId != lastChapterId) {
|
|
||||||
currentPages = when (delta) {
|
|
||||||
1 -> currentPages.dropWhile { it.chapterId == firstChapterId }
|
|
||||||
-1 -> currentPages.dropLastWhile { it.chapterId == lastChapterId }
|
|
||||||
else -> currentPages
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val pages = when (delta) {
|
|
||||||
0 -> newPages
|
|
||||||
-1 -> newPages + currentPages
|
|
||||||
1 -> currentPages + newPages
|
|
||||||
else -> error("Invalid delta $delta")
|
|
||||||
}
|
|
||||||
content.postValue(ReaderContent(pages, null))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,12 +326,26 @@ class ReaderViewModel(
|
|||||||
}.getOrDefault(defaultMode)
|
}.getOrDefault(defaultMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
private fun notifyStateChanged() {
|
||||||
|
val state = getCurrentState()
|
||||||
|
val chapter = state?.chapterId?.let(chapters::get)
|
||||||
|
val newState = ReaderUiState(
|
||||||
|
mangaName = manga?.title,
|
||||||
|
chapterName = chapter?.name,
|
||||||
|
chapterNumber = chapter?.number ?: 0,
|
||||||
|
chaptersTotal = chapters.size(),
|
||||||
|
totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0,
|
||||||
|
currentPage = state?.page ?: 0,
|
||||||
|
)
|
||||||
|
uiState.postValue(newState)
|
||||||
|
}
|
||||||
|
|
||||||
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
|
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
|
||||||
val chapters = manga?.chapters ?: return PROGRESS_NONE
|
val chapters = manga?.chapters ?: return PROGRESS_NONE
|
||||||
val chaptersCount = chapters.size
|
val chaptersCount = chapters.size
|
||||||
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
|
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
|
||||||
val pages = content.value?.pages ?: return PROGRESS_NONE
|
val pagesCount = chaptersLoader.getPagesCount(chapterId)
|
||||||
val pagesCount = pages.count { x -> x.chapterId == chapterId }
|
|
||||||
if (chaptersCount == 0 || pagesCount == 0) {
|
if (chaptersCount == 0 || pagesCount == 0) {
|
||||||
return PROGRESS_NONE
|
return PROGRESS_NONE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,7 @@ data class ReaderUiState(
|
|||||||
val mangaName: String?,
|
val mangaName: String?,
|
||||||
val chapterName: String?,
|
val chapterName: String?,
|
||||||
val chapterNumber: Int,
|
val chapterNumber: Int,
|
||||||
val chaptersTotal: Int
|
val chaptersTotal: Int,
|
||||||
|
val currentPage: Int,
|
||||||
|
val totalPages: Int,
|
||||||
)
|
)
|
||||||
@@ -2,10 +2,7 @@ package org.koitharu.kotatsu.utils.ext
|
|||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
|
||||||
|
|
||||||
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
|
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
|
||||||
var isFirstCall = true
|
var isFirstCall = true
|
||||||
@@ -35,3 +32,7 @@ fun <T> Flow<T>.throttle(timeoutMillis: (T) -> Long): Flow<T> {
|
|||||||
lastEmittedAt = now
|
lastEmittedAt = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> StateFlow<T?>.requireValue(): T = checkNotNull(value) {
|
||||||
|
"StateFlow value is null"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:keepScreenOn="true">
|
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
|
||||||
android:id="@+id/container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<org.koitharu.kotatsu.reader.ui.ReaderToastView
|
|
||||||
android:id="@+id/toastView"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
|
||||||
android:layout_marginBottom="20dp"
|
|
||||||
android:background="@drawable/bg_reader_indicator"
|
|
||||||
android:drawablePadding="6dp"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
|
||||||
android:theme="@style/ThemeOverlay.Material3.Dark"
|
|
||||||
tools:text="@string/loading_" />
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
|
||||||
android:id="@+id/appbar_top"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@color/dim"
|
|
||||||
android:elevation="0dp"
|
|
||||||
android:theme="@style/ThemeOverlay.Material3.Dark"
|
|
||||||
app:elevation="0dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@id/toolbar"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
app:popupTheme="@style/ThemeOverlay.Kotatsu" />
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@+id/toolbar_bottom"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:popupTheme="@style/ThemeOverlay.Kotatsu" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/layout_loading"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
|
||||||
android:id="@+id/progressBar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:indeterminate="true" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView_loading"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:text="@string/loading_"
|
|
||||||
android:textAppearance="?attr/textAppearanceBody2" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
@@ -58,7 +58,19 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="top"
|
android:layout_gravity="top"
|
||||||
app:popupTheme="@style/ThemeOverlay.Kotatsu" />
|
app:menu="@menu/opt_reader_bottom"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.Kotatsu">
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/slider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:stepSize="1"
|
||||||
|
android:valueFrom="0"
|
||||||
|
app:labelBehavior="floating"
|
||||||
|
app:tickVisible="false" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -20,28 +20,9 @@
|
|||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_screen_rotate"
|
android:id="@+id/action_menu"
|
||||||
android:icon="@drawable/ic_screen_rotation"
|
android:icon="@drawable/abc_ic_menu_overflow_material"
|
||||||
android:title="@string/rotate_screen"
|
android:title="@string/options"
|
||||||
android:visible="false"
|
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_reader_mode"
|
|
||||||
android:icon="@drawable/ic_loading"
|
|
||||||
android:title="@string/read_mode"
|
|
||||||
android:visible="false"
|
|
||||||
app:showAsAction="always" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_save_page"
|
|
||||||
android:title="@string/save_page"
|
|
||||||
android:visible="false"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_settings"
|
|
||||||
android:title="@string/settings"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -356,4 +356,5 @@
|
|||||||
<string name="enter_email_text">Enter your email to continue</string>
|
<string name="enter_email_text">Enter your email to continue</string>
|
||||||
<string name="removed_from_favourites">Removed from favourites</string>
|
<string name="removed_from_favourites">Removed from favourites</string>
|
||||||
<string name="removed_from_s">Removed from \"%s\"</string>
|
<string name="removed_from_s">Removed from \"%s\"</string>
|
||||||
|
<string name="options">Options</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class JsonSerializerTest {
|
|||||||
categoryId = 20,
|
categoryId = 20,
|
||||||
sortKey = 1,
|
sortKey = 1,
|
||||||
createdAt = System.currentTimeMillis(),
|
createdAt = System.currentTimeMillis(),
|
||||||
|
deletedAt = 0L,
|
||||||
)
|
)
|
||||||
val json = JsonSerializer(entity).toJson()
|
val json = JsonSerializer(entity).toJson()
|
||||||
val result = JsonDeserializer(json).toFavouriteEntity()
|
val result = JsonDeserializer(json).toFavouriteEntity()
|
||||||
@@ -71,6 +72,7 @@ class JsonSerializerTest {
|
|||||||
page = 35,
|
page = 35,
|
||||||
scroll = 24.0f,
|
scroll = 24.0f,
|
||||||
percent = 0.6f,
|
percent = 0.6f,
|
||||||
|
deletedAt = 0L,
|
||||||
)
|
)
|
||||||
val json = JsonSerializer(entity).toJson()
|
val json = JsonSerializer(entity).toJson()
|
||||||
val result = JsonDeserializer(json).toHistoryEntity()
|
val result = JsonDeserializer(json).toHistoryEntity()
|
||||||
@@ -87,6 +89,7 @@ class JsonSerializerTest {
|
|||||||
order = SortOrder.RATING.name,
|
order = SortOrder.RATING.name,
|
||||||
track = false,
|
track = false,
|
||||||
isVisibleInLibrary = true,
|
isVisibleInLibrary = true,
|
||||||
|
deletedAt = 0L,
|
||||||
)
|
)
|
||||||
val json = JsonSerializer(entity).toJson()
|
val json = JsonSerializer(entity).toJson()
|
||||||
val result = JsonDeserializer(json).toFavouriteCategoryEntity()
|
val result = JsonDeserializer(json).toFavouriteCategoryEntity()
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.domain
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class ChapterPagesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getChaptersSize() {
|
||||||
|
val pages = ChapterPages()
|
||||||
|
pages.addFirst(1L, List(12) { page(1L) })
|
||||||
|
pages.addFirst(2L, List(17) { page(2L) })
|
||||||
|
assertEquals(2, pages.chaptersSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeFirst() {
|
||||||
|
val pages = ChapterPages()
|
||||||
|
pages.addLast(1L, List(12) { page(1L) })
|
||||||
|
pages.addLast(2L, List(17) { page(2L) })
|
||||||
|
pages.addLast(4L, List(2) { page(4L) })
|
||||||
|
pages.removeFirst()
|
||||||
|
assertEquals(2, pages.chaptersSize)
|
||||||
|
assertEquals(17 + 2, pages.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeLast() {
|
||||||
|
val pages = ChapterPages()
|
||||||
|
pages.addLast(1L, List(12) { page(1L) })
|
||||||
|
pages.addLast(2L, List(17) { page(2L) })
|
||||||
|
pages.addLast(4L, List(2) { page(4L) })
|
||||||
|
pages.removeLast()
|
||||||
|
assertEquals(2, pages.chaptersSize)
|
||||||
|
assertEquals(12 + 17, pages.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clear() {
|
||||||
|
val pages = ChapterPages()
|
||||||
|
pages.addLast(1L, List(12) { page(1L) })
|
||||||
|
pages.addLast(2L, List(17) { page(2L) })
|
||||||
|
pages.addLast(4L, List(2) { page(4L) })
|
||||||
|
pages.clear()
|
||||||
|
assertEquals(0, pages.chaptersSize)
|
||||||
|
assertEquals(0, pages.size)
|
||||||
|
assertEquals(0, pages.size(1L))
|
||||||
|
assertEquals(0, pages.size(2L))
|
||||||
|
assertEquals(0, pages.size(4L))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun subList() {
|
||||||
|
val pages = ChapterPages()
|
||||||
|
pages.addLast(1L, List(12) { page(1L) })
|
||||||
|
pages.addLast(2L, List(17) { page(2L) })
|
||||||
|
pages.addFirst(4L, List(2) { page(4L) })
|
||||||
|
val subList = pages.subList(2L)
|
||||||
|
assertEquals(17, subList.size)
|
||||||
|
assertEquals(2L, subList.first().chapterId)
|
||||||
|
assertEquals(2L, subList.last().chapterId)
|
||||||
|
assertTrue(subList.all { it.chapterId == 2L })
|
||||||
|
assertEquals(subList.size, pages.size(2L))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun page(chapterId: Long) = ReaderPage(
|
||||||
|
id = Random.nextLong(),
|
||||||
|
url = "http://localhost",
|
||||||
|
referer = "http://localhost",
|
||||||
|
preview = null,
|
||||||
|
chapterId = chapterId,
|
||||||
|
index = Random.nextInt(),
|
||||||
|
source = MangaSource.DUMMY,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user