[优化] 优化书架的性能与稳定性
This commit is contained in:
@@ -115,15 +115,18 @@ object BookCover {
|
||||
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
|
||||
}
|
||||
|
||||
fun getRandomDefaultDrawable(seed: Any? = null): Drawable {
|
||||
fun getRandomDefaultPath(seed: Any? = null): String? {
|
||||
val isNightTheme = AppConfig.isNightTheme
|
||||
val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
|
||||
val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
|
||||
if (paths.isNullOrEmpty()) {
|
||||
return appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
|
||||
}
|
||||
if (paths.isNullOrEmpty()) return null
|
||||
val random = if (seed != null) Random(seed.hashCode()) else Random
|
||||
val randomPath = paths[random.nextInt(paths.size)]
|
||||
return paths[random.nextInt(paths.size)]
|
||||
}
|
||||
|
||||
fun getRandomDefaultDrawable(seed: Any? = null): Drawable {
|
||||
val randomPath = getRandomDefaultPath(seed)
|
||||
?: return appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
|
||||
return kotlin.runCatching {
|
||||
BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources)
|
||||
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.legado.app.ui.main.bookshelf
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -48,6 +49,7 @@ fun BookshelfConfigSheet(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.animateContentSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package io.legado.app.ui.main.bookshelf
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -12,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -32,6 +35,8 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
@@ -49,6 +54,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -56,6 +63,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseRuleEvent
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.ui.about.AppLogSheet
|
||||
import io.legado.app.ui.book.cache.CacheActivity
|
||||
@@ -64,7 +72,6 @@ import io.legado.app.ui.book.import.remote.RemoteBookActivity
|
||||
import io.legado.app.ui.book.manage.BookshelfManageActivity
|
||||
import io.legado.app.ui.book.search.SearchActivity
|
||||
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||
import io.legado.app.ui.file.HandleFileContract
|
||||
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||
@@ -75,6 +82,7 @@ import io.legado.app.utils.readText
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@@ -94,38 +102,79 @@ fun BookshelfScreen(
|
||||
|
||||
var showAddUrlDialog by remember { mutableStateOf(false) }
|
||||
var showImportSheet by remember { mutableStateOf(false) }
|
||||
var showExportSheet by remember { mutableStateOf(false) }
|
||||
var showConfigSheet by remember { mutableStateOf(false) }
|
||||
var showGroupManageSheet by remember { mutableStateOf(false) }
|
||||
var showLogSheet by remember { mutableStateOf(false) }
|
||||
|
||||
val importLauncher = rememberLauncherForActivityResult(HandleFileContract()) {
|
||||
runCatching {
|
||||
it.uri?.readText(context)?.let { text ->
|
||||
val groupId = uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
|
||||
viewModel.importBookshelf(text, groupId)
|
||||
val clipboardManager = LocalClipboard.current
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.events.collect { event ->
|
||||
when (event) {
|
||||
is BaseRuleEvent.ShowSnackbar -> {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = event.message,
|
||||
actionLabel = event.actionLabel,
|
||||
withDismissAction = true
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed && event.url != null) {
|
||||
clipboardManager.setClipEntry(
|
||||
ClipEntry(
|
||||
ClipData.newPlainText(
|
||||
"url",
|
||||
event.url
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
context.toastOnUi(it.localizedMessage ?: "ERROR")
|
||||
}
|
||||
}
|
||||
|
||||
val importLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
onResult = { uri ->
|
||||
uri?.let {
|
||||
runCatching {
|
||||
val text = it.readText(context)
|
||||
val groupId =
|
||||
uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
|
||||
viewModel.importBookshelf(text, groupId)
|
||||
}.onFailure {
|
||||
context.toastOnUi(it.localizedMessage ?: "ERROR")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val exportLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("application/json"),
|
||||
onResult = { uri ->
|
||||
uri?.let { viewModel.exportToUri(it, uiState.items) }
|
||||
}
|
||||
)
|
||||
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = uiState.selectedGroupIndex,
|
||||
pageCount = { uiState.groups.size }
|
||||
)
|
||||
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.currentPage }.collect { page ->
|
||||
if (uiState.groups.isNotEmpty() && page < uiState.groups.size) {
|
||||
viewModel.changeGroup(uiState.groups[page].groupId)
|
||||
snapshotFlow { pagerState.currentPage }
|
||||
.distinctUntilChanged()
|
||||
.collect { page ->
|
||||
if (uiState.groups.isNotEmpty() && page in uiState.groups.indices) {
|
||||
val targetGroupId = uiState.groups[page].groupId
|
||||
val currentGroupId =
|
||||
uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId
|
||||
if (currentGroupId != targetGroupId) {
|
||||
viewModel.changeGroup(targetGroupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.selectedGroupIndex) {
|
||||
if (uiState.selectedGroupIndex != pagerState.currentPage && uiState.selectedGroupIndex < pagerState.pageCount) {
|
||||
pagerState.scrollToPage(uiState.selectedGroupIndex)
|
||||
}
|
||||
}
|
||||
|
||||
val bookGroupStyle = BookshelfConfig.bookGroupStyle
|
||||
@@ -223,6 +272,7 @@ fun BookshelfScreen(
|
||||
RoundDropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.export_bookshelf)) },
|
||||
onClick = {
|
||||
showExportSheet = true
|
||||
dismiss()
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Default.ImportExport, null) }
|
||||
@@ -241,12 +291,12 @@ fun BookshelfScreen(
|
||||
leadingIcon = { Icon(Icons.Default.History, null) }
|
||||
)
|
||||
},
|
||||
snackbarHostState = snackbarHostState,
|
||||
bottomContent = if (bookGroupStyle == 0) {
|
||||
{
|
||||
if (uiState.groups.isNotEmpty()) {
|
||||
val selectedTabIndex = remember(pagerState.currentPage, uiState.groups.size) {
|
||||
val selectedTabIndex =
|
||||
pagerState.currentPage.coerceIn(0, uiState.groups.size - 1)
|
||||
}
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
edgePadding = 0.dp,
|
||||
@@ -312,7 +362,7 @@ fun BookshelfScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
|
||||
showFastScroll = BookshelfConfig.showBookshelfFastScroller
|
||||
) {
|
||||
items(uiState.groups) { group ->
|
||||
itemsIndexed(uiState.groups, key = { _, it -> it.groupId }) { index, group ->
|
||||
if (bookshelfLayoutMode == 0) {
|
||||
BookGroupItemList(
|
||||
group = group,
|
||||
@@ -322,11 +372,8 @@ fun BookshelfScreen(
|
||||
titleCenter = BookshelfConfig.bookshelfTitleCenter,
|
||||
titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
|
||||
onClick = {
|
||||
val index = uiState.groups.indexOf(group)
|
||||
if (index != -1) {
|
||||
scope.launch { pagerState.scrollToPage(index) }
|
||||
isInFolderRoot = false
|
||||
}
|
||||
scope.launch { pagerState.scrollToPage(index) }
|
||||
isInFolderRoot = false
|
||||
},
|
||||
onLongClick = { showGroupManageSheet = true }
|
||||
)
|
||||
@@ -340,11 +387,8 @@ fun BookshelfScreen(
|
||||
titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
|
||||
coverShadow = BookshelfConfig.bookshelfCoverShadow,
|
||||
onClick = {
|
||||
val index = uiState.groups.indexOf(group)
|
||||
if (index != -1) {
|
||||
scope.launch { pagerState.scrollToPage(index) }
|
||||
isInFolderRoot = false
|
||||
}
|
||||
scope.launch { pagerState.scrollToPage(index) }
|
||||
isInFolderRoot = false
|
||||
},
|
||||
onLongClick = { showGroupManageSheet = true }
|
||||
)
|
||||
@@ -360,8 +404,10 @@ fun BookshelfScreen(
|
||||
) { pageIndex ->
|
||||
val group = uiState.groups.getOrNull(pageIndex)
|
||||
if (group != null) {
|
||||
val books by viewModel.getBooksFlow(group.groupId)
|
||||
.collectAsState(emptyList())
|
||||
val booksFlow = remember(group.groupId) {
|
||||
viewModel.getBooksFlow(group.groupId)
|
||||
}
|
||||
val books by booksFlow.collectAsState(emptyList())
|
||||
BookshelfPage(
|
||||
paddingValues = paddingValues,
|
||||
books = books,
|
||||
@@ -409,15 +455,28 @@ fun BookshelfScreen(
|
||||
onDismissRequest = { showImportSheet = false },
|
||||
title = stringResource(R.string.import_bookshelf),
|
||||
onSelectSysFile = { types ->
|
||||
importLauncher.launch {
|
||||
mode = HandleFileContract.FILE
|
||||
allowExtensions = types
|
||||
}
|
||||
importLauncher.launch(types)
|
||||
showImportSheet = false
|
||||
},
|
||||
onManualInput = {
|
||||
showAddUrlDialog = true
|
||||
showImportSheet = false
|
||||
},
|
||||
allowExtensions = arrayOf("json", "txt")
|
||||
)
|
||||
}
|
||||
|
||||
if (showExportSheet) {
|
||||
FilePickerSheet(
|
||||
onDismissRequest = { showExportSheet = false },
|
||||
title = stringResource(R.string.export_bookshelf),
|
||||
onSelectSysDir = {
|
||||
showExportSheet = false
|
||||
exportLauncher.launch("bookshelf.json")
|
||||
},
|
||||
onUpload = {
|
||||
showExportSheet = false
|
||||
viewModel.uploadBookshelf(uiState.items)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package io.legado.app.ui.main.bookshelf
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseRuleEvent
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.AppLog
|
||||
@@ -16,6 +18,7 @@ import io.legado.app.data.entities.BookGroup
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.data.repository.BookGroupRepository
|
||||
import io.legado.app.data.repository.UploadRepository
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.book.addType
|
||||
@@ -51,6 +54,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
@@ -69,11 +73,13 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
@@ -85,7 +91,8 @@ import kotlin.math.min
|
||||
|
||||
class BookshelfViewModel(
|
||||
application: Application,
|
||||
private val bookGroupRepository: BookGroupRepository
|
||||
private val bookGroupRepository: BookGroupRepository,
|
||||
private val uploadRepository: UploadRepository
|
||||
) : BaseViewModel(application) {
|
||||
var addBookJob: Coroutine<*>? = null
|
||||
|
||||
@@ -107,6 +114,9 @@ class BookshelfViewModel(
|
||||
|
||||
val scrollTrigger = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
|
||||
protected val _eventChannel = Channel<BaseRuleEvent>()
|
||||
val events = _eventChannel.receiveAsFlow()
|
||||
|
||||
val groupsFlow: StateFlow<List<BookGroup>> = bookGroupRepository.flowShow()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
@@ -512,6 +522,63 @@ class BookshelfViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun exportToUri(uri: Uri, books: List<Book>) {
|
||||
execute {
|
||||
context.contentResolver.openOutputStream(uri)?.use { out ->
|
||||
val writer = JsonWriter(OutputStreamWriter(out, "UTF-8"))
|
||||
writer.setIndent(" ")
|
||||
writer.beginArray()
|
||||
books.forEach {
|
||||
val bookMap = hashMapOf<String, String?>()
|
||||
bookMap["name"] = it.name
|
||||
bookMap["author"] = it.author
|
||||
bookMap["intro"] = it.getDisplayIntro()
|
||||
GSON.toJson(bookMap, bookMap::class.java, writer)
|
||||
}
|
||||
writer.endArray()
|
||||
writer.close()
|
||||
}
|
||||
}.onSuccess {
|
||||
_eventChannel.trySend(BaseRuleEvent.ShowSnackbar("导出成功"))
|
||||
}.onError {
|
||||
_eventChannel.trySend(BaseRuleEvent.ShowSnackbar("导出失败\n${it.localizedMessage}"))
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadBookshelf(books: List<Book>) {
|
||||
execute {
|
||||
val json = withContext(Dispatchers.Default) {
|
||||
val list = books.map {
|
||||
val bookMap = hashMapOf<String, String?>()
|
||||
bookMap["name"] = it.name
|
||||
bookMap["author"] = it.author
|
||||
bookMap["intro"] = it.getDisplayIntro()
|
||||
bookMap
|
||||
}
|
||||
GSON.toJson(list)
|
||||
}
|
||||
uploadRepository.upload(
|
||||
fileName = "bookshelf.json",
|
||||
file = json,
|
||||
contentType = "application/json"
|
||||
)
|
||||
}.onSuccess { url ->
|
||||
_eventChannel.trySend(
|
||||
BaseRuleEvent.ShowSnackbar(
|
||||
message = "上传成功: $url",
|
||||
actionLabel = "复制链接",
|
||||
url = url
|
||||
)
|
||||
)
|
||||
}.onError {
|
||||
_eventChannel.trySend(
|
||||
BaseRuleEvent.ShowSnackbar(
|
||||
message = "上传失败: ${it.localizedMessage}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun exportBookshelf(books: List<Book>?, success: (file: File) -> Unit) {
|
||||
execute {
|
||||
books?.let {
|
||||
|
||||
@@ -13,10 +13,13 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Book
|
||||
import androidx.compose.material.icons.filled.Update
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -59,12 +62,14 @@ fun BookCover(
|
||||
val useDefault = CoverConfig.useDefaultCover
|
||||
val finalPath = if (useDefault) null else path
|
||||
|
||||
// 为每个 BookCover 组件实例基于其书名或路径生成一个随机封面
|
||||
// 这样书架上的每个默认封面都会不同,但对于同一本书又是固定的
|
||||
val randomDefault = remember(name, author, path) {
|
||||
BookCover.getRandomDefaultDrawable(seed = name ?: author ?: path ?: "")
|
||||
// 获取随机封面路径(字符串),而非直接获取 Drawable,避免主线程解码大图
|
||||
val randomPath = remember(name, author, path, isNight) {
|
||||
BookCover.getRandomDefaultPath(seed = name ?: author ?: path ?: "")
|
||||
}
|
||||
|
||||
// 检查是否有自定义随机封面设置
|
||||
val hasCustomDefault = !randomPath.isNullOrBlank()
|
||||
|
||||
var isOnlineCoverLoaded by remember(path) { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
@@ -76,25 +81,26 @@ fun BookCover(
|
||||
} else Modifier
|
||||
)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
/*
|
||||
.then(
|
||||
if (CoverConfig.coverShowStroke) {
|
||||
Modifier.border(
|
||||
0.5.dp,
|
||||
MaterialTheme.colorScheme.outlineVariant,
|
||||
RoundedCornerShape(4.dp)
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
*/
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
) {
|
||||
// 1. 封面底图
|
||||
// 如果没有自定义随机封面,显示书本图标作为底图
|
||||
if (!hasCustomDefault) {
|
||||
Icon(
|
||||
Icons.Default.Book,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
// 1. 封面图层
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(finalPath ?: randomDefault)
|
||||
.placeholder(randomDefault)
|
||||
.error(randomDefault)
|
||||
.data(finalPath ?: randomPath)
|
||||
// 限制解码分辨率
|
||||
.size(400, 560)
|
||||
.crossfade(true)
|
||||
.setParameter("sourceOrigin", sourceOrigin)
|
||||
.setParameter("loadOnlyWifi", CoverConfig.loadCoverOnlyWifi)
|
||||
@@ -115,7 +121,7 @@ fun BookCover(
|
||||
}
|
||||
)
|
||||
|
||||
// 2. 文字叠加层(当没有在线封面或者加载失败时显示)
|
||||
// 2. 文字叠加层:当没有书籍自身封面(或加载失败)时,在随机底图上绘制书名/作者
|
||||
if (!isOnlineCoverLoaded) {
|
||||
CoverTextOverlay(
|
||||
name = name,
|
||||
@@ -152,7 +158,6 @@ private fun CoverTextOverlay(
|
||||
val showAuthor =
|
||||
(if (isNight) CoverConfig.coverShowAuthorN else CoverConfig.coverShowAuthor) && showName
|
||||
|
||||
// 如果都不显示,直接返回
|
||||
if (!showName && !showAuthor) return
|
||||
|
||||
val secondaryColor = MaterialTheme.colorScheme.secondary.toArgb()
|
||||
@@ -171,7 +176,6 @@ private fun CoverTextOverlay(
|
||||
drawIntoCanvas { canvas ->
|
||||
val nativeCanvas = canvas.nativeCanvas
|
||||
|
||||
// 绘制书名
|
||||
if (showName && !name.isNullOrBlank()) {
|
||||
val paint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
@@ -200,9 +204,7 @@ private fun CoverTextOverlay(
|
||||
nativeCanvas.withSave {
|
||||
val textX = (viewWidth - maxWidth) / 2f
|
||||
val textY = viewHeight * 0.08f
|
||||
|
||||
translate(textX, textY)
|
||||
|
||||
if (CoverConfig.coverShowStroke) {
|
||||
textPaint.style = Paint.Style.STROKE
|
||||
textPaint.strokeWidth = textPaint.textSize / 12
|
||||
@@ -210,8 +212,6 @@ private fun CoverTextOverlay(
|
||||
textPaint.color = Color.White.toArgb()
|
||||
textPaint.clearShadowLayer()
|
||||
layout.draw(this)
|
||||
|
||||
// 还原
|
||||
textPaint.style = Paint.Style.FILL
|
||||
textPaint.color = originalColor
|
||||
if (CoverConfig.coverShowShadow) {
|
||||
@@ -225,7 +225,6 @@ private fun CoverTextOverlay(
|
||||
var startY = viewHeight * 0.16f
|
||||
val fm = paint.fontMetrics
|
||||
val charHeight = fm.bottom - fm.top
|
||||
|
||||
name.forEach { char ->
|
||||
if (CoverConfig.coverShowStroke) {
|
||||
val strokePaint = Paint(paint).apply {
|
||||
@@ -246,7 +245,6 @@ private fun CoverTextOverlay(
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制作者
|
||||
if (showAuthor && !author.isNullOrBlank()) {
|
||||
val paint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
@@ -257,7 +255,6 @@ private fun CoverTextOverlay(
|
||||
setShadowLayer(4f, 1f, 1f, shadowColor)
|
||||
}
|
||||
}
|
||||
|
||||
if (isHorizontal) {
|
||||
val authorText = TextUtils.ellipsize(
|
||||
author,
|
||||
@@ -265,7 +262,6 @@ private fun CoverTextOverlay(
|
||||
viewWidth * 0.9f,
|
||||
TextUtils.TruncateAt.END
|
||||
)
|
||||
|
||||
if (CoverConfig.coverShowStroke) {
|
||||
val strokePaint = Paint(paint).apply {
|
||||
color = Color.White.toArgb()
|
||||
@@ -292,7 +288,6 @@ private fun CoverTextOverlay(
|
||||
val charHeight = fm.bottom - fm.top
|
||||
var startY = viewHeight * 0.16f - (author.length * charHeight)
|
||||
startY = startY.coerceAtLeast(viewHeight * 0.2f)
|
||||
|
||||
author.forEach { char ->
|
||||
nativeCanvas.drawText(char.toString(), startX, startY, paint)
|
||||
startY += charHeight
|
||||
|
||||
Reference in New Issue
Block a user