mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-19 23:22:26 -07:00
Offer updates for extensions that aren't loaded
Extensions that aren't loaded now get updates like loaded ones, which is often what gets them loaded again. They're counted, notified and listed with the other pending updates. Every installed extension, loaded or not, records the added store whose key signed it when it loads.
This commit is contained in:
@@ -23,14 +23,18 @@ class GetExtensionsByType(
|
||||
extensionManager.notLoadedExtensionsFlow,
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { enabledLanguages, _loaded, _notLoaded, _available ->
|
||||
val (updates, loaded) = _loaded
|
||||
val (loadedUpdates, loaded) = _loaded
|
||||
.sortedWith(
|
||||
compareBy<Extension.Loaded> { !it.isObsolete }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||
)
|
||||
.partition { it.hasUpdate }
|
||||
|
||||
val notLoaded = _notLoaded
|
||||
val (notLoadedUpdates, notLoaded) = _notLoaded
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
.partition { it.hasUpdate }
|
||||
|
||||
val updates = (loadedUpdates + notLoadedUpdates)
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
|
||||
val available = _available
|
||||
|
||||
@@ -3,7 +3,7 @@ package eu.kanade.domain.extension.model
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
|
||||
data class Extensions(
|
||||
val updates: List<Extension.Loaded>,
|
||||
val updates: List<Extension.Installed>,
|
||||
val loaded: List<Extension.Loaded>,
|
||||
val available: List<Extension.Available>,
|
||||
val notLoaded: List<Extension.NotLoaded>,
|
||||
|
||||
@@ -83,7 +83,7 @@ fun ExtensionScreen(
|
||||
onOpenWebView: (Extension.Available) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension.Installed) -> Unit,
|
||||
onUpdateExtension: (Extension.Loaded) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
||||
onOpenExtension: (Extension.Loaded) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
@@ -144,7 +144,7 @@ private fun ExtensionContent(
|
||||
onOpenWebView: (Extension.Available) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension.Installed) -> Unit,
|
||||
onUpdateExtension: (Extension.Loaded) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
||||
onOpenExtension: (Extension.Loaded) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
@@ -232,7 +232,9 @@ private fun ExtensionContent(
|
||||
when (it) {
|
||||
is Extension.Available -> onOpenWebView(it)
|
||||
is Extension.Loaded -> onOpenExtension(it)
|
||||
else -> {}
|
||||
is Extension.NotLoaded -> {
|
||||
notLoadedState = it
|
||||
}
|
||||
}
|
||||
},
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
@@ -247,7 +249,11 @@ private fun ExtensionContent(
|
||||
}
|
||||
}
|
||||
is Extension.NotLoaded -> {
|
||||
notLoadedState = it
|
||||
if (it.hasUpdate) {
|
||||
onUpdateExtension(it)
|
||||
} else {
|
||||
notLoadedState = it
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -479,7 +485,7 @@ private fun ExtensionItemActions(
|
||||
}
|
||||
is Extension.NotLoaded -> {
|
||||
val isUntrusted = extension.reason is Extension.NotLoaded.Reason.Untrusted
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = if (isUntrusted) {
|
||||
MaterialSymbols.Rounded.VerifiedUser
|
||||
@@ -493,6 +499,15 @@ private fun ExtensionItemActions(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (extension.hasUpdate) {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = MaterialSymbols.Rounded.Download,
|
||||
contentDescription = stringResource(MR.strings.ext_update),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Extension.Available -> {
|
||||
if (extension.sources.isNotEmpty()) {
|
||||
|
||||
@@ -215,7 +215,7 @@ class ExtensionManager(
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the update field of the installed extensions with the given [availableExtensions].
|
||||
* 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].
|
||||
*/
|
||||
@@ -228,6 +228,9 @@ class ExtensionManager(
|
||||
loadedExtensionMapFlow.value = loadedExtensionMapFlow.value.mapValues { (_, extension) ->
|
||||
extension.withStatus(availableExtensions)
|
||||
}
|
||||
notLoadedExtensionMapFlow.value = notLoadedExtensionMapFlow.value.mapValues { (_, extension) ->
|
||||
extension.withStatus(availableExtensions)
|
||||
}
|
||||
updatePendingUpdatesCount()
|
||||
}
|
||||
|
||||
@@ -249,7 +252,7 @@ class ExtensionManager(
|
||||
*
|
||||
* @param extension The extension to be updated.
|
||||
*/
|
||||
fun updateExtension(extension: Extension.Loaded): Flow<InstallStep> {
|
||||
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
|
||||
val update = extension.findUpdate(availableExtensionListFlow.value) ?: return emptyFlow()
|
||||
val isUpdateForPrivatelyInstalled = !extension.isShared
|
||||
return installer.downloadAndInstall(update, isUpdateForPrivatelyInstalled)
|
||||
@@ -328,7 +331,7 @@ class ExtensionManager(
|
||||
|
||||
override fun onExtensionNotLoaded(extension: Extension.NotLoaded) {
|
||||
loadedExtensionMapFlow.value -= extension.pkgName
|
||||
notLoadedExtensionMapFlow.value += extension
|
||||
notLoadedExtensionMapFlow.value += extension.withStatus(availableExtensionListFlow.value)
|
||||
updatePendingUpdatesCount()
|
||||
}
|
||||
|
||||
@@ -349,12 +352,20 @@ class ExtensionManager(
|
||||
return copy(
|
||||
hasUpdate = findUpdate(availableExtensions) != null,
|
||||
isObsolete = listing == null,
|
||||
store = listing?.store ?: store,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 updatePendingUpdatesCount() {
|
||||
val pendingUpdateCount = loadedExtensionMapFlow.value.values.count { it.hasUpdate }
|
||||
val pendingUpdateCount = loadedExtensionMapFlow.value.values.count { it.hasUpdate } +
|
||||
notLoadedExtensionMapFlow.value.values.count { it.hasUpdate }
|
||||
preferences.extensionUpdatesCount.set(pendingUpdateCount)
|
||||
if (pendingUpdateCount == 0) {
|
||||
extensionUpdateNotifier.dismiss()
|
||||
|
||||
@@ -21,15 +21,16 @@ class ExtensionApi(
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loadedExtensions Extensions already loaded by [eu.kanade.tachiyomi.extension.ExtensionManager].
|
||||
* Only their versions are read, so there's nothing to gain from loading them a second time.
|
||||
* @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(loadedExtensions: List<Extension.Loaded>) {
|
||||
suspend fun checkForUpdates(installedExtensions: List<Extension.Installed>) {
|
||||
updateExtensionStores()
|
||||
|
||||
val extensions = findExtensions()
|
||||
|
||||
val extensionsWithUpdate = loadedExtensions.filter { it.findUpdate(extensions) != null }
|
||||
val extensionsWithUpdate = installedExtensions.filter { it.findUpdate(extensions) != null }
|
||||
|
||||
if (extensionsWithUpdate.isNotEmpty()) {
|
||||
extensionUpdateNotifier.promptUpdates(extensionsWithUpdate.map { it.name })
|
||||
|
||||
@@ -18,6 +18,7 @@ import logcat.LogPriority
|
||||
import mihon.app.di.appGraph
|
||||
import mihon.data.dalvik.DelegateLastClassLoaderCompat
|
||||
import mihon.domain.extension.model.ContentWarning
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import java.io.File
|
||||
@@ -122,6 +123,7 @@ internal object ExtensionLoader {
|
||||
): List<Extension.Installed> {
|
||||
val trustExtension = context.appGraph.trustExtension
|
||||
val sourcePreferences = context.appGraph.sourcePreferences
|
||||
val stores = context.appGraph.extensionStoreRepository.getAll()
|
||||
val enabledContentWarnings = sourcePreferences.enabledContentWarnings.get()
|
||||
val applyContentWarningsToInstalled = sourcePreferences.applyContentWarningsToInstalled.get()
|
||||
|
||||
@@ -178,6 +180,7 @@ internal object ExtensionLoader {
|
||||
context = context,
|
||||
extensionInfo = it,
|
||||
trustExtension = trustExtension,
|
||||
stores = stores,
|
||||
enabledContentWarnings = enabledContentWarnings,
|
||||
applyContentWarningsToInstalled = applyContentWarningsToInstalled,
|
||||
alreadyLoaded = alreadyLoaded[it.packageInfo.packageName],
|
||||
@@ -204,6 +207,7 @@ internal object ExtensionLoader {
|
||||
context = context,
|
||||
extensionInfo = extensionPackage,
|
||||
trustExtension = context.appGraph.trustExtension,
|
||||
stores = context.appGraph.extensionStoreRepository.getAll(),
|
||||
enabledContentWarnings = sourcePreferences.enabledContentWarnings.get(),
|
||||
applyContentWarningsToInstalled = sourcePreferences.applyContentWarningsToInstalled.get(),
|
||||
)
|
||||
@@ -254,6 +258,7 @@ internal object ExtensionLoader {
|
||||
context: Context,
|
||||
extensionInfo: ExtensionInfo,
|
||||
trustExtension: TrustExtension,
|
||||
stores: List<ExtensionStore>,
|
||||
enabledContentWarnings: Set<ContentWarning>,
|
||||
applyContentWarningsToInstalled: Boolean,
|
||||
alreadyLoaded: Extension.Loaded? = null,
|
||||
@@ -263,6 +268,7 @@ internal object ExtensionLoader {
|
||||
context = context,
|
||||
extensionInfo = extensionInfo,
|
||||
trustExtension = trustExtension,
|
||||
stores = stores,
|
||||
enabledContentWarnings = enabledContentWarnings,
|
||||
applyContentWarningsToInstalled = applyContentWarningsToInstalled,
|
||||
alreadyLoaded = alreadyLoaded,
|
||||
@@ -270,6 +276,7 @@ internal object ExtensionLoader {
|
||||
} catch (e: Throwable) {
|
||||
val pkgInfo = extensionInfo.packageInfo
|
||||
logcat(LogPriority.ERROR, e) { "Extension load error: ${pkgInfo.packageName}" }
|
||||
val signatures = getSignatures(pkgInfo).orEmpty()
|
||||
Extension.NotLoaded(
|
||||
name = pkgInfo.packageName,
|
||||
pkgName = pkgInfo.packageName,
|
||||
@@ -277,7 +284,8 @@ internal object ExtensionLoader {
|
||||
versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo),
|
||||
isShared = extensionInfo.isShared,
|
||||
contentWarning = ContentWarning.SAFE,
|
||||
signatures = getSignatures(pkgInfo).orEmpty(),
|
||||
signatures = signatures,
|
||||
store = stores.firstOrNull { it.signingKey in signatures },
|
||||
reason = Extension.NotLoaded.Reason.Failed(e.rootMessage, e.stackTraceToString()),
|
||||
)
|
||||
}
|
||||
@@ -293,6 +301,7 @@ internal object ExtensionLoader {
|
||||
context: Context,
|
||||
extensionInfo: ExtensionInfo,
|
||||
trustExtension: TrustExtension,
|
||||
stores: List<ExtensionStore>,
|
||||
enabledContentWarnings: Set<ContentWarning>,
|
||||
applyContentWarningsToInstalled: Boolean,
|
||||
alreadyLoaded: Extension.Loaded? = null,
|
||||
@@ -309,6 +318,7 @@ internal object ExtensionLoader {
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
val signatures = getSignatures(pkgInfo).orEmpty()
|
||||
val store = stores.firstOrNull { it.signingKey in signatures }
|
||||
val contentWarning = when {
|
||||
metaData == null -> ContentWarning.SAFE
|
||||
metaData.containsKey(METADATA_CONTENT_WARNING) -> {
|
||||
@@ -333,6 +343,7 @@ internal object ExtensionLoader {
|
||||
isShared = extensionInfo.isShared,
|
||||
contentWarning = contentWarning,
|
||||
signatures = signatures,
|
||||
store = store,
|
||||
libVersion = libVersion,
|
||||
reason = reason,
|
||||
)
|
||||
@@ -379,7 +390,7 @@ internal object ExtensionLoader {
|
||||
alreadyLoaded.versionCode == versionCode &&
|
||||
alreadyLoaded.isShared == extensionInfo.isShared
|
||||
) {
|
||||
return alreadyLoaded
|
||||
return alreadyLoaded.copy(store = store)
|
||||
}
|
||||
|
||||
val classLoader = try {
|
||||
@@ -441,6 +452,7 @@ internal object ExtensionLoader {
|
||||
icon = runCatching { appInfo.loadIcon(pkgManager) }.getOrNull(),
|
||||
isShared = extensionInfo.isShared,
|
||||
signatures = signatures,
|
||||
store = store,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ class ExtensionsViewModel(
|
||||
viewModelScope.launchIO {
|
||||
state.value.items.values.flatten()
|
||||
.map { it.extension }
|
||||
.filterIsInstance<Extension.Loaded>()
|
||||
.filterIsInstance<Extension.Installed>()
|
||||
.filter { it.hasUpdate }
|
||||
.forEach(::updateExtension)
|
||||
}
|
||||
@@ -180,7 +180,7 @@ class ExtensionsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateExtension(extension: Extension.Loaded) {
|
||||
fun updateExtension(extension: Extension.Installed) {
|
||||
viewModelScope.launchIO {
|
||||
extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
|
||||
}
|
||||
|
||||
@@ -342,7 +342,9 @@ class MainActivity : BaseActivity() {
|
||||
// Extensions updates
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
extensionApi.checkForUpdates(extensionManager.getLoadedExtensions())
|
||||
extensionApi.checkForUpdates(
|
||||
extensionManager.getLoadedExtensions() + extensionManager.getNotLoadedExtensions(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user