[优化] 增加Coil图片组件的SVG、GIF等格式的支持 #698

This commit is contained in:
HapeLee
2026-03-10 19:11:54 +08:00
parent f363424769
commit 901ff29efd
8 changed files with 95 additions and 57 deletions
+2
View File
@@ -251,6 +251,8 @@ dependencies {
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.gif)
implementation(libs.coil.svg)
implementation(libs.accompanist.webview) implementation(libs.accompanist.webview)
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
@@ -1,5 +1,10 @@
package io.legado.app.di package io.legado.app.di
import android.os.Build
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import io.legado.app.data.AppDatabase import io.legado.app.data.AppDatabase
import io.legado.app.data.repository.BookRepository import io.legado.app.data.repository.BookRepository
import io.legado.app.data.repository.DirectLinkUploadRepository import io.legado.app.data.repository.DirectLinkUploadRepository
@@ -9,6 +14,10 @@ import io.legado.app.data.repository.ReadRecordRepository
import io.legado.app.data.repository.RemoteBookRepository import io.legado.app.data.repository.RemoteBookRepository
import io.legado.app.data.repository.SearchContentRepository import io.legado.app.data.repository.SearchContentRepository
import io.legado.app.data.repository.UploadRepository import io.legado.app.data.repository.UploadRepository
import io.legado.app.help.coil.CoverFetcher
import io.legado.app.help.coil.CoverInterceptor
import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.okHttpClientManga
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
import io.legado.app.ui.book.explore.ExploreShowViewModel import io.legado.app.ui.book.explore.ExploreShowViewModel
import io.legado.app.ui.book.import.remote.RemoteBookViewModel import io.legado.app.ui.book.import.remote.RemoteBookViewModel
@@ -44,6 +53,22 @@ val appModule = module {
single<UploadRepository> { DirectLinkUploadRepository() } single<UploadRepository> { DirectLinkUploadRepository() }
single<ExploreRepository> { ExploreRepositoryImpl(get()) } single<ExploreRepository> { ExploreRepositoryImpl(get()) }
single<ImageLoader> {
ImageLoader.Builder(get())
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
add(SvgDecoder.Factory())
add(CoverInterceptor())
add(CoverFetcher.Factory(okHttpClient, okHttpClientManga))
}
.crossfade(true)
.build()
}
viewModelOf(::DictRuleViewModel) viewModelOf(::DictRuleViewModel)
viewModelOf(::RssSourceViewModel) viewModelOf(::RssSourceViewModel)
viewModelOf(::ReadRecordViewModel) viewModelOf(::ReadRecordViewModel)
@@ -1,6 +1,7 @@
package io.legado.app.help.coil package io.legado.app.help.coil
import android.net.Uri import android.net.Uri
import android.util.Base64
import coil.ImageLoader import coil.ImageLoader
import coil.decode.DataSource import coil.decode.DataSource
import coil.decode.ImageSource import coil.decode.ImageSource
@@ -9,16 +10,12 @@ import coil.fetch.Fetcher
import coil.fetch.SourceResult import coil.fetch.SourceResult
import coil.network.HttpException import coil.network.HttpException
import coil.request.Options import coil.request.Options
import io.legado.app.data.entities.BaseSource import io.legado.app.data.entities.BaseSource
import io.legado.app.help.http.await
import io.legado.app.model.ReadManga import io.legado.app.model.ReadManga
import io.legado.app.utils.ImageUtils import io.legado.app.utils.ImageUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Call import okhttp3.Call
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okio.Buffer import okio.Buffer
@@ -34,47 +31,65 @@ class CoverFetcher(
val source = options.tags.tag<BaseSource>() val source = options.tags.tag<BaseSource>()
val isManga = options.parameters.value("manga") as? Boolean == true val isManga = options.parameters.value("manga") as? Boolean == true
val request = Request.Builder() val bytes = if (url.startsWith("data:", true)) {
.url(url) val base64Data = url.substringAfter("base64,", "")
.headers(options.headers) if (base64Data.isEmpty()) {
.build() throw IOException("Invalid data URI")
}
Base64.decode(base64Data, Base64.DEFAULT)
} else {
val request = Request.Builder()
.url(url)
.headers(options.headers)
.build()
val response = callFactory.newCall(request).execute() val response = withContext(Dispatchers.IO) {
val body = response.body callFactory.newCall(request).execute()
}
val body = response.body
if (!response.isSuccessful) { if (!response.isSuccessful) {
body.close() body.close()
throw HttpException(response) throw HttpException(response)
}
val b = body.use { body ->
body.bytes()
}
b
} }
if (ImageUtils.skipDecode(source, !isManga)) { if (ImageUtils.skipDecode(source, !isManga)) {
return SourceResult( return SourceResult(
source = ImageSource(source = body.source(), context = options.context), source = ImageSource(source = Buffer().write(bytes), context = options.context),
mimeType = null, mimeType = null,
dataSource = DataSource.NETWORK dataSource = if (url.startsWith(
"data:",
true
)
) DataSource.MEMORY else DataSource.NETWORK
) )
} }
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { val decodedBytes = if (isManga) {
val bytes = body.bytes() ImageUtils.decode(url, bytes, false, source, ReadManga.book)
val decodedBytes = if (isManga) { } else {
ImageUtils.decode(url, bytes, false, source, ReadManga.book) ImageUtils.decode(url, bytes, true, source)
} else { } ?: throw IOException("图片解密失败")
ImageUtils.decode(url, bytes, true, source)
} ?: throw IOException("图片解密失败")
SourceResult( SourceResult(
source = ImageSource( source = ImageSource(
source = Buffer().write(decodedBytes), source = Buffer().write(decodedBytes),
context = options.context context = options.context
), ),
mimeType = null, mimeType = null,
dataSource = DataSource.NETWORK dataSource = if (url.startsWith(
) "data:",
} finally { true
body.close() )
} ) DataSource.MEMORY else DataSource.NETWORK
)
} }
} }
@@ -83,7 +98,8 @@ class CoverFetcher(
private val okHttpClientManga: OkHttpClient private val okHttpClientManga: OkHttpClient
) : Fetcher.Factory<Uri> { ) : Fetcher.Factory<Uri> {
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? { override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
if (data.scheme != "http" && data.scheme != "https") return null val scheme = data.scheme
if (scheme != "http" && scheme != "https" && scheme != "data") return null
val isManga = options.parameters.value("manga") as? Boolean == true val isManga = options.parameters.value("manga") as? Boolean == true
val client = if (isManga) okHttpClientManga else okHttpClient val client = if (isManga) okHttpClientManga else okHttpClient
@@ -91,4 +107,4 @@ class CoverFetcher(
return CoverFetcher(data.toString(), options, client) return CoverFetcher(data.toString(), options, client)
} }
} }
} }
@@ -25,14 +25,10 @@ import io.legado.app.data.entities.BaseSource
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.CacheManager import io.legado.app.help.CacheManager
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.coil.CoverFetcher
import io.legado.app.help.coil.CoverInterceptor
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.glide.BlurTransformation import io.legado.app.help.glide.BlurTransformation
import io.legado.app.help.glide.ImageLoader import io.legado.app.help.glide.ImageLoader
import io.legado.app.help.glide.OkHttpModelLoader import io.legado.app.help.glide.OkHttpModelLoader
import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.okHttpClientManga
import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
@@ -72,14 +68,6 @@ object BookCover {
lateinit var defaultDrawable: Drawable lateinit var defaultDrawable: Drawable
private set private set
val coverImageLoader = coil.ImageLoader.Builder(appCtx)
.components {
add(CoverInterceptor())
add(CoverFetcher.Factory(okHttpClient, okHttpClientManga))
}
.crossfade(true)
.build()
init { init {
upDefaultCover() upDefaultCover()
} }
@@ -171,7 +171,7 @@ fun RssSourceGridItem(
SourceIcon( SourceIcon(
path = source.sourceIcon.ifEmpty { R.drawable.image_rss }, path = source.sourceIcon.ifEmpty { R.drawable.image_rss },
sourceOrigin = source.sourceUrl, sourceOrigin = source.sourceUrl,
modifier = Modifier.size(40.dp) modifier = Modifier.size(48.dp)
) )
RoundDropdownMenu( RoundDropdownMenu(
expanded = showMenu, expanded = showMenu,
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -24,10 +23,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.Coil.imageLoader import coil.ImageLoader
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import io.legado.app.model.BookCover.coverImageLoader import org.koin.compose.koinInject
@Composable @Composable
fun Cover( fun Cover(
@@ -36,6 +35,7 @@ fun Cover(
badgeContent: (@Composable RowScope.() -> Unit)? = null, badgeContent: (@Composable RowScope.() -> Unit)? = null,
loadOnlyWifi: Boolean = false, loadOnlyWifi: Boolean = false,
sourceOrigin: String? = null, sourceOrigin: String? = null,
imageLoader: ImageLoader = koinInject(),
onLoadFinish: (() -> Unit)? = null onLoadFinish: (() -> Unit)? = null
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -51,7 +51,9 @@ fun Cover(
Icons.Default.Book, Icons.Default.Book,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.surfaceContainerHighest, tint = MaterialTheme.colorScheme.surfaceContainerHighest,
modifier = Modifier.size(24.dp).align(Alignment.Center) modifier = Modifier
.size(24.dp)
.align(Alignment.Center)
) )
} else { } else {
AsyncImage( AsyncImage(
@@ -65,7 +67,7 @@ fun Cover(
onError = { _, _ -> onLoadFinish?.invoke() } onError = { _, _ -> onLoadFinish?.invoke() }
) )
.build(), .build(),
imageLoader = coverImageLoader, imageLoader = imageLoader,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -10,12 +10,14 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.ImageLoader
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import io.legado.app.model.BookCover.coverImageLoader import org.koin.compose.koinInject
/** /**
* 专门用于显示源图标的组件 * 专门用于显示源图标的组件
@@ -29,6 +31,7 @@ fun SourceIcon(
modifier: Modifier = Modifier.size(32.dp), modifier: Modifier = Modifier.size(32.dp),
sourceOrigin: String? = null, sourceOrigin: String? = null,
loadOnlyWifi: Boolean = false, loadOnlyWifi: Boolean = false,
imageLoader: ImageLoader = koinInject(),
placeholderIcon: @Composable () -> Unit = { placeholderIcon: @Composable () -> Unit = {
Icon( Icon(
Icons.Default.RssFeed, Icons.Default.RssFeed,
@@ -41,7 +44,7 @@ fun SourceIcon(
val context = LocalContext.current val context = LocalContext.current
Box( Box(
modifier = modifier, modifier = modifier.clip(MaterialTheme.shapes.medium),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (path == null || (path is String && path.isEmpty())) { if (path == null || (path is String && path.isEmpty())) {
@@ -54,7 +57,7 @@ fun SourceIcon(
.setParameter("sourceOrigin", sourceOrigin) .setParameter("sourceOrigin", sourceOrigin)
.setParameter("loadOnlyWifi", loadOnlyWifi) .setParameter("loadOnlyWifi", loadOnlyWifi)
.build(), .build(),
imageLoader = coverImageLoader, imageLoader = imageLoader,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Fit, // 不裁切 contentScale = ContentScale.Fit, // 不裁切
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
+3 -1
View File
@@ -130,6 +130,8 @@ koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose" }
koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmodel" } koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmodel" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coilCompose" }
coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coilCompose" }
core-core = { module = "androidx.core:core", version.ref = "core" } core-core = { module = "androidx.core:core", version.ref = "core" }
core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" }
@@ -264,4 +266,4 @@ room = { id = "androidx.room", version.ref = "room" }
#protobuf = { id = "com.google.protobuf", version = "0.9.4" } #protobuf = { id = "com.google.protobuf", version = "0.9.4" }
download = { id = "de.undercouch.download", version = "5.6.0" } download = { id = "de.undercouch.download", version = "5.6.0" }