diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ReaderMode.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ReaderMode.kt index 792ca13b6..7d9f5fd2f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ReaderMode.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ReaderMode.kt @@ -4,7 +4,9 @@ enum class ReaderMode(val id: Int) { STANDARD(1), REVERSED(3), - WEBTOON(2); + VERTICAL(4), + WEBTOON(2), + ; companion object { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt index e454a7106..4808b347c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt @@ -7,6 +7,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment 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.vertical.VerticalReaderFragment import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment import java.util.EnumMap @@ -21,6 +22,7 @@ class ReaderManager( modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java modeMap[ReaderMode.REVERSED] = ReversedReaderFragment::class.java modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java + modeMap[ReaderMode.VERTICAL] = VerticalReaderFragment::class.java } val currentReader: BaseReaderFragment<*>? diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt index bf26e61e5..038deb6bc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt @@ -79,6 +79,7 @@ class ReaderConfigSheet : binding.buttonStandard.isChecked = mode == ReaderMode.STANDARD binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON + binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL binding.checkableGroup.addOnButtonCheckedListener(this) binding.buttonSavePage.setOnClickListener(this) @@ -153,6 +154,7 @@ class ReaderConfigSheet : R.id.button_standard -> ReaderMode.STANDARD R.id.button_webtoon -> ReaderMode.WEBTOON R.id.button_reversed -> ReaderMode.REVERSED + R.id.button_vertical -> ReaderMode.VERTICAL else -> return } if (newMode == mode) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vertical/VerticalPageAnimTransformer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vertical/VerticalPageAnimTransformer.kt new file mode 100644 index 000000000..7daf5462d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vertical/VerticalPageAnimTransformer.kt @@ -0,0 +1,31 @@ +package org.koitharu.kotatsu.reader.ui.pager.vertical + +import android.view.View +import androidx.viewpager2.widget.ViewPager2 + +class VerticalPageAnimTransformer : ViewPager2.PageTransformer { + + override fun transformPage(page: View, position: Float) = with(page) { + translationY = -position * height + pivotX = width / 2f + pivotY = height * 0.2f + cameraDistance = 20000f + when { + position < -1f || position > 1f -> { + alpha = 0f + rotationX = 0f + translationZ = -1f + } + position > 0f -> { + alpha = 1f + rotationX = 0f + translationZ = 0f + } + position <= 0f -> { + alpha = 1f + rotationX = -120 * position + translationZ = 2f + } + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vertical/VerticalReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vertical/VerticalReaderFragment.kt new file mode 100644 index 000000000..f221e0efa --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/vertical/VerticalReaderFragment.kt @@ -0,0 +1,186 @@ +package org.koitharu.kotatsu.reader.ui.pager.vertical + +import android.os.Build +import android.os.Bundle +import android.view.InputDevice +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import androidx.viewpager2.widget.ViewPager2 +import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.coroutineScope +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.prefs.ReaderAnimation +import org.koitharu.kotatsu.core.ui.list.lifecycle.PagerLifecycleDispatcher +import org.koitharu.kotatsu.core.util.ext.doOnPageChanged +import org.koitharu.kotatsu.core.util.ext.findCurrentViewHolder +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.recyclerView +import org.koitharu.kotatsu.core.util.ext.resetTransformations +import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding +import org.koitharu.kotatsu.reader.domain.PageLoader +import org.koitharu.kotatsu.reader.ui.ReaderState +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.NoAnimPageTransformer +import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder +import org.koitharu.kotatsu.reader.ui.pager.standard.PagerEventSupplier +import org.koitharu.kotatsu.reader.ui.pager.standard.PagesAdapter +import javax.inject.Inject +import kotlin.math.absoluteValue +import kotlin.math.sign + +@AndroidEntryPoint +class VerticalReaderFragment : BaseReaderFragment(), + View.OnGenericMotionListener { + + @Inject + lateinit var networkState: NetworkState + + @Inject + lateinit var pageLoader: PageLoader + + private var pagerLifecycleDispatcher: PagerLifecycleDispatcher? = null + + override fun onCreateViewBinding( + inflater: LayoutInflater, + container: ViewGroup?, + ) = FragmentReaderStandardBinding.inflate(inflater, container, false) + + override fun onViewBindingCreated( + binding: FragmentReaderStandardBinding, + savedInstanceState: Bundle?, + ) { + super.onViewBindingCreated(binding, savedInstanceState) + with(binding.pager) { + orientation = ViewPager2.ORIENTATION_VERTICAL + adapter = readerAdapter + offscreenPageLimit = 2 + doOnPageChanged(::notifyPageChanged) + setOnGenericMotionListener(this@VerticalReaderFragment) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + recyclerView?.defaultFocusHighlightEnabled = false + } + PagerEventSupplier(this).attach() + pagerLifecycleDispatcher = PagerLifecycleDispatcher(this).also { + registerOnPageChangeCallback(it) + } + } + + viewModel.pageAnimation.observe(viewLifecycleOwner) { + val transformer = when (it) { + ReaderAnimation.NONE -> NoAnimPageTransformer() + ReaderAnimation.DEFAULT -> null + ReaderAnimation.ADVANCED -> VerticalPageAnimTransformer() + } + binding.pager.setPageTransformer(transformer) + if (transformer == null) { + binding.pager.recyclerView?.children?.forEach { view -> + view.resetTransformations() + } + } + } + } + + override fun onDestroyView() { + pagerLifecycleDispatcher = null + requireViewBinding().pager.adapter = null + super.onDestroyView() + } + + override fun onZoomIn() { + (viewBinding?.pager?.findCurrentViewHolder() as? PageHolder)?.onZoomIn() + } + + override fun onZoomOut() { + (viewBinding?.pager?.findCurrentViewHolder() as? PageHolder)?.onZoomOut() + } + + override fun onGenericMotion(v: View?, event: MotionEvent): Boolean { + if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { + if (event.actionMasked == MotionEvent.ACTION_SCROLL) { + val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL) + val withCtrl = event.metaState and KeyEvent.META_CTRL_MASK != 0 + if (!withCtrl) { + switchPageBy(-axisValue.sign.toInt()) + return true + } + } + } + return false + } + + override suspend fun onPagesChanged(pages: List, pendingState: ReaderState?) = + coroutineScope { + val items = launch { + requireAdapter().setItems(pages) + yield() + pagerLifecycleDispatcher?.invalidate() + } + if (pendingState != null) { + val position = pages.indexOfFirst { + it.chapterId == pendingState.chapterId && it.index == pendingState.page + } + items.join() + if (position != -1) { + requireViewBinding().pager.setCurrentItem(position, false) + notifyPageChanged(position) + } else { + Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT) + .show() + } + } else { + items.join() + } + } + + override fun onCreateAdapter() = PagesAdapter( + lifecycleOwner = viewLifecycleOwner, + loader = pageLoader, + settings = viewModel.readerSettings, + networkState = networkState, + exceptionResolver = exceptionResolver, + ) + + override fun switchPageBy(delta: Int) { + with(requireViewBinding().pager) { + setCurrentItem(currentItem + delta, isAnimationEnabled()) + } + } + + override fun switchPageTo(position: Int, smooth: Boolean) { + with(requireViewBinding().pager) { + setCurrentItem( + position, + smooth && isAnimationEnabled() && (currentItem - position).absoluteValue < SMOOTH_SCROLL_LIMIT, + ) + } + } + + override fun getCurrentState(): ReaderState? = viewBinding?.run { + val adapter = pager.adapter as? BaseReaderAdapter<*> + val page = adapter?.getItemOrNull(pager.currentItem) ?: return@run null + ReaderState( + chapterId = page.chapterId, + page = page.index, + scroll = 0, + ) + } + + private fun notifyPageChanged(page: Int) { + viewModel.onCurrentPageChanged(page) + } + + companion object { + + const val SMOOTH_SCROLL_LIMIT = 3 + } +} diff --git a/app/src/main/res/drawable/ic_reader_vertical.xml b/app/src/main/res/drawable/ic_reader_vertical.xml new file mode 100644 index 000000000..a6bfedeab --- /dev/null +++ b/app/src/main/res/drawable/ic_reader_vertical.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/sheet_reader_config.xml b/app/src/main/res/layout/sheet_reader_config.xml index 5017ee185..95473de7c 100644 --- a/app/src/main/res/layout/sheet_reader_config.xml +++ b/app/src/main/res/layout/sheet_reader_config.xml @@ -101,6 +101,15 @@ android:text="@string/right_to_left" app:icon="@drawable/ic_reader_rtl" /> + + Volume %d Unknown volume Your reading progress will not be saved + Vertical