Track Page progress with StateFlow (#8749)

* Update ReaderProgressIndicator documentation

ReaderProgressIndicator is not always determinate (cc554530, #5605).

* Track Page progress with StateFlow

(cherry picked from commit 593172f891667c1510d1812917cad93bfa362363)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt
This commit is contained in:
Two-Ai 2022-12-16 22:18:50 -05:00 committed by Jobobby04
parent 4243d389f8
commit 5a449ec80a
4 changed files with 64 additions and 69 deletions

View File

@ -11,10 +11,9 @@ import androidx.annotation.IntRange
import com.google.android.material.progressindicator.CircularProgressIndicator import com.google.android.material.progressindicator.CircularProgressIndicator
/** /**
* A wrapper for [CircularProgressIndicator] that always rotates while being determinate. * A wrapper for [CircularProgressIndicator] that always rotates.
* *
* By always rotating we give the feedback to the user that the application isn't 'stuck', * By always rotating we give the feedback to the user that the application isn't 'stuck'.
* and by making it determinate the user also approximately knows how much the operation will take.
*/ */
class ReaderProgressIndicator @JvmOverloads constructor( class ReaderProgressIndicator @JvmOverloads constructor(
context: Context, context: Context,

View File

@ -17,7 +17,11 @@ import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.widget.ViewPagerAdapter import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import logcat.LogPriority import logcat.LogPriority
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
@ -27,7 +31,6 @@ import tachiyomi.decoder.ImageDecoder
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -62,15 +65,17 @@ class PagerPageHolder(
*/ */
private var errorLayout: ReaderErrorBinding? = null private var errorLayout: ReaderErrorBinding? = null
private val scope = CoroutineScope(Dispatchers.IO)
/** /**
* Subscription for status changes of the page. * Subscription for status changes of the page.
*/ */
private var statusSubscription: Subscription? = null private var statusSubscription: Subscription? = null
/** /**
* Subscription for progress changes of the page. * Job for progress changes of the page.
*/ */
private var progressSubscription: Subscription? = null private var progressJob: Job? = null
/** /**
* Subscription for status changes of the page. * Subscription for status changes of the page.
@ -80,7 +85,7 @@ class PagerPageHolder(
/** /**
* Subscription for progress changes of the page. * Subscription for progress changes of the page.
*/ */
private var extraProgressSubscription: Subscription? = null private var extraProgressJob: Job? = null
/** /**
* Subscription used to read the header of the image. This is needed in order to instantiate * Subscription used to read the header of the image. This is needed in order to instantiate
@ -91,8 +96,6 @@ class PagerPageHolder(
// SY --> // SY -->
var status: Page.State = Page.State.QUEUE var status: Page.State = Page.State.QUEUE
var extraStatus: Page.State = Page.State.QUEUE var extraStatus: Page.State = Page.State.QUEUE
var progress: Int = 0
var extraProgress: Int = 0
// SY <-- // SY <--
init { init {
@ -106,8 +109,8 @@ class PagerPageHolder(
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
super.onDetachedFromWindow() super.onDetachedFromWindow()
unsubscribeProgress(1) cancelProgressJob(1)
unsubscribeProgress(2) cancelProgressJob(2)
unsubscribeStatus(1) unsubscribeStatus(1)
unsubscribeStatus(2) unsubscribeStatus(2)
unsubscribeReadImageHeader() unsubscribeReadImageHeader()
@ -139,32 +142,19 @@ class PagerPageHolder(
} }
} }
/** private fun launchProgressJob() {
* Observes the progress of the page and updates view. progressJob?.cancel()
*/ progressJob = scope.launchUI {
private fun observeProgress() { page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
progressSubscription?.unsubscribe() }
progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS)
.map { page.progress }
.distinctUntilChanged()
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { value -> progressIndicator.setProgress(value) }
} }
private fun observeProgress2() { private fun launchProgressJob2() {
extraProgressSubscription?.unsubscribe() extraProgressJob?.cancel()
val extraPage = extraPage ?: return val extraPage = extraPage ?: return
extraProgressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS) extraProgressJob = scope.launchUI {
.map { extraPage.progress } extraPage.progressFlow.collectLatest { value -> progressIndicator.setProgress(((page.progressFlow.value + value) / 2 * 0.95f).roundToInt()) }
.distinctUntilChanged() }
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { value ->
extraProgress = value
progressIndicator.setProgress(((progress + extraProgress) / 2 * 0.95f).roundToInt())
}
} }
/** /**
@ -177,18 +167,18 @@ class PagerPageHolder(
Page.State.QUEUE -> setQueued() Page.State.QUEUE -> setQueued()
Page.State.LOAD_PAGE -> setLoading() Page.State.LOAD_PAGE -> setLoading()
Page.State.DOWNLOAD_IMAGE -> { Page.State.DOWNLOAD_IMAGE -> {
observeProgress() launchProgressJob()
setDownloading() setDownloading()
} }
Page.State.READY -> { Page.State.READY -> {
if (extraStatus == Page.State.READY || extraPage == null) { if (extraStatus == Page.State.READY || extraPage == null) {
setImage() setImage()
} }
unsubscribeProgress(1) cancelProgressJob(1)
} }
Page.State.ERROR -> { Page.State.ERROR -> {
setError() setError()
unsubscribeProgress(1) cancelProgressJob(1)
} }
} }
} }
@ -203,18 +193,18 @@ class PagerPageHolder(
Page.State.QUEUE -> setQueued() Page.State.QUEUE -> setQueued()
Page.State.LOAD_PAGE -> setLoading() Page.State.LOAD_PAGE -> setLoading()
Page.State.DOWNLOAD_IMAGE -> { Page.State.DOWNLOAD_IMAGE -> {
observeProgress2() launchProgressJob2()
setDownloading() setDownloading()
} }
Page.State.READY -> { Page.State.READY -> {
if (this.status == Page.State.READY) { if (this.status == Page.State.READY) {
setImage() setImage()
} }
unsubscribeProgress(2) cancelProgressJob(2)
} }
Page.State.ERROR -> { Page.State.ERROR -> {
setError() setError()
unsubscribeProgress(2) cancelProgressJob(2)
} }
} }
} }
@ -228,13 +218,10 @@ class PagerPageHolder(
if (page == 1) statusSubscription = null else extraStatusSubscription = null if (page == 1) statusSubscription = null else extraStatusSubscription = null
} }
/** private fun cancelProgressJob(page: Int) {
* Unsubscribes from the progress subscription. val job = if (page == 1) progressJob else extraProgressJob
*/ job?.cancel()
private fun unsubscribeProgress(page: Int) { if (page == 1) progressJob = null else extraProgressJob = null
val subscription = if (page == 1) progressSubscription else extraProgressSubscription
subscription?.unsubscribe()
if (page == 1) progressSubscription = null else extraProgressSubscription = null
} }
/** /**

View File

@ -18,15 +18,19 @@ import eu.kanade.tachiyomi.ui.reader.model.StencilPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit
/** /**
* Holder of the webtoon reader for a single page of a chapter. * Holder of the webtoon reader for a single page of a chapter.
@ -67,15 +71,17 @@ class WebtoonPageHolder(
*/ */
private var page: ReaderPage? = null private var page: ReaderPage? = null
private val scope = CoroutineScope(Dispatchers.IO)
/** /**
* Subscription for status changes of the page. * Subscription for status changes of the page.
*/ */
private var statusSubscription: Subscription? = null private var statusSubscription: Subscription? = null
/** /**
* Subscription for progress changes of the page. * Job for progress changes of the page.
*/ */
private var progressSubscription: Subscription? = null private var progressJob: Job? = null
/** /**
* Subscription used to read the header of the image. This is needed in order to instantiate * Subscription used to read the header of the image. This is needed in order to instantiate
@ -117,7 +123,7 @@ class WebtoonPageHolder(
*/ */
override fun recycle() { override fun recycle() {
unsubscribeStatus() unsubscribeStatus()
unsubscribeProgress() cancelProgressJob()
unsubscribeReadImageHeader() unsubscribeReadImageHeader()
removeErrorLayout() removeErrorLayout()
@ -145,19 +151,14 @@ class WebtoonPageHolder(
/** /**
* Observes the progress of the page and updates view. * Observes the progress of the page and updates view.
*/ */
private fun observeProgress() { private fun launchProgressJob() {
unsubscribeProgress() cancelProgressJob()
val page = page ?: return val page = page ?: return
progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS) progressJob = scope.launchUI {
.map { page.progress } page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
.distinctUntilChanged() }
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { value -> progressIndicator.setProgress(value) }
addSubscription(progressSubscription)
} }
/** /**
@ -170,16 +171,16 @@ class WebtoonPageHolder(
Page.State.QUEUE -> setQueued() Page.State.QUEUE -> setQueued()
Page.State.LOAD_PAGE -> setLoading() Page.State.LOAD_PAGE -> setLoading()
Page.State.DOWNLOAD_IMAGE -> { Page.State.DOWNLOAD_IMAGE -> {
observeProgress() launchProgressJob()
setDownloading() setDownloading()
} }
Page.State.READY -> { Page.State.READY -> {
setImage() setImage()
unsubscribeProgress() cancelProgressJob()
} }
Page.State.ERROR -> { Page.State.ERROR -> {
setError() setError()
unsubscribeProgress() cancelProgressJob()
} }
} }
} }
@ -195,9 +196,9 @@ class WebtoonPageHolder(
/** /**
* Unsubscribes from the progress subscription. * Unsubscribes from the progress subscription.
*/ */
private fun unsubscribeProgress() { private fun cancelProgressJob() {
removeSubscription(progressSubscription) progressJob?.cancel()
progressSubscription = null progressJob = null
} }
/** /**

View File

@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.source.model
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.ProgressListener import eu.kanade.tachiyomi.network.ProgressListener
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import rx.subjects.Subject import rx.subjects.Subject
@ -27,8 +29,14 @@ open class Page(
} }
@Transient @Transient
@Volatile private val _progressFlow = MutableStateFlow(0)
var progress: Int = 0 @Transient
val progressFlow = _progressFlow.asStateFlow()
var progress: Int
get() = _progressFlow.value
set(value) {
_progressFlow.value = value
}
@Transient @Transient
var statusSubject: Subject<State, State>? = null var statusSubject: Subject<State, State>? = null