Przeglądaj źródła

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

HapeLee 8 miesięcy temu
rodzic
commit
901ff29efd

+ 2 - 0
app/build.gradle.kts

@@ -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)

+ 25 - 0
app/src/main/java/io/legado/app/di/appModule.kt

@@ -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)

+ 51 - 35
app/src/main/java/io/legado/app/help/coil/CoverFetcher.kt

@@ -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 = withContext(Dispatchers.IO) {
+                callFactory.newCall(request).execute()
+            }
+            val body = response.body
 
-        val response = 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)
         }
     }
-}
+}

+ 0 - 12
app/src/main/java/io/legado/app/model/BookCover.kt

@@ -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()
     }

+ 1 - 1
app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt

@@ -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 - 5
app/src/main/java/io/legado/app/ui/widget/components/Cover.kt

@@ -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()

+ 6 - 3
app/src/main/java/io/legado/app/ui/widget/components/SourceIcon.kt

@@ -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
gradle/libs.versions.toml

@@ -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" }