mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Clean up extension update checking
ExtensionApi only wrapped the store repository and the launch check, which matched updates again on its own. The manager now fetches listings and checks for updates itself with the statuses it already keeps, so the check covers not-loaded extensions and keeps the Extensions tab current from launch. Statuses are derived in one place, and the extension list is split before it's sorted instead of sorted twice.
This commit is contained in:
@@ -23,19 +23,12 @@ class GetExtensionsByType(
|
||||
extensionManager.notLoadedExtensionsFlow,
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { enabledLanguages, _loaded, _notLoaded, _available ->
|
||||
val (loadedUpdates, loaded) = _loaded
|
||||
.sortedWith(
|
||||
compareBy<Extension.Loaded> { !it.isObsolete }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||
)
|
||||
.partition { it.hasUpdate }
|
||||
val byName = compareBy<Extension, String>(String.CASE_INSENSITIVE_ORDER) { it.name }
|
||||
|
||||
val (notLoadedUpdates, notLoaded) = _notLoaded
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
.partition { it.hasUpdate }
|
||||
val (loadedUpdates, loaded) = _loaded.partition { it.hasUpdate }
|
||||
val (notLoadedUpdates, notLoaded) = _notLoaded.partition { it.hasUpdate }
|
||||
|
||||
val updates = (loadedUpdates + notLoadedUpdates)
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
val updates = (loadedUpdates + notLoadedUpdates).sortedWith(byName)
|
||||
|
||||
val available = _available
|
||||
.filter { extension ->
|
||||
@@ -54,9 +47,14 @@ class GetExtensionsByType(
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
.sortedWith(byName)
|
||||
|
||||
Extensions(updates, loaded, available, notLoaded)
|
||||
Extensions(
|
||||
updates = updates,
|
||||
loaded = loaded.sortedWith(compareBy<Extension.Loaded> { !it.isObsolete }.then(byName)),
|
||||
available = available,
|
||||
notLoaded = notLoaded.sortedWith(byName),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
@@ -33,6 +32,9 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.lang.withUIContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.source.model.StubSource
|
||||
@@ -52,7 +54,8 @@ class ExtensionManager(
|
||||
private val context: Context,
|
||||
private val preferences: SourcePreferences,
|
||||
private val trustExtension: TrustExtension,
|
||||
private val api: ExtensionApi,
|
||||
private val extensionStoreRepository: ExtensionStoreRepository,
|
||||
private val updateExtensionStores: UpdateExtensionStores,
|
||||
private val installer: ExtensionInstaller,
|
||||
private val extensionUpdateNotifier: ExtensionUpdateNotifier,
|
||||
) {
|
||||
@@ -156,7 +159,7 @@ class ExtensionManager(
|
||||
.associateBy { it.pkgName }
|
||||
|
||||
// Newly loaded extensions have no status derived from the store index yet
|
||||
updatedInstalledExtensionsStatuses(availableExtensionListFlow.value)
|
||||
refreshStatuses()
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to load extensions" }
|
||||
} finally {
|
||||
@@ -166,21 +169,53 @@ class ExtensionManager(
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the available extensions in the [api] and updates [availableExtensionListFlow].
|
||||
* Finds the available extensions in the stores and updates [availableExtensionListFlow].
|
||||
*/
|
||||
suspend fun findAvailableExtensions() {
|
||||
val extensions: List<Extension.Available> = try {
|
||||
api.findExtensions()
|
||||
fetchExtensions()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
withUIContext { context.toast(MR.strings.extension_api_error) }
|
||||
return
|
||||
}
|
||||
|
||||
setAvailableExtensions(extensions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the stores and their listings like [findAvailableExtensions], then notifies about the
|
||||
* installed extensions that have an update. Stays silent when the stores can't be reached.
|
||||
*/
|
||||
suspend fun checkForUpdates() {
|
||||
val extensions = try {
|
||||
updateExtensionStores()
|
||||
fetchExtensions()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
return
|
||||
}
|
||||
|
||||
initialized.await()
|
||||
setAvailableExtensions(extensions)
|
||||
|
||||
val names = (loadedExtensionMapFlow.value.values + notLoadedExtensionMapFlow.value.values)
|
||||
.filter { it.hasUpdate }
|
||||
.map { it.name }
|
||||
if (names.isNotEmpty()) {
|
||||
extensionUpdateNotifier.promptUpdates(names)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchExtensions(): List<Extension.Available> {
|
||||
return withIOContext { extensionStoreRepository.fetchExtensions() }
|
||||
}
|
||||
|
||||
private fun setAvailableExtensions(extensions: List<Extension.Available>) {
|
||||
enableAdditionalSubLanguages(extensions)
|
||||
|
||||
availableExtensionListFlow.value = extensions
|
||||
updatedInstalledExtensionsStatuses(extensions)
|
||||
refreshStatuses()
|
||||
setupAvailableExtensionsSourcesDataMap(extensions)
|
||||
}
|
||||
|
||||
@@ -214,26 +249,6 @@ class ExtensionManager(
|
||||
subLanguagesEnabledOnFirstRun = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the update fields of the installed extensions, loaded or not, with the given [availableExtensions].
|
||||
*
|
||||
* @param availableExtensions The list of extensions given by the [api].
|
||||
*/
|
||||
private fun updatedInstalledExtensionsStatuses(availableExtensions: List<Extension.Available>) {
|
||||
if (availableExtensions.isEmpty()) {
|
||||
preferences.extensionUpdatesCount.set(0)
|
||||
return
|
||||
}
|
||||
|
||||
loadedExtensionMapFlow.value = loadedExtensionMapFlow.value.mapValues { (_, extension) ->
|
||||
extension.withStatus(availableExtensions)
|
||||
}
|
||||
notLoadedExtensionMapFlow.value = notLoadedExtensionMapFlow.value.mapValues { (_, extension) ->
|
||||
extension.withStatus(availableExtensions)
|
||||
}
|
||||
updatePendingUpdatesCount()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flow of the installation process for the given extension. It will complete
|
||||
* once the extension is installed or throws an error. The process will be canceled if
|
||||
@@ -324,15 +339,15 @@ class ExtensionManager(
|
||||
private inner class InstallationListener : ExtensionInstallReceiver.Listener {
|
||||
|
||||
override fun onExtensionLoaded(extension: Extension.Loaded) {
|
||||
registerExtension(extension.withStatus(availableExtensionListFlow.value))
|
||||
registerExtension(extension)
|
||||
notLoadedExtensionMapFlow.value -= extension.pkgName
|
||||
updatePendingUpdatesCount()
|
||||
refreshStatuses()
|
||||
}
|
||||
|
||||
override fun onExtensionNotLoaded(extension: Extension.NotLoaded) {
|
||||
loadedExtensionMapFlow.value -= extension.pkgName
|
||||
notLoadedExtensionMapFlow.value += extension.withStatus(availableExtensionListFlow.value)
|
||||
updatePendingUpdatesCount()
|
||||
notLoadedExtensionMapFlow.value += extension
|
||||
refreshStatuses()
|
||||
}
|
||||
|
||||
override fun onPackageUninstalled(pkgName: String) {
|
||||
@@ -343,29 +358,29 @@ class ExtensionManager(
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives what the store listings say about an installed extension. Without any listing there's
|
||||
* nothing to derive from, so it's left as is rather than being marked obsolete.
|
||||
* Derives what the store listings say about every installed extension, loaded or not. Without any
|
||||
* listing there's nothing to derive from, so they're left as they are rather than marked obsolete.
|
||||
*/
|
||||
private fun Extension.Loaded.withStatus(availableExtensions: List<Extension.Available>): Extension.Loaded {
|
||||
if (availableExtensions.isEmpty()) return this
|
||||
val listing = findListing(availableExtensions)
|
||||
return copy(
|
||||
hasUpdate = findUpdate(availableExtensions) != null,
|
||||
isObsolete = listing == null,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An extension that isn't loaded can still be updated, which is often what gets it loaded again.
|
||||
*/
|
||||
private fun Extension.NotLoaded.withStatus(availableExtensions: List<Extension.Available>): Extension.NotLoaded {
|
||||
if (availableExtensions.isEmpty()) return this
|
||||
return copy(hasUpdate = findUpdate(availableExtensions) != null)
|
||||
private fun refreshStatuses() {
|
||||
val available = availableExtensionListFlow.value
|
||||
if (available.isNotEmpty()) {
|
||||
loadedExtensionMapFlow.value = loadedExtensionMapFlow.value.mapValues { (_, extension) ->
|
||||
val listing = extension.findListing(available)
|
||||
extension.copy(
|
||||
hasUpdate = extension.findUpdate(available) != null,
|
||||
isObsolete = listing == null,
|
||||
)
|
||||
}
|
||||
notLoadedExtensionMapFlow.value = notLoadedExtensionMapFlow.value.mapValues { (_, extension) ->
|
||||
extension.copy(hasUpdate = extension.findUpdate(available) != null)
|
||||
}
|
||||
}
|
||||
updatePendingUpdatesCount()
|
||||
}
|
||||
|
||||
private fun updatePendingUpdatesCount() {
|
||||
val pendingUpdateCount = loadedExtensionMapFlow.value.values.count { it.hasUpdate } +
|
||||
notLoadedExtensionMapFlow.value.values.count { it.hasUpdate }
|
||||
val pendingUpdateCount = (loadedExtensionMapFlow.value.values + notLoadedExtensionMapFlow.value.values)
|
||||
.count { it.hasUpdate }
|
||||
preferences.extensionUpdatesCount.set(pendingUpdateCount)
|
||||
if (pendingUpdateCount == 0) {
|
||||
extensionUpdateNotifier.dismiss()
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class ExtensionApi(
|
||||
private val repository: ExtensionStoreRepository,
|
||||
private val updateExtensionStores: UpdateExtensionStores,
|
||||
private val extensionUpdateNotifier: ExtensionUpdateNotifier,
|
||||
) {
|
||||
|
||||
suspend fun findExtensions(): List<Extension.Available> {
|
||||
return withIOContext { repository.fetchExtensions() }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param installedExtensions Extensions already read by [eu.kanade.tachiyomi.extension.ExtensionManager],
|
||||
* loaded or not. Only their versions and signatures are read, so there's nothing to gain from reading them
|
||||
* a second time.
|
||||
*/
|
||||
suspend fun checkForUpdates(installedExtensions: List<Extension.Installed>) {
|
||||
updateExtensionStores()
|
||||
|
||||
val extensions = findExtensions()
|
||||
|
||||
val extensionsWithUpdate = installedExtensions.filter { it.findUpdate(extensions) != null }
|
||||
|
||||
if (extensionsWithUpdate.isNotEmpty()) {
|
||||
extensionUpdateNotifier.promptUpdates(extensionsWithUpdate.map { it.name })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,6 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
||||
@@ -141,8 +140,6 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
@Inject private lateinit var getIncognitoState: GetIncognitoState
|
||||
|
||||
@Inject private lateinit var extensionApi: ExtensionApi
|
||||
|
||||
@Inject private lateinit var extensionManager: ExtensionManager
|
||||
|
||||
// To be checked by splash screen. If true then splash screen will be removed.
|
||||
@@ -342,9 +339,7 @@ class MainActivity : BaseActivity() {
|
||||
// Extensions updates
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
extensionApi.checkForUpdates(
|
||||
extensionManager.getLoadedExtensions() + extensionManager.getNotLoadedExtensions(),
|
||||
)
|
||||
extensionManager.checkForUpdates()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user