Handle async cache in updates and manga screens
- Also fix concurrent accesses to main cache map - Also debounce sources and updates list updates to maybe avoid crashing due to dupe LazyColumn keys
This commit is contained in:
parent
d558f9e1d6
commit
152eb5b951
@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -305,9 +306,9 @@ class DownloadCache(
|
|||||||
* Returns a new map containing only the key entries of [transform] that are not null.
|
* Returns a new map containing only the key entries of [transform] that are not null.
|
||||||
*/
|
*/
|
||||||
private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): MutableMap<R, V> {
|
private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): MutableMap<R, V> {
|
||||||
val destination = LinkedHashMap<R, V>()
|
val mutableMap = ConcurrentHashMap<R, V>()
|
||||||
forEach { element -> transform(element)?.let { destination[it] = element.value } }
|
forEach { element -> transform(element)?.let { mutableMap[it] = element.value } }
|
||||||
return destination
|
return mutableMap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,13 +78,13 @@ class DownloadQueue(
|
|||||||
.startWith(getActiveDownloads())
|
.startWith(getActiveDownloads())
|
||||||
.onBackpressureBuffer()
|
.onBackpressureBuffer()
|
||||||
|
|
||||||
fun getStatusAsFlow(): Flow<Download> = getStatusObservable().asFlow()
|
fun statusFlow(): Flow<Download> = getStatusObservable().asFlow()
|
||||||
|
|
||||||
private fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer()
|
private fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer()
|
||||||
.startWith(Unit)
|
.startWith(Unit)
|
||||||
.map { this }
|
.map { this }
|
||||||
|
|
||||||
fun getUpdatedAsFlow(): Flow<List<Download>> = getUpdatedObservable().asFlow()
|
fun updatedFlow(): Flow<List<Download>> = getUpdatedObservable().asFlow()
|
||||||
|
|
||||||
private fun setPagesFor(download: Download) {
|
private fun setPagesFor(download: Download) {
|
||||||
if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
||||||
@ -111,7 +111,7 @@ class DownloadQueue(
|
|||||||
.filter { it.status == Download.State.DOWNLOADING }
|
.filter { it.status == Download.State.DOWNLOADING }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getProgressAsFlow(): Flow<Download> = getProgressObservable().asFlow()
|
fun progressFlow(): Flow<Download> = getProgressObservable().asFlow()
|
||||||
|
|
||||||
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
||||||
pages?.forEach { it.setStatusSubject(subject) }
|
pages?.forEach { it.setStatusSubject(subject) }
|
||||||
|
@ -16,6 +16,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -38,6 +39,7 @@ class SourcesPresenter(
|
|||||||
fun onCreate() {
|
fun onCreate() {
|
||||||
presenterScope.launchIO {
|
presenterScope.launchIO {
|
||||||
getEnabledSources.subscribe()
|
getEnabledSources.subscribe()
|
||||||
|
.debounce(500) // Avoid crashes due to LazyColumn rendering
|
||||||
.catch { exception ->
|
.catch { exception ->
|
||||||
logcat(LogPriority.ERROR, exception)
|
logcat(LogPriority.ERROR, exception)
|
||||||
_events.send(Event.FailedFetchingSources)
|
_events.send(Event.FailedFetchingSources)
|
||||||
|
@ -34,7 +34,7 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
|
|||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
presenterScope.launch {
|
presenterScope.launch {
|
||||||
downloadQueue.getUpdatedAsFlow()
|
downloadQueue.updatedFlow()
|
||||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||||
.map { downloads ->
|
.map { downloads ->
|
||||||
downloads
|
downloads
|
||||||
@ -49,9 +49,9 @@ class DownloadPresenter : BasePresenter<DownloadController>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDownloadStatusFlow() = downloadQueue.getStatusAsFlow()
|
fun getDownloadStatusFlow() = downloadQueue.statusFlow()
|
||||||
|
|
||||||
fun getDownloadProgressFlow() = downloadQueue.getProgressAsFlow()
|
fun getDownloadProgressFlow() = downloadQueue.progressFlow()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pauses the download queue.
|
* Pauses the download queue.
|
||||||
|
@ -34,6 +34,7 @@ import eu.kanade.domain.track.model.toDomainTrack
|
|||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
@ -63,6 +64,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -91,6 +93,7 @@ class MangaPresenter(
|
|||||||
private val trackManager: TrackManager = Injekt.get(),
|
private val trackManager: TrackManager = Injekt.get(),
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
|
private val downloadCache: DownloadCache = Injekt.get(),
|
||||||
private val getMangaAndChapters: GetMangaWithChapters = Injekt.get(),
|
private val getMangaAndChapters: GetMangaWithChapters = Injekt.get(),
|
||||||
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
|
private val getDuplicateLibraryManga: GetDuplicateLibraryManga = Injekt.get(),
|
||||||
private val setMangaChapterFlags: SetMangaChapterFlags = Injekt.get(),
|
private val setMangaChapterFlags: SetMangaChapterFlags = Injekt.get(),
|
||||||
@ -113,9 +116,6 @@ class MangaPresenter(
|
|||||||
private val successState: MangaScreenState.Success?
|
private val successState: MangaScreenState.Success?
|
||||||
get() = state.value as? MangaScreenState.Success
|
get() = state.value as? MangaScreenState.Success
|
||||||
|
|
||||||
private var observeDownloadsStatusJob: Job? = null
|
|
||||||
private var observeDownloadsPageJob: Job? = null
|
|
||||||
|
|
||||||
private var _trackList: List<TrackItem> = emptyList()
|
private var _trackList: List<TrackItem> = emptyList()
|
||||||
val trackList get() = _trackList
|
val trackList get() = _trackList
|
||||||
|
|
||||||
@ -169,10 +169,11 @@ class MangaPresenter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For UI changes
|
presenterScope.launchIO {
|
||||||
presenterScope.launch {
|
combine(
|
||||||
getMangaAndChapters.subscribe(mangaId)
|
getMangaAndChapters.subscribe(mangaId).distinctUntilChanged(),
|
||||||
.distinctUntilChanged()
|
downloadCache.changes,
|
||||||
|
) { mangaAndChapters, _ -> mangaAndChapters }
|
||||||
.collectLatest { (manga, chapters) ->
|
.collectLatest { (manga, chapters) ->
|
||||||
val chapterItems = chapters.toChapterItemsParams(manga)
|
val chapterItems = chapters.toChapterItemsParams(manga)
|
||||||
updateSuccessState {
|
updateSuccessState {
|
||||||
@ -181,20 +182,11 @@ class MangaPresenter(
|
|||||||
chapters = chapterItems,
|
chapters = chapterItems,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
observeDownloads()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
basePreferences.incognitoMode()
|
observeDownloads()
|
||||||
.asHotFlow { incognitoMode = it }
|
|
||||||
.launchIn(presenterScope)
|
|
||||||
|
|
||||||
basePreferences.downloadedOnly()
|
|
||||||
.asHotFlow { downloadedOnlyMode = it }
|
|
||||||
.launchIn(presenterScope)
|
|
||||||
|
|
||||||
// This block runs once on create
|
|
||||||
presenterScope.launchIO {
|
presenterScope.launchIO {
|
||||||
val manga = getMangaAndChapters.awaitManga(mangaId)
|
val manga = getMangaAndChapters.awaitManga(mangaId)
|
||||||
val chapters = getMangaAndChapters.awaitChapters(mangaId)
|
val chapters = getMangaAndChapters.awaitChapters(mangaId)
|
||||||
@ -207,7 +199,7 @@ class MangaPresenter(
|
|||||||
val needRefreshInfo = !manga.initialized
|
val needRefreshInfo = !manga.initialized
|
||||||
val needRefreshChapter = chapters.isEmpty()
|
val needRefreshChapter = chapters.isEmpty()
|
||||||
|
|
||||||
// Show what we have earlier.
|
// Show what we have earlier
|
||||||
_state.update {
|
_state.update {
|
||||||
MangaScreenState.Success(
|
MangaScreenState.Success(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
@ -238,6 +230,14 @@ class MangaPresenter(
|
|||||||
// Initial loading finished
|
// Initial loading finished
|
||||||
updateSuccessState { it.copy(isRefreshingData = false) }
|
updateSuccessState { it.copy(isRefreshingData = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
basePreferences.incognitoMode()
|
||||||
|
.asHotFlow { incognitoMode = it }
|
||||||
|
.launchIn(presenterScope)
|
||||||
|
|
||||||
|
basePreferences.downloadedOnly()
|
||||||
|
.asHotFlow { downloadedOnlyMode = it }
|
||||||
|
.launchIn(presenterScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchAllFromSource(manualFetch: Boolean = true) {
|
fun fetchAllFromSource(manualFetch: Boolean = true) {
|
||||||
@ -467,9 +467,8 @@ class MangaPresenter(
|
|||||||
// Chapters list - start
|
// Chapters list - start
|
||||||
|
|
||||||
private fun observeDownloads() {
|
private fun observeDownloads() {
|
||||||
observeDownloadsStatusJob?.cancel()
|
presenterScope.launchIO {
|
||||||
observeDownloadsStatusJob = presenterScope.launchIO {
|
downloadManager.queue.statusFlow()
|
||||||
downloadManager.queue.getStatusAsFlow()
|
|
||||||
.filter { it.manga.id == successState?.manga?.id }
|
.filter { it.manga.id == successState?.manga?.id }
|
||||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||||
.collect {
|
.collect {
|
||||||
@ -479,9 +478,8 @@ class MangaPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
observeDownloadsPageJob?.cancel()
|
presenterScope.launchIO {
|
||||||
observeDownloadsPageJob = presenterScope.launchIO {
|
downloadManager.queue.progressFlow()
|
||||||
downloadManager.queue.getProgressAsFlow()
|
|
||||||
.filter { it.manga.id == successState?.manga?.id }
|
.filter { it.manga.id == successState?.manga?.id }
|
||||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||||
.collect {
|
.collect {
|
||||||
|
@ -32,7 +32,7 @@ class MorePresenter(
|
|||||||
presenterScope.launchIO {
|
presenterScope.launchIO {
|
||||||
combine(
|
combine(
|
||||||
DownloadService.isRunning,
|
DownloadService.isRunning,
|
||||||
downloadManager.queue.getUpdatedAsFlow(),
|
downloadManager.queue.updatedFlow(),
|
||||||
) { isRunning, downloadQueue -> Pair(isRunning, downloadQueue.size) }
|
) { isRunning, downloadQueue -> Pair(isRunning, downloadQueue.size) }
|
||||||
.collectLatest { (isDownloading, downloadQueueSize) ->
|
.collectLatest { (isDownloading, downloadQueueSize) ->
|
||||||
val pendingDownloadExists = downloadQueueSize != 0
|
val pendingDownloadExists = downloadQueueSize != 0
|
||||||
|
@ -18,6 +18,7 @@ import eu.kanade.domain.updates.model.UpdatesWithRelations
|
|||||||
import eu.kanade.presentation.components.ChapterDownloadAction
|
import eu.kanade.presentation.components.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.updates.UpdatesState
|
import eu.kanade.presentation.updates.UpdatesState
|
||||||
import eu.kanade.presentation.updates.UpdatesStateImpl
|
import eu.kanade.presentation.updates.UpdatesStateImpl
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
@ -27,11 +28,12 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
|||||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -50,6 +52,7 @@ class UpdatesPresenter(
|
|||||||
private val getManga: GetManga = Injekt.get(),
|
private val getManga: GetManga = Injekt.get(),
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
|
private val downloadCache: DownloadCache = Injekt.get(),
|
||||||
private val getChapter: GetChapter = Injekt.get(),
|
private val getChapter: GetChapter = Injekt.get(),
|
||||||
basePreferences: BasePreferences = Injekt.get(),
|
basePreferences: BasePreferences = Injekt.get(),
|
||||||
uiPreferences: UiPreferences = Injekt.get(),
|
uiPreferences: UiPreferences = Injekt.get(),
|
||||||
@ -70,12 +73,6 @@ class UpdatesPresenter(
|
|||||||
// First and last selected index in list
|
// First and last selected index in list
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to observe download status changes.
|
|
||||||
*/
|
|
||||||
private var observeDownloadsStatusJob: Job? = null
|
|
||||||
private var observeDownloadsPageJob: Job? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
@ -86,10 +83,11 @@ class UpdatesPresenter(
|
|||||||
add(Calendar.MONTH, -3)
|
add(Calendar.MONTH, -3)
|
||||||
}
|
}
|
||||||
|
|
||||||
observeDownloads()
|
combine(
|
||||||
|
getUpdates.subscribe(calendar).distinctUntilChanged(),
|
||||||
getUpdates.subscribe(calendar)
|
downloadCache.changes,
|
||||||
.distinctUntilChanged()
|
) { updates, _ -> updates }
|
||||||
|
.debounce(500) // Avoid crashes due to LazyColumn rendering
|
||||||
.catch {
|
.catch {
|
||||||
logcat(LogPriority.ERROR, it)
|
logcat(LogPriority.ERROR, it)
|
||||||
_events.send(Event.InternalError)
|
_events.send(Event.InternalError)
|
||||||
@ -99,6 +97,26 @@ class UpdatesPresenter(
|
|||||||
state.isLoading = false
|
state.isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
presenterScope.launchIO {
|
||||||
|
downloadManager.queue.statusFlow()
|
||||||
|
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||||
|
.collect {
|
||||||
|
withUIContext {
|
||||||
|
updateDownloadState(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
presenterScope.launchIO {
|
||||||
|
downloadManager.queue.progressFlow()
|
||||||
|
.catch { error -> logcat(LogPriority.ERROR, error) }
|
||||||
|
.collect {
|
||||||
|
withUIContext {
|
||||||
|
updateDownloadState(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<UpdatesWithRelations>.toUpdateItems(): List<UpdatesItem> {
|
private fun List<UpdatesWithRelations>.toUpdateItems(): List<UpdatesItem> {
|
||||||
@ -125,30 +143,6 @@ class UpdatesPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun observeDownloads() {
|
|
||||||
observeDownloadsStatusJob?.cancel()
|
|
||||||
observeDownloadsStatusJob = presenterScope.launchIO {
|
|
||||||
downloadManager.queue.getStatusAsFlow()
|
|
||||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
|
||||||
.collect {
|
|
||||||
withUIContext {
|
|
||||||
updateDownloadState(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
observeDownloadsPageJob?.cancel()
|
|
||||||
observeDownloadsPageJob = presenterScope.launchIO {
|
|
||||||
downloadManager.queue.getProgressAsFlow()
|
|
||||||
.catch { error -> logcat(LogPriority.ERROR, error) }
|
|
||||||
.collect {
|
|
||||||
withUIContext {
|
|
||||||
updateDownloadState(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update status of chapters.
|
* Update status of chapters.
|
||||||
*
|
*
|
||||||
|
Loading…
Reference in New Issue
Block a user