mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-20 07:32:28 -07:00
Load installed extension and source icons through Coil
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
package eu.kanade.domain.source.model
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import mihon.app.di.appGraph
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
suspend fun Source.icon(): ImageBitmap? = withContext(Dispatchers.IO) {
|
||||
Injekt.get<Context>().appGraph.extensionManager.getAppIconForSource(id)
|
||||
?.toBitmap()
|
||||
?.asImageBitmap()
|
||||
}
|
||||
@@ -2,34 +2,23 @@ package eu.kanade.presentation.browse.components
|
||||
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.imageResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil3.compose.AsyncImage
|
||||
import eu.kanade.domain.source.model.icon
|
||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.coil.InstalledExtensionIcon
|
||||
import eu.kanade.tachiyomi.data.coil.InstalledSourceIcon
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.roundedfilled.Dangerous
|
||||
import mihon.icons.materialsymbols.roundedfilled.Warning
|
||||
@@ -45,10 +34,8 @@ fun SourceIcon(
|
||||
source: Source,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val icon = produceState<ImageBitmap?>(initialValue = null, source.id) { value = source.icon() }.value
|
||||
|
||||
when {
|
||||
source.isStub && icon == null -> {
|
||||
source.isStub -> {
|
||||
Image(
|
||||
imageVector = MaterialSymbols.RoundedFilled.Warning,
|
||||
contentDescription = null,
|
||||
@@ -56,13 +43,6 @@ fun SourceIcon(
|
||||
modifier = modifier.then(defaultModifier),
|
||||
)
|
||||
}
|
||||
icon != null -> {
|
||||
Image(
|
||||
bitmap = icon,
|
||||
contentDescription = null,
|
||||
modifier = modifier.then(defaultModifier),
|
||||
)
|
||||
}
|
||||
source.isLocal() -> {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.ic_local_source),
|
||||
@@ -71,9 +51,12 @@ fun SourceIcon(
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.ic_default_source),
|
||||
val defaultIcon = painterResource(R.mipmap.ic_default_source)
|
||||
AsyncImage(
|
||||
model = InstalledSourceIcon(source.id),
|
||||
contentDescription = null,
|
||||
placeholder = defaultIcon,
|
||||
error = defaultIcon,
|
||||
modifier = modifier.then(defaultModifier),
|
||||
)
|
||||
}
|
||||
@@ -98,20 +81,14 @@ fun ExtensionIcon(
|
||||
)
|
||||
}
|
||||
is Extension.Loaded -> {
|
||||
val icon by extension.getIcon(density)
|
||||
when (icon) {
|
||||
Result.Loading -> Box(modifier = modifier)
|
||||
is Result.Success -> Image(
|
||||
bitmap = (icon as Result.Success<ImageBitmap>).value,
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
)
|
||||
Result.Error -> Image(
|
||||
bitmap = ImageBitmap.imageResource(id = R.mipmap.ic_default_source),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
val defaultIcon = painterResource(R.mipmap.ic_default_source)
|
||||
AsyncImage(
|
||||
model = InstalledExtensionIcon(extension.pkgName, extension.versionCode, density),
|
||||
contentDescription = null,
|
||||
placeholder = defaultIcon,
|
||||
error = defaultIcon,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
is Extension.NotLoaded -> Image(
|
||||
imageVector = MaterialSymbols.RoundedFilled.Dangerous,
|
||||
@@ -121,29 +98,3 @@ fun ExtensionIcon(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): State<Result<ImageBitmap>> {
|
||||
val context = LocalContext.current
|
||||
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
||||
withContext(Dispatchers.IO) {
|
||||
value = try {
|
||||
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
|
||||
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
||||
Result.Success(
|
||||
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
||||
.toBitmap()
|
||||
.asImageBitmap(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Result.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result<out T> {
|
||||
data object Loading : Result<Nothing>()
|
||||
data object Error : Result<Nothing>()
|
||||
data class Success<out T>(val value: T) : Result<T>()
|
||||
}
|
||||
|
||||
@@ -32,11 +32,15 @@ import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.CoverRequestInterceptor
|
||||
import eu.kanade.tachiyomi.data.coil.ExtensionIconFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.ImageDecoder
|
||||
import eu.kanade.tachiyomi.data.coil.InstalledExtensionIconKeyer
|
||||
import eu.kanade.tachiyomi.data.coil.InstalledSourceIconKeyer
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||
import eu.kanade.tachiyomi.data.coil.MangaKeyer
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||
@@ -97,6 +101,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
|
||||
@Inject private lateinit var sourceManager: Lazy<SourceManager>
|
||||
|
||||
@Inject private lateinit var extensionManager: Lazy<ExtensionManager>
|
||||
|
||||
@Inject private lateinit var widgetManager: WidgetManager
|
||||
|
||||
@Inject private lateinit var migrations: Set<Migration>
|
||||
@@ -226,9 +232,13 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
add(BufferedSourceFetcher.Factory())
|
||||
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy, coverCache, sourceManager))
|
||||
add(MangaCoverFetcher.MangaFactory(callFactoryLazy, coverCache, sourceManager))
|
||||
add(ExtensionIconFetcher.ExtensionFactory(context))
|
||||
add(ExtensionIconFetcher.SourceFactory(context, extensionManager))
|
||||
// Keyer
|
||||
add(MangaCoverKeyer(coverCache))
|
||||
add(MangaKeyer())
|
||||
add(InstalledExtensionIconKeyer())
|
||||
add(InstalledSourceIconKeyer())
|
||||
// Interceptor
|
||||
add(CoverRequestInterceptor())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DataSource
|
||||
import coil3.fetch.FetchResult
|
||||
import coil3.fetch.Fetcher
|
||||
import coil3.fetch.ImageFetchResult
|
||||
import coil3.key.Keyer
|
||||
import coil3.request.Options
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
|
||||
/**
|
||||
* The icon of an installed extension, drawn at [density].
|
||||
*/
|
||||
data class InstalledExtensionIcon(
|
||||
val pkgName: String,
|
||||
val versionCode: Long,
|
||||
val density: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* The icon of the installed extension that provides the source with [sourceId].
|
||||
*/
|
||||
data class InstalledSourceIcon(val sourceId: Long)
|
||||
|
||||
class ExtensionIconFetcher(
|
||||
private val context: Context,
|
||||
private val getPkgName: suspend () -> String?,
|
||||
private val density: Int?,
|
||||
) : Fetcher {
|
||||
|
||||
override suspend fun fetch(): FetchResult {
|
||||
val pkgName = getPkgName() ?: error("No installed extension")
|
||||
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)?.applicationInfo
|
||||
?: error("Extension $pkgName isn't installed")
|
||||
val drawable: Drawable = if (density == null) {
|
||||
appInfo.loadIcon(context.packageManager)
|
||||
} else {
|
||||
context.packageManager.getResourcesForApplication(appInfo)
|
||||
.getDrawableForDensity(appInfo.icon, density, null)
|
||||
?: error("Extension $pkgName has no icon")
|
||||
}
|
||||
// Adaptive icons aren't bitmaps, and only bitmaps are kept in the memory cache
|
||||
return ImageFetchResult(
|
||||
image = drawable.toBitmap().asImage(),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
|
||||
class ExtensionFactory(private val context: Context) : Fetcher.Factory<InstalledExtensionIcon> {
|
||||
override fun create(data: InstalledExtensionIcon, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return ExtensionIconFetcher(context, { data.pkgName }, data.density)
|
||||
}
|
||||
}
|
||||
|
||||
class SourceFactory(
|
||||
private val context: Context,
|
||||
private val extensionManager: Lazy<ExtensionManager>,
|
||||
) : Fetcher.Factory<InstalledSourceIcon> {
|
||||
override fun create(data: InstalledSourceIcon, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return ExtensionIconFetcher(
|
||||
context = context,
|
||||
getPkgName = { extensionManager.value.getExtensionPackage(data.sourceId) },
|
||||
density = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InstalledExtensionIconKeyer : Keyer<InstalledExtensionIcon> {
|
||||
override fun key(data: InstalledExtensionIcon, options: Options): String {
|
||||
return "extension;${data.pkgName};${data.versionCode};${data.density}"
|
||||
}
|
||||
}
|
||||
|
||||
class InstalledSourceIconKeyer : Keyer<InstalledSourceIcon> {
|
||||
override fun key(data: InstalledSourceIcon, options: Options): String {
|
||||
return "source;${data.sourceId}"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
@@ -43,7 +42,6 @@ import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.source.model.StubSource
|
||||
import tachiyomi.i18n.MR
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* The manager of extensions installed as another apk which extend the available sources. It handles
|
||||
@@ -70,8 +68,6 @@ class ExtensionManager(
|
||||
// A full load replaces both maps, so install events wait for it and apply on top instead of being overwritten
|
||||
private val loadMutex = Mutex()
|
||||
|
||||
private val iconMap = ConcurrentHashMap<String, Drawable>()
|
||||
|
||||
@Volatile
|
||||
private var stores = emptyList<ExtensionStore>()
|
||||
|
||||
@@ -147,15 +143,6 @@ class ExtensionManager(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAppIconForSource(sourceId: Long): Drawable? {
|
||||
val pkgName = getExtensionPackage(sourceId) ?: return null
|
||||
|
||||
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
|
||||
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo!!
|
||||
.loadIcon(context.packageManager)
|
||||
}
|
||||
}
|
||||
|
||||
private var availableExtensionsSourcesData: Map<Long, StubSource> = emptyMap()
|
||||
|
||||
private fun setupAvailableExtensionsSourcesDataMap(extensions: List<Extension.Available>) {
|
||||
|
||||
@@ -442,7 +442,6 @@ internal object ExtensionLoader {
|
||||
contentWarning = contentWarning,
|
||||
sources = sources,
|
||||
pkgFactory = metaData.getString(METADATA_SOURCE_FACTORY),
|
||||
icon = runCatching { appInfo.loadIcon(pkgManager) }.getOrNull(),
|
||||
isShared = extensionInfo.isShared,
|
||||
signatures = signatures,
|
||||
store = store,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.extension.model
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import mihon.domain.extension.model.ContentWarning
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
@@ -95,7 +94,6 @@ sealed interface Extension {
|
||||
override val signatures: List<String>,
|
||||
val pkgFactory: String?,
|
||||
val sources: List<Source>,
|
||||
val icon: Drawable?,
|
||||
override val hasUpdate: Boolean = false,
|
||||
val isObsolete: Boolean = false,
|
||||
override val store: ExtensionStore? = null,
|
||||
|
||||
Reference in New Issue
Block a user