diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bea2b2978..e0b899da0 100644 --- a/app/build.gradle.kts +++ b/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) diff --git a/app/src/main/java/io/legado/app/di/appModule.kt b/app/src/main/java/io/legado/app/di/appModule.kt index 1b8e41d9a..709d7dd61 100644 --- a/app/src/main/java/io/legado/app/di/appModule.kt +++ b/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 { DirectLinkUploadRepository() } single { ExploreRepositoryImpl(get()) } + single { + 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) diff --git a/app/src/main/java/io/legado/app/help/coil/CoverFetcher.kt b/app/src/main/java/io/legado/app/help/coil/CoverFetcher.kt index dfa749ea7..bf187c3f9 100644 --- a/app/src/main/java/io/legado/app/help/coil/CoverFetcher.kt +++ b/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() 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 { 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) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/model/BookCover.kt b/app/src/main/java/io/legado/app/model/BookCover.kt index fa8b8d8fc..bd7b35e1b 100644 --- a/app/src/main/java/io/legado/app/model/BookCover.kt +++ b/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() } diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt index e2016b93c..72bb0a764 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt +++ b/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, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/Cover.kt b/app/src/main/java/io/legado/app/ui/widget/components/Cover.kt index cee8887c0..ac512309a 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/Cover.kt +++ b/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() diff --git a/app/src/main/java/io/legado/app/ui/widget/components/SourceIcon.kt b/app/src/main/java/io/legado/app/ui/widget/components/SourceIcon.kt index 9e45bfc28..d5d99dffa 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/SourceIcon.kt +++ b/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() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 900d47da5..6ba5b5cb5 100644 --- a/gradle/libs.versions.toml +++ b/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" } \ No newline at end of file +download = { id = "de.undercouch.download", version = "5.6.0" }