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