Show page loading progress
This commit is contained in:
@@ -10,13 +10,13 @@ import kotlinx.coroutines.flow.onEach
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
|
||||
import org.koitharu.kotatsu.download.domain.DownloadManager
|
||||
import org.koitharu.kotatsu.utils.JobStateFlow
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import org.koitharu.kotatsu.utils.progress.ProgressJob
|
||||
|
||||
fun downloadItemAD(
|
||||
scope: CoroutineScope,
|
||||
coil: ImageLoader,
|
||||
) = adapterDelegateViewBinding<JobStateFlow<DownloadManager.State>, JobStateFlow<DownloadManager.State>, ItemDownloadBinding>(
|
||||
) = adapterDelegateViewBinding<ProgressJob<DownloadManager.State>, ProgressJob<DownloadManager.State>, ItemDownloadBinding>(
|
||||
{ inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) }
|
||||
) {
|
||||
|
||||
@@ -24,7 +24,7 @@ fun downloadItemAD(
|
||||
|
||||
bind {
|
||||
job?.cancel()
|
||||
job = item.onFirst { state ->
|
||||
job = item.progressAsFlow().onFirst { state ->
|
||||
binding.imageViewCover.newImageRequest(state.manga.coverUrl)
|
||||
.referer(state.manga.publicUrl)
|
||||
.placeholder(state.cover)
|
||||
|
||||
@@ -5,12 +5,12 @@ import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.koitharu.kotatsu.download.domain.DownloadManager
|
||||
import org.koitharu.kotatsu.utils.JobStateFlow
|
||||
import org.koitharu.kotatsu.utils.progress.ProgressJob
|
||||
|
||||
class DownloadsAdapter(
|
||||
scope: CoroutineScope,
|
||||
coil: ImageLoader,
|
||||
) : AsyncListDifferDelegationAdapter<JobStateFlow<DownloadManager.State>>(DiffCallback()) {
|
||||
) : AsyncListDifferDelegationAdapter<ProgressJob<DownloadManager.State>>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(downloadItemAD(scope, coil))
|
||||
@@ -18,23 +18,23 @@ class DownloadsAdapter(
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return items[position].value.startId.toLong()
|
||||
return items[position].progressValue.startId.toLong()
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<JobStateFlow<DownloadManager.State>>() {
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ProgressJob<DownloadManager.State>>() {
|
||||
|
||||
override fun areItemsTheSame(
|
||||
oldItem: JobStateFlow<DownloadManager.State>,
|
||||
newItem: JobStateFlow<DownloadManager.State>,
|
||||
oldItem: ProgressJob<DownloadManager.State>,
|
||||
newItem: ProgressJob<DownloadManager.State>,
|
||||
): Boolean {
|
||||
return oldItem.value.startId == newItem.value.startId
|
||||
return oldItem.progressValue.startId == newItem.progressValue.startId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: JobStateFlow<DownloadManager.State>,
|
||||
newItem: JobStateFlow<DownloadManager.State>,
|
||||
oldItem: ProgressJob<DownloadManager.State>,
|
||||
newItem: ProgressJob<DownloadManager.State>,
|
||||
): Boolean {
|
||||
return oldItem.value == newItem.value
|
||||
return oldItem.progressValue == newItem.progressValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,8 @@ import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.download.domain.DownloadManager
|
||||
import org.koitharu.kotatsu.utils.JobStateFlow
|
||||
import org.koitharu.kotatsu.utils.ext.toArraySet
|
||||
import org.koitharu.kotatsu.utils.progress.ProgressJob
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.collections.set
|
||||
|
||||
@@ -42,7 +42,7 @@ class DownloadService : BaseService() {
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
private lateinit var downloadManager: DownloadManager
|
||||
|
||||
private val jobs = LinkedHashMap<Int, JobStateFlow<DownloadManager.State>>()
|
||||
private val jobs = LinkedHashMap<Int, ProgressJob<DownloadManager.State>>()
|
||||
private val jobCount = MutableStateFlow(0)
|
||||
private val mutex = Mutex()
|
||||
private val controlReceiver = ControlReceiver()
|
||||
@@ -93,7 +93,7 @@ class DownloadService : BaseService() {
|
||||
startId: Int,
|
||||
manga: Manga,
|
||||
chaptersIds: Set<Long>?,
|
||||
): JobStateFlow<DownloadManager.State> {
|
||||
): ProgressJob<DownloadManager.State> {
|
||||
val initialState = DownloadManager.State.Queued(startId, manga, null)
|
||||
val stateFlow = MutableStateFlow<DownloadManager.State>(initialState)
|
||||
val job = lifecycleScope.launch {
|
||||
@@ -131,7 +131,7 @@ class DownloadService : BaseService() {
|
||||
}
|
||||
}
|
||||
}
|
||||
return JobStateFlow(stateFlow, job)
|
||||
return ProgressJob(job, stateFlow)
|
||||
}
|
||||
|
||||
inner class ControlReceiver : BroadcastReceiver() {
|
||||
@@ -149,7 +149,7 @@ class DownloadService : BaseService() {
|
||||
|
||||
class DownloadBinder(private val service: DownloadService) : Binder() {
|
||||
|
||||
val downloads: Flow<Collection<JobStateFlow<DownloadManager.State>>>
|
||||
val downloads: Flow<Collection<ProgressJob<DownloadManager.State>>>
|
||||
get() = service.jobCount.mapLatest { service.jobs.values }
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.local.data
|
||||
|
||||
import android.content.Context
|
||||
import com.tomclaw.cache.DiskLruCache
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||
import org.koitharu.kotatsu.utils.ext.subdir
|
||||
@@ -30,4 +31,33 @@ class PagesCache(context: Context) {
|
||||
file.delete()
|
||||
return res
|
||||
}
|
||||
|
||||
fun put(
|
||||
url: String,
|
||||
inputStream: InputStream,
|
||||
contentLength: Long,
|
||||
progress: MutableStateFlow<Float>,
|
||||
): File {
|
||||
val file = File(cacheDir, url.longHashCode().toString())
|
||||
file.outputStream().use { out ->
|
||||
var bytesCopied: Long = 0
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var bytes = inputStream.read(buffer)
|
||||
while (bytes >= 0) {
|
||||
out.write(buffer, 0, bytes)
|
||||
bytesCopied += bytes
|
||||
publishProgress(contentLength, bytesCopied, progress)
|
||||
bytes = inputStream.read(buffer)
|
||||
}
|
||||
}
|
||||
val res = lruCache.put(url, file)
|
||||
file.delete()
|
||||
return res
|
||||
}
|
||||
|
||||
private fun publishProgress(contentLength: Long, bytesCopied: Long, progress: MutableStateFlow<Float>) {
|
||||
if (contentLength > 0) {
|
||||
progress.value = (bytesCopied.toDouble() / contentLength.toDouble()).toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.net.Uri
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -22,23 +24,28 @@ import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.await
|
||||
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
|
||||
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
private const val PROGRESS_UNDEFINED = -1f
|
||||
private const val PREFETCH_LIMIT_DEFAULT = 10
|
||||
|
||||
class PageLoader : KoinComponent, Closeable {
|
||||
|
||||
val loaderScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private val okHttp = get<OkHttpClient>()
|
||||
private val cache = get<PagesCache>()
|
||||
private val tasks = LongSparseArray<Deferred<File>>()
|
||||
private val tasks = LongSparseArray<ProgressDeferred<File, Float>>()
|
||||
private val convertLock = Mutex()
|
||||
private var repository: MangaRepository? = null
|
||||
private var prefetchQueue = LinkedList<MangaPage>()
|
||||
private val counter = AtomicInteger(0)
|
||||
private var prefetchQueueLimit = 10 // TODO adaptive
|
||||
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive
|
||||
private val emptyProgressFlow: StateFlow<Float> = MutableStateFlow(-1f)
|
||||
|
||||
override fun close() {
|
||||
loaderScope.cancel()
|
||||
@@ -66,21 +73,25 @@ class PageLoader : KoinComponent, Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadPage(page: MangaPage, force: Boolean): File {
|
||||
fun loadPageAsync(page: MangaPage, force: Boolean) : ProgressDeferred<File, Float> {
|
||||
if (!force) {
|
||||
cache[page.url]?.let {
|
||||
return it
|
||||
return getCompletedTask(it)
|
||||
}
|
||||
}
|
||||
var task = tasks[page.id]
|
||||
if (force) {
|
||||
task?.cancel()
|
||||
} else if (task?.isCancelled == false) {
|
||||
return task.await()
|
||||
return task
|
||||
}
|
||||
task = loadPageAsync(page)
|
||||
task = loadPageAsyncImpl(page)
|
||||
tasks[page.id] = task
|
||||
return task.await()
|
||||
return task
|
||||
}
|
||||
|
||||
suspend fun loadPage(page: MangaPage, force: Boolean): File {
|
||||
return loadPageAsync(page, force).await()
|
||||
}
|
||||
|
||||
suspend fun convertInPlace(file: File) {
|
||||
@@ -101,21 +112,23 @@ class PageLoader : KoinComponent, Closeable {
|
||||
private fun onIdle() {
|
||||
synchronized(prefetchQueue) {
|
||||
val page = prefetchQueue.pollFirst() ?: return
|
||||
tasks[page.id] = loadPageAsync(page)
|
||||
tasks[page.id] = loadPageAsyncImpl(page)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPageAsync(page: MangaPage): Deferred<File> {
|
||||
return loaderScope.async {
|
||||
private fun loadPageAsyncImpl(page: MangaPage): ProgressDeferred<File, Float> {
|
||||
val progress = MutableStateFlow(PROGRESS_UNDEFINED)
|
||||
val deferred = loaderScope.async {
|
||||
counter.incrementAndGet()
|
||||
try {
|
||||
loadPageImpl(page)
|
||||
loadPageImpl(page, progress)
|
||||
} finally {
|
||||
if (counter.decrementAndGet() == 0) {
|
||||
onIdle()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ProgressDeferred(deferred, progress)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -128,7 +141,7 @@ class PageLoader : KoinComponent, Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadPageImpl(page: MangaPage): File {
|
||||
private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow<Float>): File {
|
||||
val pageUrl = getRepository(page.source).getPageUrl(page)
|
||||
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
|
||||
val uri = Uri.parse(pageUrl)
|
||||
@@ -157,10 +170,15 @@ class PageLoader : KoinComponent, Closeable {
|
||||
}
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
body.byteStream().use {
|
||||
cache.put(pageUrl, it)
|
||||
cache.put(pageUrl, it, body.contentLength(), progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCompletedTask(file: File): ProgressDeferred<File, Float> {
|
||||
val deferred = CompletableDeferred(file)
|
||||
return ProgressDeferred(deferred, emptyProgressFlow)
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,17 @@ package org.koitharu.kotatsu.reader.ui.pager
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.utils.ext.launchAfter
|
||||
import org.koitharu.kotatsu.utils.ext.launchInstead
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
@@ -32,13 +31,17 @@ class PageHolderDelegate(
|
||||
private var error: Throwable? = null
|
||||
|
||||
fun onBind(page: MangaPage) {
|
||||
job = scope.launchInstead(job) {
|
||||
val prevJob = job
|
||||
job = scope.launch {
|
||||
prevJob?.cancelAndJoin()
|
||||
doLoad(page, force = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun retry(page: MangaPage) {
|
||||
job = scope.launchInstead(job) {
|
||||
val prevJob = job
|
||||
job = scope.launch {
|
||||
prevJob?.cancelAndJoin()
|
||||
(error as? ResolvableException)?.let {
|
||||
exceptionResolver.resolve(it)
|
||||
}
|
||||
@@ -69,30 +72,39 @@ class PageHolderDelegate(
|
||||
val file = this.file
|
||||
error = e
|
||||
if (state == State.LOADED && e is IOException && file != null && file.exists()) {
|
||||
job = scope.launchAfter(job) {
|
||||
state = State.CONVERTING
|
||||
try {
|
||||
loader.convertInPlace(file)
|
||||
state = State.CONVERTED
|
||||
callback.onImageReady(file.toUri())
|
||||
} catch (e2: Throwable) {
|
||||
e.addSuppressed(e2)
|
||||
state = State.ERROR
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
tryConvert(file, e)
|
||||
} else {
|
||||
state = State.ERROR
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doLoad(data: MangaPage, force: Boolean) {
|
||||
private fun tryConvert(file: File, e: Exception) {
|
||||
val prevJob = job
|
||||
job = scope.launch {
|
||||
prevJob?.join()
|
||||
state = State.CONVERTING
|
||||
try {
|
||||
loader.convertInPlace(file)
|
||||
state = State.CONVERTED
|
||||
callback.onImageReady(file.toUri())
|
||||
} catch (e2: Throwable) {
|
||||
e.addSuppressed(e2)
|
||||
state = State.ERROR
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun CoroutineScope.doLoad(data: MangaPage, force: Boolean) {
|
||||
state = State.LOADING
|
||||
error = null
|
||||
callback.onLoadingStarted()
|
||||
try {
|
||||
val file = loader.loadPage(data, force)
|
||||
val task = loader.loadPageAsync(data, force)
|
||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||
val file = task.await()
|
||||
progressObserver.cancel()
|
||||
this@PageHolderDelegate.file = file
|
||||
state = State.LOADED
|
||||
callback.onImageReady(file.toUri())
|
||||
@@ -105,6 +117,11 @@ class PageHolderDelegate(
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeProgress(scope: CoroutineScope, progress: Flow<Float>) = progress
|
||||
.debounce(500)
|
||||
.onEach { callback.onProgressChanged((100 * it).toInt()) }
|
||||
.launchIn(scope)
|
||||
|
||||
private enum class State {
|
||||
EMPTY, LOADING, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR
|
||||
}
|
||||
@@ -120,5 +137,7 @@ class PageHolderDelegate(
|
||||
fun onImageShowing(zoom: ZoomMode)
|
||||
|
||||
fun onImageShown()
|
||||
|
||||
fun onProgressChanged(progress: Int)
|
||||
}
|
||||
}
|
||||
@@ -44,9 +44,14 @@ open class PageHolder(
|
||||
override fun onLoadingStarted() {
|
||||
binding.layoutError.isVisible = false
|
||||
binding.progressBar.isVisible = true
|
||||
binding.textViewProgress.isVisible = true
|
||||
binding.ssiv.recycle()
|
||||
}
|
||||
|
||||
override fun onProgressChanged(progress: Int) {
|
||||
binding.textViewProgress.text = if (progress in 0..100) "%d%%".format(progress) else null
|
||||
}
|
||||
|
||||
override fun onImageReady(uri: Uri) {
|
||||
binding.ssiv.setImage(ImageSource.uri(uri))
|
||||
}
|
||||
@@ -89,6 +94,7 @@ open class PageHolder(
|
||||
|
||||
override fun onImageShown() {
|
||||
binding.progressBar.isVisible = false
|
||||
binding.textViewProgress.isVisible = false
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
@@ -104,5 +110,6 @@ open class PageHolder(
|
||||
)
|
||||
binding.layoutError.isVisible = true
|
||||
binding.progressBar.isVisible = false
|
||||
binding.textViewProgress.isVisible = false
|
||||
}
|
||||
}
|
||||
@@ -44,9 +44,14 @@ class WebtoonHolder(
|
||||
override fun onLoadingStarted() {
|
||||
binding.layoutError.isVisible = false
|
||||
binding.progressBar.isVisible = true
|
||||
binding.textViewProgress.isVisible = true
|
||||
binding.ssiv.recycle()
|
||||
}
|
||||
|
||||
override fun onProgressChanged(progress: Int) {
|
||||
binding.textViewProgress.text = if (progress in 0..100) "%d%%".format(progress) else null
|
||||
}
|
||||
|
||||
override fun onImageReady(uri: Uri) {
|
||||
binding.ssiv.setImage(ImageSource.uri(uri))
|
||||
}
|
||||
@@ -68,6 +73,7 @@ class WebtoonHolder(
|
||||
|
||||
override fun onImageShown() {
|
||||
binding.progressBar.isVisible = false
|
||||
binding.textViewProgress.isVisible = false
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
@@ -83,6 +89,7 @@ class WebtoonHolder(
|
||||
)
|
||||
binding.layoutError.isVisible = true
|
||||
binding.progressBar.isVisible = false
|
||||
binding.textViewProgress.isVisible = false
|
||||
}
|
||||
|
||||
fun getScrollY() = binding.ssiv.getScroll()
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
||||
class DeferredStateFlow<R, S>(
|
||||
private val stateFlow: StateFlow<S>,
|
||||
private val deferred: Deferred<R>,
|
||||
) : StateFlow<S> by stateFlow, Deferred<R> by deferred {
|
||||
|
||||
suspend fun collectAndAwait(): R {
|
||||
return coroutineScope {
|
||||
val collectJob = launchIn(this)
|
||||
val result = await()
|
||||
collectJob.cancelAndJoin()
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
||||
class JobStateFlow<S>(
|
||||
private val stateFlow: StateFlow<S>,
|
||||
private val job: Job,
|
||||
) : StateFlow<S> by stateFlow, Job by job {
|
||||
|
||||
suspend fun collectAndJoin(): Unit {
|
||||
coroutineScope {
|
||||
val collectJob = launchIn(this)
|
||||
join()
|
||||
collectJob.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,47 +4,9 @@ import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
inline fun CoroutineScope.launchAfter(
|
||||
job: Job?,
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
crossinline block: suspend CoroutineScope.() -> Unit
|
||||
): Job = launch(context, start) {
|
||||
try {
|
||||
job?.join()
|
||||
} catch (e: Throwable) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
block()
|
||||
}
|
||||
|
||||
inline fun CoroutineScope.launchInstead(
|
||||
job: Job?,
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
crossinline block: suspend CoroutineScope.() -> Unit
|
||||
): Job = launch(context, start) {
|
||||
try {
|
||||
job?.cancelAndJoin()
|
||||
} catch (e: Throwable) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
block()
|
||||
}
|
||||
|
||||
val IgnoreErrors
|
||||
get() = CoroutineExceptionHandler { _, e ->
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.utils.progress
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class ProgressDeferred<T, P>(
|
||||
private val deferred: Deferred<T>,
|
||||
private val progress: StateFlow<P>,
|
||||
) : Deferred<T> by deferred {
|
||||
|
||||
val progressValue: P
|
||||
get() = progress.value
|
||||
|
||||
fun progressAsFlow(): Flow<P> = progress
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.utils.progress
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class ProgressJob<P>(
|
||||
private val job: Job,
|
||||
private val progress: StateFlow<P>,
|
||||
) : Job by job {
|
||||
|
||||
val progressValue: P
|
||||
get() = progress.value
|
||||
|
||||
fun progressAsFlow(): Flow<P> = progress
|
||||
}
|
||||
@@ -19,6 +19,15 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
|
||||
tools:text="146%" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_number"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -39,8 +48,7 @@
|
||||
android:layout_marginEnd="60dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_error"
|
||||
@@ -52,7 +60,7 @@
|
||||
app:drawableTopCompat="@drawable/ic_error_large"
|
||||
tools:text="@tools:sample/lorem[6]" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
<Button
|
||||
android:id="@+id/button_retry"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -21,6 +21,15 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
|
||||
tools:text="146%" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_error"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
Reference in New Issue
Block a user