Wait for extensions to load before the list is propagated (#3841)

This commit is contained in:
AntsyLich
2026-08-23 02:37:01 +06:00
committed by GitHub
parent 46c8f07c12
commit 270b78ba95
5 changed files with 37 additions and 33 deletions
@@ -6,7 +6,6 @@ import com.hippo.unifile.UniFile
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -69,7 +68,6 @@ class DownloadCache(
private val context: Context,
private val provider: DownloadProvider,
private val sourceManager: SourceManager,
private val extensionManager: ExtensionManager,
private val storageManager: StorageManager,
) {
@@ -356,7 +354,6 @@ class DownloadCache(
// Try to wait until extensions and sources have loaded
var sources = emptyList<Source>()
withTimeoutOrNull(30.seconds) {
extensionManager.isInitialized.first { it }
sourceManager.isInitialized.first { it }
sources = getSources()
@@ -23,9 +23,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import logcat.LogPriority
@@ -55,19 +56,18 @@ class ExtensionManager(
val scope = CoroutineScope(SupervisorJob())
private val _isInitialized = MutableStateFlow(false)
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
private val isInitialized = MutableStateFlow(false)
private val iconMap = mutableMapOf<String, Drawable>()
private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensions(scope)
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsOnceInitialized()
private val availableExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
val availableExtensionsFlow = availableExtensionMapFlow.mapExtensions(scope)
private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensions(scope)
val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensionsOnceInitialized()
init {
scope.launch(Dispatchers.IO) {
@@ -79,7 +79,7 @@ class ExtensionManager(
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages.isSet()
fun getExtensionPackage(sourceId: Long): String? {
return installedExtensionsFlow.value.find { extension ->
return installedExtensionMapFlow.value.values.find { extension ->
extension.sources.any { it.id == sourceId }
}
?.pkgName
@@ -128,7 +128,7 @@ class ExtensionManager(
.filterIsInstance<LoadResult.Untrusted>()
.associate { it.extension.pkgName to it.extension }
_isInitialized.value = true
isInitialized.value = true
}
/**
@@ -378,4 +378,12 @@ class ExtensionManager(
private fun <T : Extension> StateFlow<Map<String, T>>.mapExtensions(scope: CoroutineScope): StateFlow<List<T>> {
return map { it.values.toList() }.stateIn(scope, SharingStarted.Lazily, value.values.toList())
}
/**
* Extensions are loaded in the background, and [stateIn] would replay the empty list it was
* seeded with at construction, so this only starts emitting once that finished.
*/
private fun <T : Extension> StateFlow<Map<String, T>>.mapExtensionsOnceInitialized(): Flow<List<T>> {
return onStart { isInitialized.first { it } }.map { it.values.toList() }
}
}
@@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -48,8 +47,6 @@ class AndroidSourceManager(
init {
scope.launch {
extensionManager.isInitialized.first { it }
extensionManager.installedExtensionsFlow
.collectLatest { extensions ->
val mutableMap = ConcurrentHashMap<Long, Source>(
@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -98,7 +99,7 @@ abstract class SearchViewModel(
)
}
private fun getSelectedSources(): List<Source> {
private suspend fun getSelectedSources(): List<Source> {
val enabledSources = getEnabledSources()
val filter = extensionFilter
@@ -106,7 +107,7 @@ abstract class SearchViewModel(
return enabledSources
}
return extensionManager.installedExtensionsFlow.value
return extensionManager.installedExtensionsFlow.first()
.filter { it.pkgName == filter }
.flatMap { it.sources }
.filter { it in enabledSources }
@@ -139,23 +140,23 @@ abstract class SearchViewModel(
searchJob?.cancel()
val sources = getSelectedSources()
// Reuse previous results if possible
if (sameQuery) {
val existingResults = state.value.items
updateItems(
sources
.associateWith { existingResults[it] ?: SearchItemResult.Loading },
)
} else {
updateItems(
sources
.associateWith { SearchItemResult.Loading },
)
}
searchJob = viewModelScope.launchIO {
val sources = getSelectedSources()
// Reuse previous results if possible
if (sameQuery) {
val existingResults = state.value.items
updateItems(
sources
.associateWith { existingResults[it] ?: SearchItemResult.Loading },
)
} else {
updateItems(
sources
.associateWith { SearchItemResult.Loading },
)
}
sources.map { source ->
async {
if (state.value.items[source] !is SearchItemResult.Loading) {
@@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.first
import kotlinx.datetime.TimeZone
import kotlinx.datetime.offsetAt
import kotlinx.datetime.toLocalDateTime
@@ -62,10 +63,10 @@ class CrashLogUtil(
""".trimIndent()
}
private fun getExtensionsInfo(): String? {
private suspend fun getExtensionsInfo(): String? {
val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName }
val extensionInfoList = extensionManager.installedExtensionsFlow.value
val extensionInfoList = extensionManager.installedExtensionsFlow.first()
.sortedBy { it.name }
.mapNotNull {
val availableExtension = availableExtensions[it.pkgName]