fix: Download progress not progressing

This commit is contained in:
Ahmad Ansori Palembani 2024-12-12 20:50:21 +07:00
parent 391496f5e8
commit bc97bdca3c
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
12 changed files with 271 additions and 40 deletions

View file

@ -15,6 +15,14 @@ import eu.kanade.tachiyomi.util.system.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import uy.kohesive.injekt.injectLazy
import yokai.domain.download.DownloadPreferences
@ -405,4 +413,33 @@ class DownloadManager(val context: Context) {
chapters
}
}
fun statusFlow(): Flow<Download> = queueState
.flatMapLatest { downloads ->
downloads
.map { download ->
download.statusFlow.drop(1).map { download }
}
.merge()
}
.onStart {
emitAll(
queueState.value.filter { download -> download.status == Download.State.DOWNLOADING }.asFlow(),
)
}
fun progressFlow(): Flow<Download> = queueState
.flatMapLatest { downloads ->
downloads
.map { download ->
download.progressFlow.drop(1).map { download }
}
.merge()
}
.onStart {
emitAll(
queueState.value.filter { download -> download.status == Download.State.DOWNLOADING }
.asFlow(),
)
}
}

View file

@ -5,8 +5,14 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlin.math.roundToInt
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
@ -29,6 +35,21 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
_statusFlow.value = status
}
@Transient
val progressFlow = flow {
if (pages == null) {
emit(0)
while (pages == null) {
delay(50)
}
}
val progressFlows = pages!!.map(Page::progressFlow)
emitAll(combine(progressFlows) { it.average().toInt() })
}
.distinctUntilChanged()
.debounce(50)
val pageProgress: Int
get() {
val pages = pages ?: return 0

View file

@ -0,0 +1,84 @@
package eu.kanade.tachiyomi.data.download.model
import androidx.annotation.CallSuper
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.system.launchUI
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
sealed class DownloadQueue {
interface Listener {
val progressJobs: MutableMap<Download, Job>
// Override with presenterScope or viewScope
val queueListenerScope: CoroutineScope
fun onPageProgressUpdate(download: Download) {
onProgressUpdate(download)
}
fun onProgressUpdate(download: Download)
fun onQueueUpdate(download: Download)
// Subscribe on presenter/controller creation on UI thread
@CallSuper
fun onStatusChange(download: Download) {
when (download.status) {
Download.State.DOWNLOADING -> {
launchProgressJob(download)
// Initial update of the downloaded pages
onQueueUpdate(download)
}
Download.State.DOWNLOADED -> {
cancelProgressJob(download)
onProgressUpdate(download)
onQueueUpdate(download)
}
Download.State.ERROR -> cancelProgressJob(download)
else -> {
/* unused */
}
}
}
/**
* Observe the progress of a download and notify the view.
*
* @param download the download to observe its progress.
*/
private fun launchProgressJob(download: Download) {
val job = queueListenerScope.launchUI {
while (download.pages == null) {
delay(50)
}
val progressFlows = download.pages!!.map(Page::progressFlow)
combine(progressFlows, Array<Int>::sum)
.distinctUntilChanged()
.debounce(50)
.collectLatest {
onPageProgressUpdate(download)
}
}
// Avoid leaking jobs
progressJobs.remove(download)?.cancel()
progressJobs[download] = job
}
/**
* Unsubscribes the given download from the progress subscriptions.
*
* @param download the download to unsubscribe.
*/
private fun cancelProgressJob(download: Download) {
progressJobs.remove(download)?.cancel()
}
}
}

View file

@ -1,10 +1,11 @@
package eu.kanade.tachiyomi.ui.base.presenter
import androidx.annotation.CallSuper
import java.lang.ref.WeakReference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import java.lang.ref.WeakReference
open class BaseCoroutinePresenter<T> {
var presenterScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@ -24,6 +25,7 @@ open class BaseCoroutinePresenter<T> {
open fun onCreate() {
}
@CallSuper
open fun onDestroy() {
presenterScope.cancel()
weakView = null

View file

@ -2,8 +2,11 @@ package eu.kanade.tachiyomi.ui.download
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
import eu.kanade.tachiyomi.util.system.launchUI
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy
@ -11,7 +14,8 @@ import uy.kohesive.injekt.injectLazy
/**
* Presenter of [DownloadBottomSheet].
*/
class DownloadBottomPresenter : BaseCoroutinePresenter<DownloadBottomSheet>() {
class DownloadBottomPresenter : BaseCoroutinePresenter<DownloadBottomSheet>(),
DownloadQueue.Listener {
/**
* Download manager.
@ -19,12 +23,24 @@ class DownloadBottomPresenter : BaseCoroutinePresenter<DownloadBottomSheet>() {
val downloadManager: DownloadManager by injectLazy()
var items = listOf<DownloadHeaderItem>()
override val progressJobs = mutableMapOf<Download, Job>()
override val queueListenerScope get() = presenterScope
/**
* Property to get the queue from the download manager.
*/
val downloadQueueState
get() = downloadManager.queueState
override fun onCreate() {
presenterScope.launchUI {
downloadManager.statusFlow().collect(::onStatusChange)
}
presenterScope.launchUI {
downloadManager.progressFlow().collect { view?.onUpdateDownloadedPages(it) }
}
}
fun getItems() {
presenterScope.launch {
val items = downloadQueueState.value
@ -84,4 +100,22 @@ class DownloadBottomPresenter : BaseCoroutinePresenter<DownloadBottomSheet>() {
fun cancelDownloads(downloads: List<Download>) {
downloadManager.deletePendingDownloads(*downloads.toTypedArray())
}
override fun onStatusChange(download: Download) {
super.onStatusChange(download)
view?.update(downloadManager.isRunning)
}
override fun onQueueUpdate(download: Download) {
view?.onUpdateDownloadedPages(download)
}
override fun onProgressUpdate(download: Download) {
view?.onUpdateProgress(download)
}
override fun onPageProgressUpdate(download: Download) {
super.onPageProgressUpdate(download)
view?.onUpdateDownloadedPages(download)
}
}

View file

@ -320,7 +320,6 @@ class DownloadBottomSheet @JvmOverloads constructor(
}
}
presenter.reorder(downloads)
controller?.updateChapterDownload(download, false)
}
/**

View file

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.ui.migration.BaseMigrationPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@ -41,11 +42,9 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>()
override fun onCreate() {
super.onCreate()
presenterScope.launch {
presenterScope.launchUI {
downloadManager.queueState.collect {
withUIContext {
view?.updateDownloadStatus(!downloadManager.isPaused())
}
view?.updateDownloadStatus(downloadManager.isRunning)
}
}

View file

@ -56,8 +56,6 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.retry
@ -189,11 +187,7 @@ class LibraryPresenter(
override fun onCreate() {
super.onCreate()
downloadManager.queueState.onEach {
presenterScope.launchUI {
view?.updateDownloadStatus(!downloadManager.isPaused())
}
}.launchIn(presenterScope)
if (!controllerIsSubClass) {
lastLibraryItems?.let { libraryItems = it }
lastCategories?.let { categories = it }
@ -203,6 +197,12 @@ class LibraryPresenter(
lastAllLibraryItems = null
}
presenterScope.launchUI {
downloadManager.queueState.collect {
view?.updateDownloadStatus(downloadManager.isRunning)
}
}
subscribeLibrary()
if (!preferences.showLibrarySearchSuggestions().isSet()) {

View file

@ -792,6 +792,15 @@ class MangaDetailsController :
binding.swipeRefresh.isRefreshing = enabled
}
fun updateChapterDownload(download: Download) {
getHolder(download.chapter)?.notifyStatus(
download.status,
presenter.isLockedFromSearch,
download.progress,
true,
)
}
private fun getHolder(chapter: Chapter): ChapterHolder? {
return binding.recycler.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
}

View file

@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.data.database.models.sortDescending
import eu.kanade.tachiyomi.data.database.models.updateCoverLastModified
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -62,6 +63,7 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.launchNonCancellableIO
import eu.kanade.tachiyomi.util.system.launchNow
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.withIOContext
import eu.kanade.tachiyomi.util.system.withUIContext
import eu.kanade.tachiyomi.widget.TriStateCheckBox
@ -70,8 +72,10 @@ import java.io.FileOutputStream
import java.io.OutputStream
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -106,7 +110,8 @@ class MangaDetailsPresenter(
private val downloadManager: DownloadManager = Injekt.get(),
private val chapterFilter: ChapterFilter = Injekt.get(),
private val storageManager: StorageManager = Injekt.get(),
) : BaseCoroutinePresenter<MangaDetailsController>() {
) : BaseCoroutinePresenter<MangaDetailsController>(),
DownloadQueue.Listener {
private val getAvailableScanlators: GetAvailableScanlators by injectLazy()
private val getCategories: GetCategories by injectLazy()
private val getChapter: GetChapter by injectLazy()
@ -172,6 +177,9 @@ class MangaDetailsPresenter(
var allChapterScanlators: Set<String> = emptySet()
override val progressJobs: MutableMap<Download, Job> = mutableMapOf()
override val queueListenerScope get() = presenterScope
override fun onCreate() {
val controller = view ?: return
@ -179,12 +187,15 @@ class MangaDetailsPresenter(
if (!::manga.isInitialized) runBlocking { refreshMangaFromDb() }
syncData()
downloadManager.queueState.onEach { queue ->
getChapters(queue)
withUIContext {
view?.updateChapters(chapters)
}
}.launchIn(presenterScope)
presenterScope.launchUI {
downloadManager.statusFlow().collect(::onStatusChange)
}
presenterScope.launchUI {
downloadManager.progressFlow().collect(::onProgressUpdate)
}
presenterScope.launchIO {
downloadManager.queueState.collectLatest(::onQueueUpdate)
}
runBlocking {
tracks = getTrack.awaitAllByMangaId(manga.id!!)
@ -1132,6 +1143,24 @@ class MangaDetailsPresenter(
return if (date <= 0L) null else date
}
private suspend fun onQueueUpdate(queue: List<Download>) = withIOContext {
getChapters(queue)
withUIContext {
view?.updateChapters(chapters)
}
}
override fun onQueueUpdate(download: Download) {
presenterScope.launchIO {
onQueueUpdate(downloadManager.queueState.value)
}
}
override fun onProgressUpdate(download: Download) {
chapters.find { it.id == download.chapter.id }?.download = download
view?.updateChapterDownload(download)
}
companion object {
const val MULTIPLE_VOLUMES = 1
const val TENS_OF_CHAPTERS = 2

View file

@ -629,13 +629,8 @@ class RecentsController(bundle: Bundle? = null) :
}
}
fun updateChapterDownload(download: Download, updateDLSheet: Boolean = true) {
if (view == null) return
if (updateDLSheet) {
binding.downloadBottomSheet.dlBottomSheet.update(!presenter.downloadManager.isPaused())
binding.downloadBottomSheet.dlBottomSheet.onUpdateProgress(download)
binding.downloadBottomSheet.dlBottomSheet.onUpdateDownloadedPages(download)
}
fun updateChapterDownload(download: Download) {
if (view == null || !this::adapter.isInitialized) return
val id = download.chapter.id ?: return
val item = adapter.getItemByChapterId(id) ?: return
val holder = binding.recycler.findViewHolderForItemId(item.id!!) as? RecentMangaHolder ?: return

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.HistoryImpl
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga
@ -29,6 +30,7 @@ import kotlin.math.abs
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -53,7 +55,8 @@ class RecentsPresenter(
val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(),
private val chapterFilter: ChapterFilter = Injekt.get(),
) : BaseCoroutinePresenter<RecentsController>() {
) : BaseCoroutinePresenter<RecentsController>(),
DownloadQueue.Listener {
private val handler: DatabaseHandler by injectLazy()
private val getChapter: GetChapter by injectLazy()
@ -97,9 +100,26 @@ class RecentsPresenter(
private val isOnFirstPage: Boolean
get() = pageOffset == 0
override val progressJobs = mutableMapOf<Download, Job>()
override val queueListenerScope get() = presenterScope
override fun onCreate() {
super.onCreate()
downloadManager.queueState.onEach(::updateDownloads).launchIn(presenterScope)
presenterScope.launchUI {
downloadManager.statusFlow().collect(::onStatusChange)
}
presenterScope.launchUI {
downloadManager.progressFlow().collect(::onProgressUpdate)
}
presenterScope.launchIO {
downloadManager.queueState.collectLatest {
setDownloadedChapters(recentItems, it)
withUIContext {
view?.showLists(recentItems, true)
view?.updateDownloadStatus(!downloadManager.isPaused())
}
}
}
downloadManager.isDownloaderRunning.onEach(::downloadStatusChanged).launchIn(presenterScope)
LibraryUpdateJob.updateFlow.onEach(::onUpdateManga).launchIn(presenterScope)
if (lastRecents != null) {
@ -520,16 +540,6 @@ class RecentsPresenter(
}
}
fun updateDownloads(queue: List<Download>) {
presenterScope.launch {
setDownloadedChapters(recentItems)
withContext(Dispatchers.Main) {
view?.showLists(recentItems, true)
view?.updateDownloadStatus(!downloadManager.isPaused())
}
}
}
private fun downloadStatusChanged(downloading: Boolean) {
presenterScope.launchUI {
view?.updateDownloadStatus(downloading)
@ -685,6 +695,18 @@ class RecentsPresenter(
}
}
override fun onProgressUpdate(download: Download) {
// don't do anything
}
override fun onQueueUpdate(download: Download) {
view?.updateChapterDownload(download)
}
override fun onPageProgressUpdate(download: Download) {
view?.updateChapterDownload(download)
}
enum class GroupType {
BySeries,
ByWeek,