Make the source manager surface suspend (#3869)

Assisted-by: Claude:claude-opus-5
This commit is contained in:
AntsyLich
2026-08-28 23:36:28 +06:00
committed by GitHub
parent c1d1f6b161
commit 5fadea61e7
50 changed files with 328 additions and 251 deletions
@@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -24,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
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
@@ -56,18 +56,18 @@ class ExtensionManager(
val scope = CoroutineScope(SupervisorJob())
private val isInitialized = MutableStateFlow(false)
private val initialized = CompletableDeferred<Unit>()
private val iconMap = mutableMapOf<String, Drawable>()
private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsOnceInitialized()
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsWhenInitialized()
private val availableExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
val availableExtensionsFlow = availableExtensionMapFlow.mapExtensions(scope)
private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensionsOnceInitialized()
val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensionsWhenInitialized()
init {
scope.launch(Dispatchers.IO) {
@@ -78,8 +78,13 @@ class ExtensionManager(
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages.isSet()
fun getExtensionPackage(sourceId: Long): String? {
return installedExtensionMapFlow.value.values.find { extension ->
suspend fun getInstalledExtensions(): List<Extension.Installed> {
initialized.await()
return installedExtensionMapFlow.value.values.toList()
}
suspend fun getExtensionPackage(sourceId: Long): String? {
return getInstalledExtensions().find { extension ->
extension.sources.any { it.id == sourceId }
}
?.pkgName
@@ -94,7 +99,7 @@ class ExtensionManager(
}
}
fun getAppIconForSource(sourceId: Long): Drawable? {
suspend fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = getExtensionPackage(sourceId) ?: return null
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
@@ -118,17 +123,23 @@ class ExtensionManager(
* Loads and registers the installed extensions.
*/
private fun initExtensions() {
val extensions = ExtensionLoader.loadExtensions(context)
try {
val extensions = ExtensionLoader.loadExtensions(context)
installedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Success>()
.associate { it.extension.pkgName to it.extension }
installedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Success>()
.associate { it.extension.pkgName to it.extension }
untrustedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Untrusted>()
.associate { it.extension.pkgName to it.extension }
untrustedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Untrusted>()
.associate { it.extension.pkgName to it.extension }
isInitialized.value = true
initialized.complete(Unit)
} catch (e: Throwable) {
// Release anything waiting on the extensions before the failure propagates
initialized.complete(Unit)
throw e
}
}
/**
@@ -380,10 +391,9 @@ class ExtensionManager(
}
/**
* 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.
* Extensions are loaded in the background, so this flow 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() }
private fun <T : Extension> StateFlow<Map<String, T>>.mapExtensionsWhenInitialized(): Flow<List<T>> {
return onStart { initialized.await() }.map { it.values.toList() }
}
}