Double reader integration

This commit is contained in:
Koitharu
2024-01-29 18:57:18 +02:00
parent 4194609929
commit 131a0ffcaa
13 changed files with 127 additions and 36 deletions

View File

@@ -101,6 +101,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
}
var isReaderDoubleOnLandscape: Boolean
get() = prefs.getBoolean(KEY_READER_DOUBLE_PAGES, false)
set(value) = prefs.edit { putBoolean(KEY_READER_DOUBLE_PAGES, value) }
val isReaderVolumeButtonsEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_VOLUME_BUTTONS, false)
@@ -472,6 +476,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_GRID_SIZE = "grid_size"
const val KEY_REMOTE_SOURCES = "remote_sources"
const val KEY_LOCAL_STORAGE = "local_storage"
const val KEY_READER_DOUBLE_PAGES = "reader_double_pages"
const val KEY_READER_ZOOM_BUTTONS = "reader_zoom_buttons"
const val KEY_READER_VOLUME_BUTTONS = "reader_volume_buttons"
const val KEY_TRACKER_ENABLED = "tracker_enabled"

View File

@@ -6,7 +6,6 @@ enum class ReaderMode(val id: Int) {
REVERSED(3),
VERTICAL(4),
WEBTOON(2),
DOUBLE(5),
;
companion object {

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build.VERSION_CODES.R
import android.os.Bundle
import android.transition.Fade
import android.transition.Slide
@@ -108,7 +109,7 @@ class ReaderActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityReaderBinding.inflate(layoutInflater))
readerManager = ReaderManager(supportFragmentManager, viewBinding.container)
readerManager = ReaderManager(supportFragmentManager, viewBinding.container, settings)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = TapGridDispatcher(this, this)
scrollTimer = scrollTimerFactory.create(this, this)

View File

@@ -1,9 +1,10 @@
package org.koitharu.kotatsu.reader.ui
import android.content.res.Configuration
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.doublepage.DoubleReaderFragment
@@ -16,13 +17,13 @@ import java.util.EnumMap
class ReaderManager(
private val fragmentManager: FragmentManager,
private val container: FragmentContainerView,
private val settings: AppSettings,
) {
private val modeMap = EnumMap<ReaderMode, Class<out BaseReaderFragment<*>>>(ReaderMode::class.java)
init {
val isTablet = container.resources.getBoolean(R.bool.is_tablet)
modeMap[ReaderMode.STANDARD] = if (isTablet) {
modeMap[ReaderMode.STANDARD] = if (useDoublePages()) {
DoubleReaderFragment::class.java
} else {
PagerReaderFragment::class.java
@@ -49,6 +50,9 @@ class ReaderManager(
}
}
private fun useDoublePages() = container.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& settings.isReaderDoubleOnLandscape
/*fun replace(reader: BaseReaderFragment<*>) {
fragmentManager.commit {
setReorderingAllowed(true)

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.reader.ui.config
import android.net.Uri
import android.os.Build.VERSION_CODES.R
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -80,7 +81,8 @@ class ReaderConfigSheet :
binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED
binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
binding.buttonDouble.isChecked = mode == ReaderMode.DOUBLE
binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape
binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD
binding.checkableGroup.addOnButtonCheckedListener(this)
binding.buttonSavePage.setOnClickListener(this)
@@ -89,6 +91,7 @@ class ReaderConfigSheet :
binding.buttonColorFilter.setOnClickListener(this)
binding.sliderTimer.addOnChangeListener(this)
binding.switchScrollTimer.setOnCheckedChangeListener(this)
binding.switchDoubleReader.setOnCheckedChangeListener(this)
settings.observeAsStateFlow(
scope = lifecycleScope + Dispatchers.Default,
@@ -140,6 +143,11 @@ class ReaderConfigSheet :
R.id.switch_screen_lock_rotation -> {
orientationHelper.isLocked = isChecked
}
R.id.switch_double_reader -> {
settings.isReaderDoubleOnLandscape = isChecked
findCallback()?.onReaderModeChanged(mode)
}
}
}
@@ -156,9 +164,9 @@ class ReaderConfigSheet :
R.id.button_webtoon -> ReaderMode.WEBTOON
R.id.button_reversed -> ReaderMode.REVERSED
R.id.button_vertical -> ReaderMode.VERTICAL
R.id.button_double -> ReaderMode.DOUBLE
else -> return
}
viewBinding?.switchDoubleReader?.isEnabled = newMode == ReaderMode.STANDARD
if (newMode == mode) {
return
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.reader.ui.pager
import android.os.Build
import android.os.Build.VERSION_CODES.R
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
@@ -75,7 +76,7 @@ abstract class BasePagerReaderFragment : BaseReaderFragment<FragmentReaderPagerB
viewModel.pageAnimation.observe(viewLifecycleOwner) {
val transformer = when (it) {
ReaderAnimation.NONE -> NoAnimPageTransformer()
ReaderAnimation.NONE -> NoAnimPageTransformer(binding.pager.orientation)
ReaderAnimation.DEFAULT -> null
ReaderAnimation.ADVANCED -> onCreateAdvancedTransformer()
}

View File

@@ -16,4 +16,10 @@ class DoublePageLayoutManager(
lp?.width = width / 2
return super.checkLayoutParams(lp)
}
override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) {
val offscreenSpace = width / 2
extraLayoutSpace[0] = offscreenSpace
extraLayoutSpace[1] = offscreenSpace
}
}

View File

@@ -1,8 +1,10 @@
package org.koitharu.kotatsu.reader.ui.pager.doublepage
import android.os.Build.VERSION_CODES.R
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
@@ -12,16 +14,15 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.ui.list.lifecycle.RecyclerViewLifecycleDispatcher
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.databinding.FragmentReaderDoubleBinding
import org.koitharu.kotatsu.parsers.util.toIntUp
import org.koitharu.kotatsu.reader.domain.PageLoader
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.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
import javax.inject.Inject
import kotlin.math.absoluteValue
@@ -34,6 +35,8 @@ class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>() {
@Inject
lateinit var pageLoader: PageLoader
private var recyclerLifecycleDispatcher: RecyclerViewLifecycleDispatcher? = null
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -46,12 +49,16 @@ class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>() {
super.onViewBindingCreated(binding, savedInstanceState)
with(binding.recyclerView) {
adapter = readerAdapter
recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also {
addOnScrollListener(it)
}
addOnScrollListener(PageScrollListener(viewModel))
DoublePageSnapHelper().attachToRecyclerView(this)
}
}
override fun onDestroyView() {
recyclerLifecycleDispatcher = null
requireViewBinding().recyclerView.adapter = null
super.onDestroyView()
}
@@ -60,6 +67,9 @@ class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>() {
val items = launch {
requireAdapter().setItems(pages)
yield()
viewBinding?.recyclerView?.let { rv ->
recyclerLifecycleDispatcher?.invalidate(rv)
}
}
if (pendingState != null) {
var position = pages.indexOfFirst {
@@ -67,7 +77,7 @@ class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>() {
}
items.join()
if (position != -1) {
position = position or 1
position = position.toPagePosition()
requireViewBinding().recyclerView.firstVisibleItemPosition = position
viewModel.onCurrentPageChanged(position, position + 1)
} else {
@@ -88,26 +98,34 @@ class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>() {
)
override fun onZoomIn() {
(viewBinding ?: return).recyclerView.pageHolders()
(viewBinding ?: return).recyclerView.visiblePageHolders()
.forEach { it.onZoomIn() }
}
override fun onZoomOut() {
(viewBinding ?: return).recyclerView.pageHolders()
(viewBinding ?: return).recyclerView.visiblePageHolders()
.forEach { it.onZoomOut() }
}
override fun switchPageBy(delta: Int) {
switchPageTo((requireViewBinding().recyclerView.currentItem() + delta) or 1, delta.absoluteValue > 1)
if (delta.absoluteValue > 1 || !isAnimationEnabled()) {
switchPageTo(getCurrentItem() + delta + delta, false)
return
}
val rv = viewBinding?.recyclerView ?: return
val distance = rv.width * delta
rv.smoothScrollBy(distance, 0, AccelerateDecelerateInterpolator())
}
override fun switchPageTo(position: Int, smooth: Boolean) {
requireViewBinding().recyclerView.firstVisibleItemPosition = position or 1
val lm = viewBinding?.recyclerView?.layoutManager as? LinearLayoutManager ?: return
val targetPosition = position.toPagePosition()
lm.scrollToPositionWithOffset(targetPosition, 0)
}
override fun getCurrentState(): ReaderState? = viewBinding?.run {
val adapter = recyclerView.adapter as? BaseReaderAdapter<*>
val page = adapter?.getItemOrNull(recyclerView.currentItem()) ?: return@run null
val page = adapter?.getItemOrNull(getCurrentItem()) ?: return@run null
ReaderState(
chapterId = page.chapterId,
page = page.index,
@@ -115,16 +133,10 @@ class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>() {
)
}
private fun RecyclerView.currentItem(): Int {
val lm = layoutManager as LinearLayoutManager
return ((lm.findFirstVisibleItemPosition() + lm.findLastVisibleItemPosition()) / 2f).toIntUp()
}
private fun getCurrentItem() = (requireViewBinding().recyclerView.layoutManager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition().toPagePosition()
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 fun Int.toPagePosition() = this and 1.inv()
private class PageScrollListener(
private val viewModel: ReaderViewModel,

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.reader.ui.pager.doublepage
import androidx.core.view.children
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
fun RecyclerView.visiblePageHolders(): Sequence<PageHolder> {
val lm = layoutManager as? LinearLayoutManager ?: return emptySequence()
return (lm.findFirstVisibleItemPosition()..lm.findLastVisibleItemPosition()).asSequence()
.mapNotNull { findViewHolderForAdapterPosition(it) as? PageHolder }
}
fun RecyclerView.allPageHolders(): Sequence<PageHolder> {
return children.mapNotNull {
findContainingViewHolder(it) as? PageHolder
}
}

View File

@@ -3,13 +3,22 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
import android.view.View
import androidx.viewpager2.widget.ViewPager2
class NoAnimPageTransformer : ViewPager2.PageTransformer {
class NoAnimPageTransformer(
private val orientation: Int
) : ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) {
page.translationX = when {
orientation != ViewPager2.ORIENTATION_HORIZONTAL -> 0f
position in -0.5f..0.5f -> -position * page.width.toFloat()
position > 0 -> page.width.toFloat()
else -> -page.width.toFloat()
}
page.translationY = when {
orientation != ViewPager2.ORIENTATION_VERTICAL -> 0f
position in -0.5f..0.5f -> -position * page.height.toFloat()
position > 0 -> page.height.toFloat()
else -> -page.height.toFloat()
}
}
}

View File

@@ -0,0 +1,22 @@
<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,21l-0,-3l2,0l-0,3z" />
<path
android:fillColor="#FF000000"
android:pathData="m13,3l-0,3l-2,0l-0,-3l2,0" />
<path
android:fillColor="#FF000000"
android:pathData="m11,16l-0,-3l2,0l-0,3l-2,0" />
<path
android:fillColor="#FF000000"
android:pathData="m11,11l-0,-3l2,0l-0,3l-2,0" />
</vector>

View File

@@ -119,15 +119,6 @@
android:text="@string/webtoon"
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>
<TextView
@@ -139,7 +130,7 @@
android:textAppearance="?attr/textAppearanceBodySmall" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_scroll_timer"
android:id="@+id/switch_double_reader"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:layout_marginTop="@dimen/margin_normal"
@@ -148,6 +139,20 @@
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:singleLine="true"
android:text="@string/use_two_pages_landscape"
android:textAppearance="?attr/textAppearanceButton"
android:textColor="?colorOnSurfaceVariant"
app:drawableStartCompat="@drawable/ic_split_horizontal" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_scroll_timer"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:ellipsize="end"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:singleLine="true"
android:text="@string/automatic_scroll"
android:textAppearance="?attr/textAppearanceButton"
android:textColor="?colorOnSurfaceVariant"

View File

@@ -582,4 +582,5 @@
<string name="long_tap_action">Long tap action</string>
<string name="none">None</string>
<string name="config_reset_confirm">Reset settings to default values? This action cannot be undone.</string>
<string name="use_two_pages_landscape">Use two pages layout on landscape orientation (beta)</string>
</resources>