diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
new file mode 100644
index 000000000..371f2e299
--- /dev/null
+++ b/.idea/appInsightsSettings.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt
index b90bd8c6b..7dc07c3cc 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt
@@ -488,6 +488,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_WEBTOON_GAPS, false)
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_GAPS, value) }
+ var isWebtoonPullGestureEnabled: Boolean
+ get() = prefs.getBoolean(KEY_WEBTOON_PULL_GESTURE, false)
+ set(value) = prefs.edit { putBoolean(KEY_WEBTOON_PULL_GESTURE, value) }
+
@get:FloatRange(from = 0.0, to = 0.5)
val defaultWebtoonZoomOut: Float
get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f
@@ -748,6 +752,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_WEBTOON_GAPS = "webtoon_gaps"
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
+ const val KEY_WEBTOON_PULL_GESTURE = "webtoon_pull_gesture"
const val KEY_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale"
const val KEY_SOURCES_GRID = "sources_grid"
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
index 8f0146c3e..e951521d2 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
@@ -157,6 +157,12 @@ class ReaderViewModel @Inject constructor(
valueProducer = { isWebtoonGapsEnabled },
)
+ val isWebtoonPullGestureEnabled = settings.observeAsStateFlow(
+ scope = viewModelScope + Dispatchers.Default,
+ key = AppSettings.KEY_WEBTOON_PULL_GESTURE,
+ valueProducer = { isWebtoonPullGestureEnabled },
+ )
+
val defaultWebtoonZoomOut = observeIsWebtoonZoomEnabled().flatMapLatest {
if (it) {
observeWebtoonZoomOut()
@@ -345,11 +351,14 @@ class ReaderViewModel @Inject constructor(
return@launchJob
}
ensureActive()
- if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
- loadPrevNextChapter(pages.last().chapterId, isNext = true)
- }
- if (lowerPos <= BOUNDS_PAGE_OFFSET) {
- loadPrevNextChapter(pages.first().chapterId, isNext = false)
+ val autoLoadAllowed = readerMode.value != ReaderMode.WEBTOON || !isWebtoonPullGestureEnabled.value
+ if (autoLoadAllowed) {
+ if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
+ loadPrevNextChapter(pages.last().chapterId, isNext = true)
+ }
+ if (lowerPos <= BOUNDS_PAGE_OFFSET) {
+ loadPrevNextChapter(pages.first().chapterId, isNext = false)
+ }
}
if (pageLoader.isPrefetchApplicable()) {
pageLoader.prefetch(pages.trySublist(upperPos + 1, upperPos + PREFETCH_LIMIT))
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 dc042aea8..577e9a9c7 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
@@ -86,6 +86,8 @@ class ReaderConfigSheet :
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape
binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD || mode == ReaderMode.REVERSED
+ binding.switchPullGesture.isChecked = settings.isWebtoonPullGestureEnabled
+ binding.switchPullGesture.isEnabled = mode == ReaderMode.WEBTOON
binding.checkableGroup.addOnButtonCheckedListener(this)
binding.buttonSavePage.setOnClickListener(this)
@@ -96,6 +98,7 @@ class ReaderConfigSheet :
binding.buttonScrollTimer.setOnClickListener(this)
binding.buttonBookmark.setOnClickListener(this)
binding.switchDoubleReader.setOnCheckedChangeListener(this)
+ binding.switchPullGesture.setOnCheckedChangeListener(this)
viewModel.isBookmarkAdded.observe(viewLifecycleOwner) {
binding.buttonBookmark.setText(if (it) R.string.bookmark_remove else R.string.bookmark_add)
@@ -172,6 +175,10 @@ class ReaderConfigSheet :
settings.isReaderDoubleOnLandscape = isChecked
findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked)
}
+
+ R.id.switch_pull_gesture -> {
+ settings.isWebtoonPullGestureEnabled = isChecked
+ }
}
}
@@ -191,6 +198,7 @@ class ReaderConfigSheet :
else -> return
}
viewBinding?.switchDoubleReader?.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED
+ viewBinding?.switchPullGesture?.isEnabled = newMode == ReaderMode.WEBTOON
if (newMode == mode) {
return
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt
index 000b539f5..15f4325e3 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt
@@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
+import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@@ -46,6 +47,8 @@ class WebtoonReaderFragment : BaseReaderFragment()
override fun onViewBindingCreated(binding: FragmentReaderWebtoonBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
+ var canGoPrev = true
+ var canGoNext = true
with(binding.recyclerView) {
setHasFixedSize(true)
adapter = readerAdapter
@@ -53,6 +56,40 @@ class WebtoonReaderFragment : BaseReaderFragment()
recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also {
addOnScrollListener(it)
}
+ setOnPullGestureListener(object : WebtoonRecyclerView.OnPullGestureListener {
+ override fun onPullProgressTop(progress: Float) {
+ if (canGoPrev) {
+ binding.feedbackTop.setText(R.string.pull_to_prev_chapter)
+ } else {
+ binding.feedbackTop.setText(R.string.pull_top_no_prev)
+ }
+ updateFeedback(binding.feedbackTop, progress)
+ }
+ override fun onPullProgressBottom(progress: Float) {
+ if (canGoNext) {
+ binding.feedbackBottom.setText(R.string.pull_to_next_chapter)
+ } else {
+ binding.feedbackBottom.setText(R.string.pull_bottom_no_next)
+ }
+ updateFeedback(binding.feedbackBottom, progress)
+ }
+ override fun onPullTriggeredTop() {
+ fadeOut(binding.feedbackTop)
+ if (canGoPrev) {
+ viewModel.switchChapterBy(-1)
+ }
+ }
+ override fun onPullTriggeredBottom() {
+ fadeOut(binding.feedbackBottom)
+ if (canGoNext) {
+ viewModel.switchChapterBy(1)
+ }
+ }
+ override fun onPullCancelled() {
+ fadeOut(binding.feedbackTop)
+ fadeOut(binding.feedbackBottom)
+ }
+ })
}
viewModel.isWebtoonZooEnabled.observe(viewLifecycleOwner) {
binding.frame.isZoomEnable = it
@@ -70,6 +107,21 @@ class WebtoonReaderFragment : BaseReaderFragment()
viewModel.readerSettingsProducer.observe(viewLifecycleOwner) {
it.applyBackground(binding.root)
}
+ viewModel.readerMode.observe(viewLifecycleOwner) { mode ->
+ binding.recyclerView.isPullGestureEnabled = (mode == org.koitharu.kotatsu.core.prefs.ReaderMode.WEBTOON) && viewModel.isWebtoonPullGestureEnabled.value
+ }
+ viewModel.isWebtoonPullGestureEnabled.observe(viewLifecycleOwner) { enabled ->
+ binding.recyclerView.isPullGestureEnabled = (viewModel.readerMode.value == org.koitharu.kotatsu.core.prefs.ReaderMode.WEBTOON) && enabled
+ }
+ viewModel.uiState.observe(viewLifecycleOwner) { state ->
+ if (state != null) {
+ canGoPrev = state.chapterIndex > 0
+ canGoNext = state.chapterIndex < state.chaptersTotal - 1
+ } else {
+ canGoPrev = true
+ canGoNext = true
+ }
+ }
}
override fun onDestroyView() {
@@ -178,3 +230,14 @@ class WebtoonReaderFragment : BaseReaderFragment()
return getChildAdapterPosition(view)
}
}
+
+private fun updateFeedback(tv: TextView, progress: Float) {
+ val clamped = progress.coerceIn(0f, 1.2f)
+ tv.alpha = clamped.coerceAtMost(1f)
+ tv.scaleX = 0.9f + 0.1f * clamped.coerceAtMost(1f)
+ tv.scaleY = tv.scaleX
+}
+
+private fun fadeOut(tv: TextView) {
+ tv.animate().alpha(0f).setDuration(150L).start()
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt
index 04eb2fd90..baf0bddc6 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonRecyclerView.kt
@@ -1,8 +1,10 @@
package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.content.Context
+import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
+import android.widget.EdgeEffect
import androidx.core.view.ViewCompat.TYPE_TOUCH
import androidx.core.view.forEach
import androidx.core.view.isEmpty
@@ -23,6 +25,71 @@ class WebtoonRecyclerView @JvmOverloads constructor(
private val detachedViews = Collections.newSetFromMap(WeakHashMap())
private var isFixingScroll = false
+ var isPullGestureEnabled: Boolean = false
+ var pullThreshold: Float = 0.3f
+ private var pullProgressTop: Float = 0f
+ private var pullProgressBottom: Float = 0f
+ private var pullListener: OnPullGestureListener? = null
+
+ init {
+ setEdgeEffectFactory(object : EdgeEffectFactory() {
+ override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
+ return object : EdgeEffect(view.context) {
+ override fun onPull(deltaDistance: Float) {
+ val sign = if (direction == DIRECTION_TOP) 1f else if (direction == DIRECTION_BOTTOM) 1f else 0f
+ if (sign != 0f) onPull(deltaDistance, 0.5f)
+ }
+
+ override fun onPull(deltaDistance: Float, displacement: Float) {
+ if (!isPullGestureEnabled) return
+ if (direction == DIRECTION_TOP) {
+ pullProgressTop = (pullProgressTop + deltaDistance).coerceAtLeast(0f)
+ pullListener?.onPullProgressTop(pullProgressTop / pullThreshold)
+ } else if (direction == DIRECTION_BOTTOM) {
+ pullProgressBottom = (pullProgressBottom + deltaDistance).coerceAtLeast(0f)
+ pullListener?.onPullProgressBottom(pullProgressBottom / pullThreshold)
+ }
+ }
+
+ override fun onRelease() {
+ if (!isPullGestureEnabled) {
+ pullProgressTop = 0f
+ pullProgressBottom = 0f
+ return
+ }
+ var triggered = false
+ if (direction == DIRECTION_TOP) {
+ if (pullProgressTop >= pullThreshold) {
+ pullListener?.onPullTriggeredTop()
+ triggered = true
+ }
+ pullProgressTop = 0f
+ pullListener?.onPullProgressTop(0f)
+ } else if (direction == DIRECTION_BOTTOM) {
+ if (pullProgressBottom >= pullThreshold) {
+ pullListener?.onPullTriggeredBottom()
+ triggered = true
+ }
+ pullProgressBottom = 0f
+ pullListener?.onPullProgressBottom(0f)
+ }
+ if (!triggered) {
+ pullListener?.onPullCancelled()
+ }
+ }
+
+ override fun draw(canvas: Canvas?): Boolean {
+ return false
+ }
+ }
+ }
+ })
+ }
+
+ fun setOnPullGestureListener(listener: OnPullGestureListener?) {
+ pullListener = listener
+ }
+
override fun onChildDetachedFromWindow(child: View) {
super.onChildDetachedFromWindow(child)
detachedViews.add(child)
@@ -188,4 +255,12 @@ class WebtoonRecyclerView @JvmOverloads constructor(
lastVisiblePosition: Int,
)
}
+
+ interface OnPullGestureListener {
+ fun onPullProgressTop(progress: Float)
+ fun onPullProgressBottom(progress: Float)
+ fun onPullTriggeredTop()
+ fun onPullTriggeredBottom()
+ fun onPullCancelled()
+ }
}
diff --git a/app/src/main/res/drawable/ic_gesture.xml b/app/src/main/res/drawable/ic_gesture.xml
new file mode 100644
index 000000000..6c4c9e374
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gesture.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_reader_webtoon.xml b/app/src/main/res/layout/fragment_reader_webtoon.xml
index d1d068765..766bb9063 100644
--- a/app/src/main/res/layout/fragment_reader_webtoon.xml
+++ b/app/src/main/res/layout/fragment_reader_webtoon.xml
@@ -16,4 +16,30 @@
android:orientation="vertical"
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />
+
+
+
+
diff --git a/app/src/main/res/layout/sheet_reader_config.xml b/app/src/main/res/layout/sheet_reader_config.xml
index c5f981b62..042a195b3 100644
--- a/app/src/main/res/layout/sheet_reader_config.xml
+++ b/app/src/main/res/layout/sheet_reader_config.xml
@@ -129,6 +129,20 @@
android:textColor="?colorOnSurfaceVariant"
app:drawableStartCompat="@drawable/ic_split_horizontal" />
+
+
Data and privacy
Restore previously created backup
Allow zoom in gesture in webtoon mode
+ Release to open previous chapter
+ Release to open next chapter
+ No previous chapter
+ No next chapter
Show the current time and reading progress at the top of the screen
Show page numbers in bottom corner
Clear cookies for specified domain only. In most cases will invalidate authorization
@@ -644,6 +648,8 @@
Show updated
Gaps in webtoon mode
Show vertical gaps between pages in webtoon mode
+ Enable pull gesture
+ Use pull gesture to switch chapters in webtoon
Less frequently
More frequently
Frequency of check