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.AppScope
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -69,7 +68,6 @@ class DownloadCache(
private val context: Context, private val context: Context,
private val provider: DownloadProvider, private val provider: DownloadProvider,
private val sourceManager: SourceManager, private val sourceManager: SourceManager,
private val extensionManager: ExtensionManager,
private val storageManager: StorageManager, private val storageManager: StorageManager,
) { ) {
@@ -356,7 +354,6 @@ class DownloadCache(
// Try to wait until extensions and sources have loaded // Try to wait until extensions and sources have loaded
var sources = emptyList<Source>() var sources = emptyList<Source>()
withTimeoutOrNull(30.seconds) { withTimeoutOrNull(30.seconds) {
extensionManager.isInitialized.first { it }
sourceManager.isInitialized.first { it } sourceManager.isInitialized.first { it }
sources = getSources() sources = getSources()
@@ -23,9 +23,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
@@ -55,19 +56,18 @@ class ExtensionManager(
val scope = CoroutineScope(SupervisorJob()) val scope = CoroutineScope(SupervisorJob())
private val _isInitialized = MutableStateFlow(false) private val isInitialized = MutableStateFlow(false)
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
private val iconMap = mutableMapOf<String, Drawable>() private val iconMap = mutableMapOf<String, Drawable>()
private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>()) 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>()) private val availableExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
val availableExtensionsFlow = availableExtensionMapFlow.mapExtensions(scope) val availableExtensionsFlow = availableExtensionMapFlow.mapExtensions(scope)
private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>()) private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensions(scope) val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensionsOnceInitialized()
init { init {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
@@ -79,7 +79,7 @@ class ExtensionManager(
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages.isSet() private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages.isSet()
fun getExtensionPackage(sourceId: Long): String? { fun getExtensionPackage(sourceId: Long): String? {
return installedExtensionsFlow.value.find { extension -> return installedExtensionMapFlow.value.values.find { extension ->
extension.sources.any { it.id == sourceId } extension.sources.any { it.id == sourceId }
} }
?.pkgName ?.pkgName
@@ -128,7 +128,7 @@ class ExtensionManager(
.filterIsInstance<LoadResult.Untrusted>() .filterIsInstance<LoadResult.Untrusted>()
.associate { it.extension.pkgName to it.extension } .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>> { 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()) 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.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -48,8 +47,6 @@ class AndroidSourceManager(
init { init {
scope.launch { scope.launch {
extensionManager.isInitialized.first { it }
extensionManager.installedExtensionsFlow extensionManager.installedExtensionsFlow
.collectLatest { extensions -> .collectLatest { extensions ->
val mutableMap = ConcurrentHashMap<Long, Source>( val mutableMap = ConcurrentHashMap<Long, Source>(
@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch 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 enabledSources = getEnabledSources()
val filter = extensionFilter val filter = extensionFilter
@@ -106,7 +107,7 @@ abstract class SearchViewModel(
return enabledSources return enabledSources
} }
return extensionManager.installedExtensionsFlow.value return extensionManager.installedExtensionsFlow.first()
.filter { it.pkgName == filter } .filter { it.pkgName == filter }
.flatMap { it.sources } .flatMap { it.sources }
.filter { it in enabledSources } .filter { it in enabledSources }
@@ -139,23 +140,23 @@ abstract class SearchViewModel(
searchJob?.cancel() 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 { 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 -> sources.map { source ->
async { async {
if (state.value.items[source] !is SearchItemResult.Loading) { 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.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.first
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.offsetAt import kotlinx.datetime.offsetAt
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
@@ -62,10 +63,10 @@ class CrashLogUtil(
""".trimIndent() """.trimIndent()
} }
private fun getExtensionsInfo(): String? { private suspend fun getExtensionsInfo(): String? {
val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName } val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName }
val extensionInfoList = extensionManager.installedExtensionsFlow.value val extensionInfoList = extensionManager.installedExtensionsFlow.first()
.sortedBy { it.name } .sortedBy { it.name }
.mapNotNull { .mapNotNull {
val availableExtension = availableExtensions[it.pkgName] val availableExtension = availableExtensions[it.pkgName]