[优化] 增加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.lifecycle.runtime.compose)
implementation(libs.coil.compose)
implementation(libs.coil.gif)
implementation(libs.coil.svg)
implementation(libs.accompanist.webview)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.lifecycle.viewmodel.compose)
@@ -1,5 +1,10 @@
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.repository.BookRepository
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.SearchContentRepository
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.explore.ExploreShowViewModel
import io.legado.app.ui.book.import.remote.RemoteBookViewModel
@@ -44,6 +53,22 @@ val appModule = module {
single<UploadRepository> { DirectLinkUploadRepository() }
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(::RssSourceViewModel)
viewModelOf(::ReadRecordViewModel)
@@ -1,6 +1,7 @@
package io.legado.app.help.coil
import android.net.Uri
import android.util.Base64
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
@@ -9,16 +10,12 @@ import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.network.HttpException
import coil.request.Options
import io.legado.app.data.entities.BaseSource
import io.legado.app.help.http.await
import io.legado.app.model.ReadManga
import io.legado.app.utils.ImageUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.Buffer
@@ -34,47 +31,65 @@ class CoverFetcher(
val source = options.tags.tag<BaseSource>()
val isManga = options.parameters.value("manga") as? Boolean == true
val request = Request.Builder()
.url(url)
.headers(options.headers)
.build()
val bytes = if (url.startsWith("data:", true)) {
val base64Data = url.substringAfter("base64,", "")
if (base64Data.isEmpty()) {
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 body = response.body
val response = withContext(Dispatchers.IO) {
callFactory.newCall(request).execute()
}
val body = response.body
if (!response.isSuccessful) {
body.close()
throw HttpException(response)
if (!response.isSuccessful) {
body.close()
throw HttpException(response)
}
val b = body.use { body ->
body.bytes()
}
b
}
if (ImageUtils.skipDecode(source, !isManga)) {
return SourceResult(
source = ImageSource(source = body.source(), context = options.context),
source = ImageSource(source = Buffer().write(bytes), context = options.context),
mimeType = null,
dataSource = DataSource.NETWORK
dataSource = if (url.startsWith(
"data:",
true
)
) DataSource.MEMORY else DataSource.NETWORK
)
}
return withContext(Dispatchers.IO) {
try {
val bytes = body.bytes()
val decodedBytes = if (isManga) {
ImageUtils.decode(url, bytes, false, source, ReadManga.book)
} else {
ImageUtils.decode(url, bytes, true, source)
} ?: throw IOException("图片解密失败")
val decodedBytes = if (isManga) {
ImageUtils.decode(url, bytes, false, source, ReadManga.book)
} else {
ImageUtils.decode(url, bytes, true, source)
} ?: throw IOException("图片解密失败")
SourceResult(
source = ImageSource(
source = Buffer().write(decodedBytes),
context = options.context
),
mimeType = null,
dataSource = DataSource.NETWORK
)
} finally {
body.close()
}
SourceResult(
source = ImageSource(
source = Buffer().write(decodedBytes),
context = options.context
),
mimeType = null,
dataSource = if (url.startsWith(
"data:",
true
)
) DataSource.MEMORY else DataSource.NETWORK
)
}
}
@@ -83,7 +98,8 @@ class CoverFetcher(
private val okHttpClientManga: OkHttpClient
) : Fetcher.Factory<Uri> {
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 client = if (isManga) okHttpClientManga else okHttpClient
@@ -91,4 +107,4 @@ class CoverFetcher(
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.help.CacheManager
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.glide.BlurTransformation
import io.legado.app.help.glide.ImageLoader
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.Companion.setCoroutineContext
import io.legado.app.model.analyzeRule.AnalyzeUrl
@@ -72,14 +68,6 @@ object BookCover {
lateinit var defaultDrawable: Drawable
private set
val coverImageLoader = coil.ImageLoader.Builder(appCtx)
.components {
add(CoverInterceptor())
add(CoverFetcher.Factory(okHttpClient, okHttpClientManga))
}
.crossfade(true)
.build()
init {
upDefaultCover()
}
@@ -171,7 +171,7 @@ fun RssSourceGridItem(
SourceIcon(
path = source.sourceIcon.ifEmpty { R.drawable.image_rss },
sourceOrigin = source.sourceUrl,
modifier = Modifier.size(40.dp)
modifier = Modifier.size(48.dp)
)
RoundDropdownMenu(
expanded = showMenu,
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.Coil.imageLoader
import coil.ImageLoader
import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.legado.app.model.BookCover.coverImageLoader
import org.koin.compose.koinInject
@Composable
fun Cover(
@@ -36,6 +35,7 @@ fun Cover(
badgeContent: (@Composable RowScope.() -> Unit)? = null,
loadOnlyWifi: Boolean = false,
sourceOrigin: String? = null,
imageLoader: ImageLoader = koinInject(),
onLoadFinish: (() -> Unit)? = null
) {
val context = LocalContext.current
@@ -51,7 +51,9 @@ fun Cover(
Icons.Default.Book,
contentDescription = null,
tint = MaterialTheme.colorScheme.surfaceContainerHighest,
modifier = Modifier.size(24.dp).align(Alignment.Center)
modifier = Modifier
.size(24.dp)
.align(Alignment.Center)
)
} else {
AsyncImage(
@@ -65,7 +67,7 @@ fun Cover(
onError = { _, _ -> onLoadFinish?.invoke() }
)
.build(),
imageLoader = coverImageLoader,
imageLoader = imageLoader,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
@@ -10,12 +10,14 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.ImageLoader
import coil.compose.AsyncImage
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),
sourceOrigin: String? = null,
loadOnlyWifi: Boolean = false,
imageLoader: ImageLoader = koinInject(),
placeholderIcon: @Composable () -> Unit = {
Icon(
Icons.Default.RssFeed,
@@ -41,7 +44,7 @@ fun SourceIcon(
val context = LocalContext.current
Box(
modifier = modifier,
modifier = modifier.clip(MaterialTheme.shapes.medium),
contentAlignment = Alignment.Center
) {
if (path == null || (path is String && path.isEmpty())) {
@@ -54,7 +57,7 @@ fun SourceIcon(
.setParameter("sourceOrigin", sourceOrigin)
.setParameter("loadOnlyWifi", loadOnlyWifi)
.build(),
imageLoader = coverImageLoader,
imageLoader = imageLoader,
contentDescription = null,
contentScale = ContentScale.Fit, // 不裁切
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" }
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-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" }
download = { id = "de.undercouch.download", version = "5.6.0" }
download = { id = "de.undercouch.download", version = "5.6.0" }