Double reader integration
This commit is contained in:
@@ -6,6 +6,7 @@ enum class ReaderMode(val id: Int) {
|
|||||||
REVERSED(3),
|
REVERSED(3),
|
||||||
VERTICAL(4),
|
VERTICAL(4),
|
||||||
WEBTOON(2),
|
WEBTOON(2),
|
||||||
|
DOUBLE(5),
|
||||||
;
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import androidx.fragment.app.commit
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.doublepage.DoublePageReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.doublepage.DoubleReaderFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.vertical.VerticalReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.vertical.VerticalReaderFragment
|
||||||
@@ -23,7 +23,7 @@ class ReaderManager(
|
|||||||
init {
|
init {
|
||||||
val isTablet = container.resources.getBoolean(R.bool.is_tablet)
|
val isTablet = container.resources.getBoolean(R.bool.is_tablet)
|
||||||
modeMap[ReaderMode.STANDARD] = if (isTablet) {
|
modeMap[ReaderMode.STANDARD] = if (isTablet) {
|
||||||
DoublePageReaderFragment::class.java
|
DoubleReaderFragment::class.java
|
||||||
} else {
|
} else {
|
||||||
PagerReaderFragment::class.java
|
PagerReaderFragment::class.java
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class ReaderConfigSheet :
|
|||||||
binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED
|
binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED
|
||||||
binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON
|
binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON
|
||||||
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
|
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
|
||||||
|
binding.buttonDouble.isChecked = mode == ReaderMode.DOUBLE
|
||||||
|
|
||||||
binding.checkableGroup.addOnButtonCheckedListener(this)
|
binding.checkableGroup.addOnButtonCheckedListener(this)
|
||||||
binding.buttonSavePage.setOnClickListener(this)
|
binding.buttonSavePage.setOnClickListener(this)
|
||||||
@@ -155,6 +156,7 @@ class ReaderConfigSheet :
|
|||||||
R.id.button_webtoon -> ReaderMode.WEBTOON
|
R.id.button_webtoon -> ReaderMode.WEBTOON
|
||||||
R.id.button_reversed -> ReaderMode.REVERSED
|
R.id.button_reversed -> ReaderMode.REVERSED
|
||||||
R.id.button_vertical -> ReaderMode.VERTICAL
|
R.id.button_vertical -> ReaderMode.VERTICAL
|
||||||
|
R.id.button_double -> ReaderMode.DOUBLE
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
if (newMode == mode) {
|
if (newMode == mode) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
@@ -17,14 +17,16 @@ import org.koitharu.kotatsu.databinding.FragmentReaderDoubleBinding
|
|||||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
|
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class DoublePageReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>() {
|
class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var networkState: NetworkState
|
lateinit var networkState: NetworkState
|
||||||
@@ -44,7 +46,7 @@ class DoublePageReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>
|
|||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
adapter = readerAdapter
|
adapter = readerAdapter
|
||||||
addOnScrollListener(PageScrollListener())
|
addOnScrollListener(PageScrollListener(viewModel))
|
||||||
DoublePageSnapHelper().attachToRecyclerView(this)
|
DoublePageSnapHelper().attachToRecyclerView(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,28 +56,28 @@ class DoublePageReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) =
|
override suspend fun onPagesChanged(pages: List<ReaderPage>, pendingState: ReaderState?) = coroutineScope {
|
||||||
coroutineScope {
|
val items = launch {
|
||||||
val items = async {
|
requireAdapter().setItems(pages)
|
||||||
requireAdapter().setItems(pages)
|
yield()
|
||||||
yield()
|
|
||||||
}
|
|
||||||
if (pendingState != null) {
|
|
||||||
val position = pages.indexOfFirst {
|
|
||||||
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
|
||||||
}
|
|
||||||
items.await()
|
|
||||||
if (position != -1) {
|
|
||||||
requireViewBinding().recyclerView.firstVisibleItemPosition = position or 1
|
|
||||||
notifyPageChanged(position)
|
|
||||||
} else {
|
|
||||||
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
items.await()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (pendingState != null) {
|
||||||
|
var position = pages.indexOfFirst {
|
||||||
|
it.chapterId == pendingState.chapterId && it.index == pendingState.page
|
||||||
|
}
|
||||||
|
items.join()
|
||||||
|
if (position != -1) {
|
||||||
|
position = position or 1
|
||||||
|
requireViewBinding().recyclerView.firstVisibleItemPosition = position
|
||||||
|
viewModel.onCurrentPageChanged(position, position + 1)
|
||||||
|
} else {
|
||||||
|
Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateAdapter() = DoublePagesAdapter(
|
override fun onCreateAdapter() = DoublePagesAdapter(
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
@@ -85,6 +87,16 @@ class DoublePageReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>
|
|||||||
exceptionResolver = exceptionResolver,
|
exceptionResolver = exceptionResolver,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override fun onZoomIn() {
|
||||||
|
(viewBinding ?: return).recyclerView.pageHolders()
|
||||||
|
.forEach { it.onZoomIn() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onZoomOut() {
|
||||||
|
(viewBinding ?: return).recyclerView.pageHolders()
|
||||||
|
.forEach { it.onZoomOut() }
|
||||||
|
}
|
||||||
|
|
||||||
override fun switchPageBy(delta: Int) {
|
override fun switchPageBy(delta: Int) {
|
||||||
switchPageTo((requireViewBinding().recyclerView.currentItem() + delta) or 1, delta.absoluteValue > 1)
|
switchPageTo((requireViewBinding().recyclerView.currentItem() + delta) or 1, delta.absoluteValue > 1)
|
||||||
}
|
}
|
||||||
@@ -103,25 +115,38 @@ class DoublePageReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyPageChanged(page: Int) {
|
|
||||||
viewModel.onCurrentPageChanged(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun RecyclerView.currentItem(): Int {
|
private fun RecyclerView.currentItem(): Int {
|
||||||
val lm = layoutManager as LinearLayoutManager
|
val lm = layoutManager as LinearLayoutManager
|
||||||
return ((lm.findFirstVisibleItemPosition() + lm.findLastVisibleItemPosition()) / 2f).toIntUp()
|
return ((lm.findFirstVisibleItemPosition() + lm.findLastVisibleItemPosition()) / 2f).toIntUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class PageScrollListener : RecyclerView.OnScrollListener() {
|
private fun RecyclerView.pageHolders(): Sequence<PageHolder> {
|
||||||
|
val lm = layoutManager as? LinearLayoutManager ?: return emptySequence()
|
||||||
|
return (lm.findFirstVisibleItemPosition()..lm.findLastVisibleItemPosition()).asSequence()
|
||||||
|
.mapNotNull { findViewHolderForAdapterPosition(it) as? PageHolder }
|
||||||
|
}
|
||||||
|
|
||||||
private var lastPage = RecyclerView.NO_POSITION
|
private class PageScrollListener(
|
||||||
|
private val viewModel: ReaderViewModel,
|
||||||
|
) : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
|
private var firstPos = RecyclerView.NO_POSITION
|
||||||
|
private var lastPos = RecyclerView.NO_POSITION
|
||||||
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
val page = recyclerView.currentItem()
|
val lm = recyclerView.layoutManager as? LinearLayoutManager
|
||||||
if (page != lastPage) {
|
if (lm == null) {
|
||||||
lastPage = page
|
firstPos = RecyclerView.NO_POSITION
|
||||||
notifyPageChanged(page)
|
lastPos = RecyclerView.NO_POSITION
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val newFirstPos = lm.findFirstVisibleItemPosition()
|
||||||
|
val newLastPos = lm.findLastVisibleItemPosition()
|
||||||
|
if (newFirstPos != firstPos || newLastPos != lastPos) {
|
||||||
|
firstPos = newFirstPos
|
||||||
|
lastPos = newLastPos
|
||||||
|
viewModel.onCurrentPageChanged(newFirstPos, newLastPos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
24
app/src/main/res/drawable/ic_pager_double_ltr.xml
Normal file
24
app/src/main/res/drawable/ic_pager_double_ltr.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M4,4C2.89,4 2,4.89 2,6L2,18C2,19.11 2.89,20 4,20L9,20L9,18L8,18L6,18L4,18L4,6L6,6L8,6L9,6L9,4L4,4zM15,4L15,6L16,6L18,6L20,6L20,18L18,18L16,18L15,18L15,20L20,20C21.1,20 22,19.11 22,18L22,6C22,4.89 21.1,4 20,4L15,4z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="m11,22.43v-3h2v3z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="m13,1.57v3h-2v-3h2"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="m11,17.43v-3h2v3h-2"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="m11,9.57v-3h2v3h-2"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="m14.5,15v-2h-7v-2h7V9l4,3 -4,3"/>
|
||||||
|
</vector>
|
||||||
@@ -119,6 +119,15 @@
|
|||||||
android:text="@string/webtoon"
|
android:text="@string/webtoon"
|
||||||
app:icon="@drawable/ic_script" />
|
app:icon="@drawable/ic_script" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_double"
|
||||||
|
style="@style/Widget.Kotatsu.ToggleButton.Vertical"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/two_pages"
|
||||||
|
app:icon="@drawable/ic_pager_double_ltr" />
|
||||||
|
|
||||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -567,4 +567,5 @@
|
|||||||
<string name="incognito_mode_hint">Your reading progress will not be saved</string>
|
<string name="incognito_mode_hint">Your reading progress will not be saved</string>
|
||||||
<string name="vertical">Vertical</string>
|
<string name="vertical">Vertical</string>
|
||||||
<string name="last_read">Last read</string>
|
<string name="last_read">Last read</string>
|
||||||
|
<string name="two_pages">Two pages</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user