diff --git a/app/build.gradle b/app/build.gradle index 502612564..927147f4d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -134,7 +134,7 @@ dependencies { implementation 'io.coil-kt:coil-base:2.5.0' implementation 'io.coil-kt:coil-svg:2.5.0' - implementation 'com.github.KotatsuApp:subsampling-scale-image-view:cf089a264d' + implementation 'com.github.KotatsuApp:subsampling-scale-image-view:2bcc9791b1' implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'io.noties.markwon:core:4.6.2' diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/lifecycle/LifecycleAwareViewHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/lifecycle/LifecycleAwareViewHolder.kt new file mode 100644 index 000000000..33f75ed37 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/lifecycle/LifecycleAwareViewHolder.kt @@ -0,0 +1,88 @@ +package org.koitharu.kotatsu.core.ui.list.lifecycle + +import android.view.View +import androidx.annotation.CallSuper +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.recyclerview.widget.RecyclerView + +abstract class LifecycleAwareViewHolder( + itemView: View, + private val parentLifecycleOwner: LifecycleOwner, +) : RecyclerView.ViewHolder(itemView), LifecycleOwner { + + @Suppress("LeakingThis") + final override val lifecycle = LifecycleRegistry(this) + private var isCurrent = false + + init { + parentLifecycleOwner.lifecycle.addObserver(ParentLifecycleObserver()) + if (parentLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) { + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + } + } + + fun setIsCurrent(value: Boolean) { + isCurrent = value + dispatchResumed() + } + + @CallSuper + open fun onStart() = lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START) + + @CallSuper + open fun onResume() = lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + + @CallSuper + open fun onPause() = lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) + + @CallSuper + open fun onStop() = lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + + private fun dispatchResumed() { + val isParentResumed = parentLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) + if (isCurrent && isParentResumed) { + if (!isResumed()) { + onResume() + } + } else { + if (isResumed()) { + onPause() + } + } + } + + protected fun isResumed(): Boolean { + return lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) + } + + private inner class ParentLifecycleObserver : DefaultLifecycleObserver { + + override fun onCreate(owner: LifecycleOwner) { + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + } + + override fun onStart(owner: LifecycleOwner) { + onStart() + } + + override fun onResume(owner: LifecycleOwner) { + dispatchResumed() + } + + override fun onPause(owner: LifecycleOwner) { + dispatchResumed() + } + + override fun onStop(owner: LifecycleOwner) { + onStop() + } + + override fun onDestroy(owner: LifecycleOwner) { + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + owner.lifecycle.removeObserver(this) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/lifecycle/PagerLifecycleDispatcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/lifecycle/PagerLifecycleDispatcher.kt new file mode 100644 index 000000000..c6d9a72a3 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/list/lifecycle/PagerLifecycleDispatcher.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.core.ui.list.lifecycle + +import androidx.core.view.children +import androidx.viewpager2.widget.ViewPager2 +import org.koitharu.kotatsu.core.util.ext.recyclerView + +class PagerLifecycleDispatcher( + private val pager: ViewPager2, +) : ViewPager2.OnPageChangeCallback() { + + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + val rv = pager.recyclerView ?: return + for (child in rv.children) { + val wh = rv.getChildViewHolder(child) ?: continue + (wh as? LifecycleAwareViewHolder)?.setIsCurrent(wh.absoluteAdapterPosition == position) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt index b41dac8ed..5059e5512 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt @@ -2,13 +2,15 @@ package org.koitharu.kotatsu.reader.ui.pager import android.content.Context import androidx.annotation.CallSuper -import androidx.recyclerview.widget.RecyclerView +import androidx.lifecycle.LifecycleOwner import androidx.viewbinding.ViewBinding import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.core.ui.list.lifecycle.LifecycleAwareViewHolder import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.config.ReaderSettings +import org.koitharu.kotatsu.reader.ui.pager.PageHolderDelegate.State abstract class BasePageHolder( protected val binding: B, @@ -16,7 +18,8 @@ abstract class BasePageHolder( protected val settings: ReaderSettings, networkState: NetworkState, exceptionResolver: ExceptionResolver, -) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback { + lifecycleOwner: LifecycleOwner, +) : LifecycleAwareViewHolder(binding.root, lifecycleOwner), PageHolderDelegate.Callback { @Suppress("LeakingThis") protected val delegate = PageHolderDelegate(loader, settings, this, networkState, exceptionResolver) @@ -43,6 +46,13 @@ abstract class BasePageHolder( protected abstract fun onBind(data: ReaderPage) + override fun onResume() { + super.onResume() + if (delegate.state == State.ERROR && !delegate.isLoading()) { + boundData?.let { delegate.retry(it.toMangaPage(), isFromUser = false) } + } + } + @CallSuper open fun onAttachedToWindow() { delegate.onAttachedToWindow() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt index 9ac31c5f8..80af91780 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt @@ -35,7 +35,8 @@ class PageHolderDelegate( ) : DefaultOnImageEventListener, Observer { private val scope = loader.loaderScope + Dispatchers.Main.immediate - private var state = State.EMPTY + var state = State.EMPTY + private set private var job: Job? = null private var file: File? = null private var error: Throwable? = null @@ -44,6 +45,8 @@ class PageHolderDelegate( callback.onConfigChanged() } + fun isLoading() = job?.isActive == true + fun onBind(page: MangaPage) { val prevJob = job job = scope.launch { @@ -52,12 +55,15 @@ class PageHolderDelegate( } } - fun retry(page: MangaPage) { + fun retry(page: MangaPage, isFromUser: Boolean) { val prevJob = job job = scope.launch { prevJob?.cancelAndJoin() val e = error if (e != null && ExceptionResolver.canResolve(e)) { + if (!isFromUser) { + return@launch + } exceptionResolver.resolve(e) } doLoad(page, force = true) @@ -85,7 +91,7 @@ class PageHolderDelegate( } fun reload() { - if (state == State.SHOWN ) { + if (state == State.SHOWN) { file?.let { callback.onImageReady(it.toUri()) } @@ -166,7 +172,7 @@ class PageHolderDelegate( callback.onError(e) if (e is IOException && !networkState.value) { networkState.awaitForConnection() - retry(data) + retry(data, isFromUser = false) } } } @@ -176,7 +182,7 @@ class PageHolderDelegate( .onEach { callback.onProgressChanged((100 * it).toInt()) } .launchIn(scope) - private enum class State { + enum class State { EMPTY, LOADING, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt index d4f08e24c..42cdffb00 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt @@ -30,7 +30,7 @@ open class PageHolder( settings: ReaderSettings, networkState: NetworkState, exceptionResolver: ExceptionResolver, -) : BasePageHolder(binding, loader, settings, networkState, exceptionResolver), +) : BasePageHolder(binding, loader, settings, networkState, exceptionResolver, owner), View.OnClickListener, ZoomControl.ZoomControlListener { @@ -129,7 +129,7 @@ open class PageHolder( final override fun onClick(v: View) { when (v.id) { - R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return) + R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return, isFromUser = true) R.id.button_error_details -> delegate.showErrorDetails(boundData?.url) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt index 5e373db7d..fae8b9f75 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt @@ -17,6 +17,7 @@ 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 @@ -61,6 +62,7 @@ class PagerReaderFragment : BaseReaderFragment(), recyclerView?.defaultFocusHighlightEnabled = false } PagerEventSupplier(this).attach() + registerOnPageChangeCallback(PagerLifecycleDispatcher(this)) } viewModel.pageAnimation.observe(viewLifecycleOwner) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt index d2edc326a..764bd2125 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt @@ -25,7 +25,7 @@ class WebtoonHolder( settings: ReaderSettings, networkState: NetworkState, exceptionResolver: ExceptionResolver, -) : BasePageHolder(binding, loader, settings, networkState, exceptionResolver), +) : BasePageHolder(binding, loader, settings, networkState, exceptionResolver, owner), View.OnClickListener { private var scrollToRestore = 0 @@ -107,7 +107,7 @@ class WebtoonHolder( override fun onClick(v: View) { when (v.id) { - R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return) + R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return, isFromUser = true) R.id.button_error_details -> delegate.showErrorDetails(boundData?.url) } }