Track Page progress with StateFlow (#8749)
* Update ReaderProgressIndicator documentation
ReaderProgressIndicator is not always determinate (cc554530
, #5605).
* Track Page progress with StateFlow
This commit is contained in:
parent
e20c66b156
commit
593172f891
4 changed files with 54 additions and 50 deletions
|
@ -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,
|
||||||
|
|
|
@ -13,8 +13,13 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
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.widget.ViewPagerAdapter
|
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||||
|
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
|
||||||
|
@ -22,7 +27,6 @@ import rx.schedulers.Schedulers
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View of the ViewPager that contains a page of a chapter.
|
* View of the ViewPager that contains a page of a chapter.
|
||||||
|
@ -54,15 +58,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 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
|
||||||
|
@ -81,7 +87,7 @@ class PagerPageHolder(
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
super.onDetachedFromWindow()
|
super.onDetachedFromWindow()
|
||||||
unsubscribeProgress()
|
cancelProgressJob()
|
||||||
unsubscribeStatus()
|
unsubscribeStatus()
|
||||||
unsubscribeReadImageHeader()
|
unsubscribeReadImageHeader()
|
||||||
}
|
}
|
||||||
|
@ -100,18 +106,11 @@ class PagerPageHolder(
|
||||||
.subscribe { processStatus(it) }
|
.subscribe { processStatus(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
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) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -124,16 +123,16 @@ 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 -> {
|
||||||
setImage()
|
setImage()
|
||||||
unsubscribeProgress()
|
cancelProgressJob()
|
||||||
}
|
}
|
||||||
Page.State.ERROR -> {
|
Page.State.ERROR -> {
|
||||||
setError()
|
setError()
|
||||||
unsubscribeProgress()
|
cancelProgressJob()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,12 +145,9 @@ class PagerPageHolder(
|
||||||
statusSubscription = null
|
statusSubscription = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun cancelProgressJob() {
|
||||||
* Unsubscribes from the progress subscription.
|
progressJob?.cancel()
|
||||||
*/
|
progressJob = null
|
||||||
private fun unsubscribeProgress() {
|
|
||||||
progressSubscription?.unsubscribe()
|
|
||||||
progressSubscription = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
|
@ -26,8 +28,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
|
||||||
|
|
Reference in a new issue