diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt index cfda4f249..ca474d020 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt @@ -23,19 +23,12 @@ class GetExtensionsByType( extensionManager.notLoadedExtensionsFlow, extensionManager.availableExtensionsFlow, ) { enabledLanguages, _loaded, _notLoaded, _available -> - val (loadedUpdates, loaded) = _loaded - .sortedWith( - compareBy { !it.isObsolete } - .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, - ) - .partition { it.hasUpdate } + val byName = compareBy(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 { !it.isObsolete }.then(byName)), + available = available, + notLoaded = notLoaded.sortedWith(byName), + ) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index acdaa1489..80757bded 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -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 = 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 { + return withIOContext { extensionStoreRepository.fetchExtensions() } + } + + private fun setAvailableExtensions(extensions: List) { 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) { - 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.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.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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt deleted file mode 100644 index 26239de75..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt +++ /dev/null @@ -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 { - 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) { - updateExtensionStores() - - val extensions = findExtensions() - - val extensionsWithUpdate = installedExtensions.filter { it.findUpdate(extensions) != null } - - if (extensionsWithUpdate.isNotEmpty()) { - extensionUpdateNotifier.promptUpdates(extensionsWithUpdate.map { it.name }) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 7e179c905..b995fa45a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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) }