[优化] 增加Coil图片组件的SVG、GIF等格式的支持 #698
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user