Slider in reader #204

This commit is contained in:
Koitharu
2022-07-20 14:08:58 +03:00
parent efffbab4a7
commit 9c94a273ea
14 changed files with 349 additions and 213 deletions

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.transition.Slide
import androidx.transition.TransitionManager
@@ -20,8 +19,6 @@ import androidx.transition.TransitionSet
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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.settings.SettingsActivity
import org.koitharu.kotatsu.utils.GridTouchHelper
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.*
import java.util.concurrent.TimeUnit
@@ -68,7 +64,6 @@ class ReaderActivity :
}
private lateinit var touchHelper: GridTouchHelper
private lateinit var orientationHelper: ScreenOrientationHelper
private lateinit var controlDelegate: ReaderControlDelegate
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
private var gestureInsets: Insets = Insets.NONE
@@ -81,18 +76,12 @@ class ReaderActivity :
readerManager = ReaderManager(supportFragmentManager, R.id.container)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this)
orientationHelper = ScreenOrientationHelper(this)
controlDelegate = ReaderControlDelegate(lifecycleScope, get(), this)
binding.toolbarBottom.inflateMenu(R.menu.opt_reader_bottom)
binding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
binding.slider.setLabelFormatter(PageLabelFormatter())
ReaderSliderListener(this, viewModel).attachToSlider(binding.slider)
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.readerMode.observe(this, this::onInitReader)
viewModel.onPageSaved.observe(this, this::onPageSaved)
@@ -114,15 +103,6 @@ class ReaderActivity :
if (readerManager.currentMode != 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) {
lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1))
}
@@ -136,7 +116,7 @@ class ReaderActivity :
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_reader_mode -> {
R.id.action_menu -> {
val currentMode = readerManager.currentMode ?: return false
ReaderConfigDialog.show(supportFragmentManager, currentMode)
}
@@ -150,9 +130,6 @@ class ReaderActivity :
viewModel.getCurrentState()?.chapterId ?: 0L
)
}
R.id.action_screen_rotate -> {
orientationHelper.toggleOrientation()
}
R.id.action_pages_thumbs -> {
val pages = viewModel.getCurrentChapterPages()
if (!pages.isNullOrEmpty()) {
@@ -166,12 +143,6 @@ class ReaderActivity :
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 -> {
if (viewModel.isBookmarkAdded.value == true) {
viewModel.removeBookmark()
@@ -199,7 +170,6 @@ class ReaderActivity :
val menu = binding.toolbarBottom.menu
menu.findItem(R.id.action_bookmark).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) {
@@ -351,6 +321,7 @@ class ReaderActivity :
title = uiState?.chapterName ?: uiState?.mangaName ?: getString(R.string.loading_)
if (uiState == null) {
supportActionBar?.subtitle = null
binding.slider.isVisible = false
return
}
supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) {
@@ -363,6 +334,13 @@ class ReaderActivity :
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(

View File

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

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui
import android.net.Uri
import android.util.LongSparseArray
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.AnyThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
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.MangaPage
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.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireValue
import java.util.*
private const val BOUNDS_PAGE_OFFSET = 2
private const val PAGES_TRIM_THRESHOLD = 120
private const val PREFETCH_LIMIT = 10
class ReaderViewModel(
@@ -53,25 +54,16 @@ class ReaderViewModel(
private var bookmarkJob: Job? = null
private val currentState = MutableStateFlow(initialState)
private val mangaData = MutableStateFlow(intent.manga)
private val chapters = LongSparseArray<MangaChapter>()
private val chapters: LongSparseArray<MangaChapter>
get() = chaptersLoader.chapters
val pageLoader = PageLoader()
private val chaptersLoader = ChaptersLoader()
val readerMode = MutableLiveData<ReaderMode>()
val onPageSaved = SingleLiveEvent<Uri?>()
val onShowToast = SingleLiveEvent<Int>()
val uiState: LiveData<ReaderUiState?> = combine(
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 uiState = MutableLiveData<ReaderUiState?>(null)
val content = MutableLiveData(ReaderContent(emptyList(), null))
val manga: Manga?
@@ -134,7 +126,6 @@ class ReaderViewModel(
}
}
// TODO check performance
fun saveCurrentState(state: ReaderState? = null) {
if (state != null) {
currentState.value = state
@@ -151,8 +142,7 @@ class ReaderViewModel(
fun getCurrentChapterPages(): List<MangaPage>? {
val chapterId = currentState.value?.chapterId ?: return null
val pages = content.value?.pages ?: return null
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
return chaptersLoader.getPages(chapterId).map { it.toMangaPage() }
}
fun saveCurrentPage(
@@ -195,11 +185,12 @@ class ReaderViewModel(
loadingJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
content.postValue(ReaderContent(emptyList(), null))
val newPages = loadChapter(id)
content.postValue(ReaderContent(newPages, ReaderState(id, 0, 0)))
chaptersLoader.loadSingleChapter(mangaData.requireValue(), id)
content.postValue(ReaderContent(chaptersLoader.snapshot(), ReaderState(id, 0, 0)))
}
}
// TODO move to background?
fun onCurrentPageChanged(position: Int) {
val pages = content.value?.pages ?: return
pages.getOrNull(position)?.let { page ->
@@ -207,14 +198,15 @@ class ReaderViewModel(
cs?.copy(chapterId = page.chapterId, page = page.index)
}
}
notifyStateChanged()
if (pages.isEmpty() || loadingJob?.isActive == true) {
return
}
if (position <= BOUNDS_PAGE_OFFSET) {
loadPrevNextChapter(pages.first().chapterId, -1)
loadPrevNextChapter(pages.first().chapterId, isNext = false)
}
if (position >= pages.size - BOUNDS_PAGE_OFFSET) {
loadPrevNextChapter(pages.last().chapterId, 1)
loadPrevNextChapter(pages.last().chapterId, isNext = true)
}
if (pageLoader.isPrefetchApplicable()) {
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT))
@@ -279,55 +271,21 @@ class ReaderViewModel(
mangaData.value = manga.filterChapters(branch)
readerMode.postValue(mode)
val pages = loadChapter(requireNotNull(currentState.value).chapterId)
chaptersLoader.loadSingleChapter(manga, requireNotNull(currentState.value).chapterId)
// save state
currentState.value?.let {
val percent = computePercent(it.chapterId, it.page)
historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent)
}
content.postValue(ReaderContent(pages, currentState.value))
notifyStateChanged()
content.postValue(ReaderContent(chaptersLoader.snapshot(), currentState.value))
}
}
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
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) {
private fun loadPrevNextChapter(currentId: Long, isNext: Boolean) {
loadingJob = launchLoadingJob(Dispatchers.Default) {
val chapters = mangaData.value?.chapters ?: return@launchLoadingJob
val predicate: (MangaChapter) -> Boolean = { it.id == currentId }
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))
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
content.postValue(ReaderContent(chaptersLoader.snapshot(), null))
}
}
@@ -368,12 +326,26 @@ class ReaderViewModel(
}.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 {
val chapters = manga?.chapters ?: return PROGRESS_NONE
val chaptersCount = chapters.size
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
val pages = content.value?.pages ?: return PROGRESS_NONE
val pagesCount = pages.count { x -> x.chapterId == chapterId }
val pagesCount = chaptersLoader.getPagesCount(chapterId)
if (chaptersCount == 0 || pagesCount == 0) {
return PROGRESS_NONE
}
@@ -401,4 +373,4 @@ private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, p
it.printStackTraceDebug()
}
}
}
}

View File

@@ -4,5 +4,7 @@ data class ReaderUiState(
val mangaName: String?,
val chapterName: String?,
val chapterNumber: Int,
val chaptersTotal: Int
)
val chaptersTotal: Int,
val currentPage: Int,
val totalPages: Int,
)

View File

@@ -2,10 +2,7 @@ package org.koitharu.kotatsu.utils.ext
import android.os.SystemClock
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.*
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
var isFirstCall = true
@@ -34,4 +31,8 @@ fun <T> Flow<T>.throttle(timeoutMillis: (T) -> Long): Flow<T> {
emit(value)
lastEmittedAt = now
}
}
}
fun <T> StateFlow<T?>.requireValue(): T = checkNotNull(value) {
"StateFlow value is null"
}

View File

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

View File

@@ -58,7 +58,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
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>
@@ -86,4 +98,4 @@
</LinearLayout>
</FrameLayout>
</FrameLayout>

View File

@@ -20,28 +20,9 @@
app:showAsAction="always" />
<item
android:id="@+id/action_screen_rotate"
android:icon="@drawable/ic_screen_rotation"
android:title="@string/rotate_screen"
android:visible="false"
android:id="@+id/action_menu"
android:icon="@drawable/abc_ic_menu_overflow_material"
android:title="@string/options"
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>

View File

@@ -356,4 +356,5 @@
<string name="enter_email_text">Enter your email to continue</string>
<string name="removed_from_favourites">Removed from favourites</string>
<string name="removed_from_s">Removed from \"%s\"</string>
<string name="options">Options</string>
</resources>

View File

@@ -21,6 +21,7 @@ class JsonSerializerTest {
categoryId = 20,
sortKey = 1,
createdAt = System.currentTimeMillis(),
deletedAt = 0L,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toFavouriteEntity()
@@ -71,6 +72,7 @@ class JsonSerializerTest {
page = 35,
scroll = 24.0f,
percent = 0.6f,
deletedAt = 0L,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toHistoryEntity()
@@ -87,9 +89,10 @@ class JsonSerializerTest {
order = SortOrder.RATING.name,
track = false,
isVisibleInLibrary = true,
deletedAt = 0L,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toFavouriteCategoryEntity()
assertEquals(entity, result)
}
}
}

View File

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