fix: 优化书籍详情内存占用
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package io.legado.app.ui.book.info
|
package io.legado.app.ui.book.info
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
@@ -13,8 +14,8 @@ import kotlinx.collections.immutable.persistentListOf
|
|||||||
const val READER_RESULT_DELETED = 100
|
const val READER_RESULT_DELETED = 100
|
||||||
|
|
||||||
data class BookInfoUiState(
|
data class BookInfoUiState(
|
||||||
val book: Book? = null,
|
val book: BookInfoBookUi? = null,
|
||||||
val chapterList: List<BookChapter> = emptyList(),
|
val hasChapters: Boolean = false,
|
||||||
val webFiles: List<BookInfoWebFile> = emptyList(),
|
val webFiles: List<BookInfoWebFile> = emptyList(),
|
||||||
val kindLabels: List<String> = emptyList(),
|
val kindLabels: List<String> = emptyList(),
|
||||||
val groupNames: String? = null,
|
val groupNames: String? = null,
|
||||||
@@ -22,7 +23,7 @@ data class BookInfoUiState(
|
|||||||
val readRecordTotalTime: Long = 0L,
|
val readRecordTotalTime: Long = 0L,
|
||||||
val readRecordTimelineDays: List<ReadRecordTimelineDay> = emptyList(),
|
val readRecordTimelineDays: List<ReadRecordTimelineDay> = emptyList(),
|
||||||
val inBookshelf: Boolean = false,
|
val inBookshelf: Boolean = false,
|
||||||
val bookSource: BookSource? = null,
|
val bookSource: BookInfoSourceUi? = null,
|
||||||
val relatedBooks: ImmutableList<RelatedBooksUi> = persistentListOf(),
|
val relatedBooks: ImmutableList<RelatedBooksUi> = persistentListOf(),
|
||||||
val isTocLoading: Boolean = true,
|
val isTocLoading: Boolean = true,
|
||||||
val isBusy: Boolean = false,
|
val isBusy: Boolean = false,
|
||||||
@@ -33,11 +34,39 @@ data class BookInfoUiState(
|
|||||||
val dialog: BookInfoDialog? = null,
|
val dialog: BookInfoDialog? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class BookInfoBookUi(
|
||||||
|
val bookUrl: String,
|
||||||
|
val name: String,
|
||||||
|
val author: String,
|
||||||
|
val realAuthor: String,
|
||||||
|
val origin: String,
|
||||||
|
val originName: String,
|
||||||
|
val coverPath: String?,
|
||||||
|
val group: Long,
|
||||||
|
val isLocal: Boolean,
|
||||||
|
val type: Int,
|
||||||
|
val canUpdate: Boolean,
|
||||||
|
val splitLongChapter: Boolean,
|
||||||
|
val durChapterTitle: String?,
|
||||||
|
val latestChapterTitle: String?,
|
||||||
|
val totalChapterNum: Int,
|
||||||
|
val durChapterIndex: Int,
|
||||||
|
val remark: String?,
|
||||||
|
val displayIntro: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class BookInfoSourceUi(
|
||||||
|
val sourceUrl: String,
|
||||||
|
val hasLogin: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
sealed interface BookInfoSheet {
|
sealed interface BookInfoSheet {
|
||||||
data object None : BookInfoSheet
|
data object None : BookInfoSheet
|
||||||
data object CoverPicker : BookInfoSheet
|
data object CoverPicker : BookInfoSheet
|
||||||
data object GroupPicker : BookInfoSheet
|
data object GroupPicker : BookInfoSheet
|
||||||
data object SourcePicker : BookInfoSheet
|
data class SourcePicker(val oldBook: Book) : BookInfoSheet
|
||||||
data object ReadRecord : BookInfoSheet
|
data object ReadRecord : BookInfoSheet
|
||||||
data class WebFiles(val openAfterImport: Boolean) : BookInfoSheet
|
data class WebFiles(val openAfterImport: Boolean) : BookInfoSheet
|
||||||
data class ArchiveEntries(
|
data class ArchiveEntries(
|
||||||
|
|||||||
@@ -64,12 +64,10 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
|
import coil.size.Size
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.constant.BookType
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookChapter
|
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||||
import io.legado.app.ui.main.homepage.modules.BannerModule
|
import io.legado.app.ui.main.homepage.modules.BannerModule
|
||||||
@@ -252,7 +250,7 @@ private fun BookInfoScreenContent(
|
|||||||
}
|
}
|
||||||
BookInfoSummary(
|
BookInfoSummary(
|
||||||
book = book,
|
book = book,
|
||||||
chapterList = state.chapterList,
|
hasChapters = state.hasChapters,
|
||||||
onRemarkClick = { onIntent(BookInfoIntent.RemarkClick) },
|
onRemarkClick = { onIntent(BookInfoIntent.RemarkClick) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -294,10 +292,10 @@ private fun BookInfoScreenContent(
|
|||||||
onConfirm = { onIntent(BookInfoIntent.SelectGroup(it)) },
|
onConfirm = { onIntent(BookInfoIntent.SelectGroup(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BookInfoSheet.SourcePicker -> state.book?.let { book ->
|
is BookInfoSheet.SourcePicker -> {
|
||||||
ChangeSourceSheet(
|
ChangeSourceSheet(
|
||||||
show = currentSheet == BookInfoSheet.SourcePicker,
|
show = currentSheet is BookInfoSheet.SourcePicker,
|
||||||
oldBook = book,
|
oldBook = sheet.oldBook,
|
||||||
onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
|
onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
|
||||||
onReplace = { source, newBook, toc, options ->
|
onReplace = { source, newBook, toc, options ->
|
||||||
onIntent(BookInfoIntent.ReplaceWithSource(source, newBook, toc, options))
|
onIntent(BookInfoIntent.ReplaceWithSource(source, newBook, toc, options))
|
||||||
@@ -419,12 +417,12 @@ private fun BookInfoTransparentTopAppBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? {
|
private fun rememberBookInfoColorTheme(book: BookInfoBookUi?): ThemeOverrideState? {
|
||||||
val useDefaultCover = AppConfig.useDefaultCover || book?.customCoverUrl == "use_default_cover"
|
val useDefaultCover = AppConfig.useDefaultCover || book?.coverPath == "use_default_cover"
|
||||||
if (useDefaultCover) return null
|
if (useDefaultCover) return null
|
||||||
|
|
||||||
val imageLoader = koinInject<ImageLoader>()
|
val imageLoader = koinInject<ImageLoader>()
|
||||||
val coverPath = book?.getDisplayCover()
|
val coverPath = book?.coverPath
|
||||||
val sourceOrigin = book?.origin
|
val sourceOrigin = book?.origin
|
||||||
val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
|
val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
|
||||||
val requestKey = remember(coverPath, sourceOrigin, loadOnlyWifi) {
|
val requestKey = remember(coverPath, sourceOrigin, loadOnlyWifi) {
|
||||||
@@ -480,8 +478,21 @@ private fun BookInfoTopBarActions(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BookInfoBackdrop(
|
private fun BookInfoBackdrop(
|
||||||
book: Book,
|
book: BookInfoBookUi,
|
||||||
) {
|
) {
|
||||||
|
val backdropState = remember(
|
||||||
|
book.name,
|
||||||
|
book.author,
|
||||||
|
book.coverPath,
|
||||||
|
book.origin,
|
||||||
|
) {
|
||||||
|
BookInfoBackdropState(
|
||||||
|
name = book.name,
|
||||||
|
author = book.author,
|
||||||
|
coverPath = book.coverPath,
|
||||||
|
sourceOrigin = book.origin,
|
||||||
|
)
|
||||||
|
}
|
||||||
val seedOverlay = lerp(
|
val seedOverlay = lerp(
|
||||||
LegadoTheme.colorScheme.secondaryContainer,
|
LegadoTheme.colorScheme.secondaryContainer,
|
||||||
LegadoTheme.seedColor,
|
LegadoTheme.seedColor,
|
||||||
@@ -489,21 +500,25 @@ private fun BookInfoBackdrop(
|
|||||||
)
|
)
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Crossfade(
|
Crossfade(
|
||||||
targetState = book,
|
targetState = backdropState,
|
||||||
animationSpec = tween(800),
|
animationSpec = tween(800),
|
||||||
label = "BackdropCrossfade"
|
label = "BackdropCrossfade"
|
||||||
) { currentBook ->
|
) { currentBook ->
|
||||||
BookCoverImage(
|
BookCoverImage(
|
||||||
name = currentBook.name,
|
name = currentBook.name,
|
||||||
author = currentBook.author,
|
author = currentBook.author,
|
||||||
path = currentBook.getDisplayCover(),
|
path = currentBook.coverPath,
|
||||||
sourceOrigin = currentBook.origin,
|
sourceOrigin = currentBook.sourceOrigin,
|
||||||
|
memoryCacheKey = currentBook.coverPath?.let { "$it#book-info-backdrop" },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(480.dp)
|
.height(480.dp)
|
||||||
.blur(24.dp),
|
.blur(24.dp),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
showLoadingPlaceholder = false,
|
showLoadingPlaceholder = false,
|
||||||
|
requestBuilder = {
|
||||||
|
size(Size(384, 384))
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
@@ -530,6 +545,13 @@ private fun BookInfoBackdrop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class BookInfoBackdropState(
|
||||||
|
val name: String,
|
||||||
|
val author: String,
|
||||||
|
val coverPath: String?,
|
||||||
|
val sourceOrigin: String?,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BookInfoOverflowMenu(
|
private fun BookInfoOverflowMenu(
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
@@ -563,7 +585,7 @@ private fun BookInfoOverflowMenu(
|
|||||||
onClick = { onMenuAction(BookInfoMenuAction.Upload) }
|
onClick = { onMenuAction(BookInfoMenuAction.Upload) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!state.bookSource?.loginUrl.isNullOrBlank()) {
|
if (state.bookSource?.hasLogin == true) {
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
text = stringResource(R.string.login),
|
text = stringResource(R.string.login),
|
||||||
onClick = { onMenuAction(BookInfoMenuAction.Login) }
|
onClick = { onMenuAction(BookInfoMenuAction.Login) }
|
||||||
@@ -602,7 +624,7 @@ private fun BookInfoOverflowMenu(
|
|||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
text = stringResource(R.string.split_long_chapter),
|
text = stringResource(R.string.split_long_chapter),
|
||||||
onClick = { onMenuAction(BookInfoMenuAction.ToggleSplitLongChapter) },
|
onClick = { onMenuAction(BookInfoMenuAction.ToggleSplitLongChapter) },
|
||||||
isSelected = book.getSplitLongChapter()
|
isSelected = book.splitLongChapter
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
@@ -623,7 +645,7 @@ private fun BookInfoOverflowMenu(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BookInfoHeader(
|
private fun BookInfoHeader(
|
||||||
book: Book,
|
book: BookInfoBookUi,
|
||||||
kindLabels: List<String>,
|
kindLabels: List<String>,
|
||||||
groupNames: String?,
|
groupNames: String?,
|
||||||
onCoverClick: () -> Unit,
|
onCoverClick: () -> Unit,
|
||||||
@@ -668,7 +690,7 @@ private fun BookInfoHeader(
|
|||||||
CoilBookCover(
|
CoilBookCover(
|
||||||
name = book.name,
|
name = book.name,
|
||||||
author = book.author,
|
author = book.author,
|
||||||
path = book.getDisplayCover(),
|
path = book.coverPath,
|
||||||
sourceOrigin = book.origin,
|
sourceOrigin = book.origin,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(112.dp)
|
.width(112.dp)
|
||||||
@@ -720,7 +742,7 @@ private fun BookInfoHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
AnimatedTextLine(
|
AnimatedTextLine(
|
||||||
text = stringResource(R.string.author_show, book.getRealAuthor()),
|
text = stringResource(R.string.author_show, book.realAuthor),
|
||||||
style = LegadoTheme.typography.bodyLarge,
|
style = LegadoTheme.typography.bodyLarge,
|
||||||
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.combinedClickable(
|
modifier = Modifier.combinedClickable(
|
||||||
@@ -886,8 +908,8 @@ private fun BookInfoActionCard(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BookInfoSummary(
|
private fun BookInfoSummary(
|
||||||
book: Book,
|
book: BookInfoBookUi,
|
||||||
chapterList: List<BookChapter>,
|
hasChapters: Boolean,
|
||||||
onRemarkClick: () -> Unit,
|
onRemarkClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -928,7 +950,7 @@ private fun BookInfoSummary(
|
|||||||
color = LegadoTheme.colorScheme.secondary,
|
color = LegadoTheme.colorScheme.secondary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (chapterList.isEmpty()) {
|
if (!hasChapters) {
|
||||||
AnimatedTextLine(
|
AnimatedTextLine(
|
||||||
text = stringResource(R.string.error_load_toc),
|
text = stringResource(R.string.error_load_toc),
|
||||||
style = LegadoTheme.typography.bodySmall,
|
style = LegadoTheme.typography.bodySmall,
|
||||||
@@ -952,7 +974,7 @@ private fun BookInfoSummary(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
AnimatedTextLine(
|
AnimatedTextLine(
|
||||||
text = book.getDisplayIntro().orEmpty().ifBlank { stringResource(R.string.intro_show_null) },
|
text = book.displayIntro.orEmpty().ifBlank { stringResource(R.string.intro_show_null) },
|
||||||
style = LegadoTheme.typography.bodyMedium,
|
style = LegadoTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,12 +62,14 @@ import io.legado.app.utils.postEvent
|
|||||||
import io.legado.app.utils.splitNotBlank
|
import io.legado.app.utils.splitNotBlank
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
@@ -76,6 +78,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class BookInfoViewModel(
|
class BookInfoViewModel(
|
||||||
@@ -119,6 +122,7 @@ class BookInfoViewModel(
|
|||||||
|
|
||||||
private var changeSourceCoroutine: Coroutine<*>? = null
|
private var changeSourceCoroutine: Coroutine<*>? = null
|
||||||
private var readRecordObserveJob: Job? = null
|
private var readRecordObserveJob: Job? = null
|
||||||
|
private var relatedBooksLoadJob: Job? = null
|
||||||
|
|
||||||
fun initData(intent: Intent) {
|
fun initData(intent: Intent) {
|
||||||
initData(
|
initData(
|
||||||
@@ -162,6 +166,7 @@ class BookInfoViewModel(
|
|||||||
bookSource = null
|
bookSource = null
|
||||||
chapterChanged = false
|
chapterChanged = false
|
||||||
clearReadRecordObserve()
|
clearReadRecordObserve()
|
||||||
|
relatedBooksLoadJob?.cancel()
|
||||||
syncUiState()
|
syncUiState()
|
||||||
execute {
|
execute {
|
||||||
val dbBook = appDb.bookDao.getBook(bookUrl)
|
val dbBook = appDb.bookDao.getBook(bookUrl)
|
||||||
@@ -214,7 +219,8 @@ class BookInfoViewModel(
|
|||||||
?.let { showDialog(BookInfoDialog.PhotoPreview(it)) }
|
?.let { showDialog(BookInfoDialog.PhotoPreview(it)) }
|
||||||
|
|
||||||
BookInfoIntent.GroupClick -> setSheet(BookInfoSheet.GroupPicker)
|
BookInfoIntent.GroupClick -> setSheet(BookInfoSheet.GroupPicker)
|
||||||
BookInfoIntent.ChangeSourceClick -> setSheet(BookInfoSheet.SourcePicker)
|
BookInfoIntent.ChangeSourceClick -> currentBook?.uiCopy()
|
||||||
|
?.let { setSheet(BookInfoSheet.SourcePicker(it)) }
|
||||||
BookInfoIntent.ReadRecordClick -> setSheet(BookInfoSheet.ReadRecord)
|
BookInfoIntent.ReadRecordClick -> setSheet(BookInfoSheet.ReadRecord)
|
||||||
BookInfoIntent.RemarkClick -> showDialog(BookInfoDialog.EditRemark(currentBook?.remark))
|
BookInfoIntent.RemarkClick -> showDialog(BookInfoDialog.EditRemark(currentBook?.remark))
|
||||||
is BookInfoIntent.SaveCover -> {
|
is BookInfoIntent.SaveCover -> {
|
||||||
@@ -733,7 +739,7 @@ class BookInfoViewModel(
|
|||||||
} else {
|
} else {
|
||||||
loadChapter(loadedBook, runPreUpdateJs)
|
loadChapter(loadedBook, runPreUpdateJs)
|
||||||
}
|
}
|
||||||
loadRelatedBooks(loadedBook, source)
|
scheduleRelatedBooksLoad(loadedBook, source)
|
||||||
}.onError {
|
}.onError {
|
||||||
AppLog.put("获取书籍信息失败\n${it.localizedMessage}", it)
|
AppLog.put("获取书籍信息失败\n${it.localizedMessage}", it)
|
||||||
context.toastOnUi(R.string.error_get_book_info)
|
context.toastOnUi(R.string.error_get_book_info)
|
||||||
@@ -792,7 +798,7 @@ class BookInfoViewModel(
|
|||||||
if (chapters.isNotEmpty()) {
|
if (chapters.isNotEmpty()) {
|
||||||
currentChapterList = chapters
|
currentChapterList = chapters
|
||||||
syncUiState(isTocLoading = false)
|
syncUiState(isTocLoading = false)
|
||||||
source?.let { loadRelatedBooks(book, it) }
|
source?.let { scheduleRelatedBooksLoad(book, it) }
|
||||||
} else {
|
} else {
|
||||||
loadChapter(book)
|
loadChapter(book)
|
||||||
}
|
}
|
||||||
@@ -1291,8 +1297,8 @@ class BookInfoViewModel(
|
|||||||
private fun syncUiState(isTocLoading: Boolean = _uiState.value.isTocLoading) {
|
private fun syncUiState(isTocLoading: Boolean = _uiState.value.isTocLoading) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
book = currentBook?.uiCopy(),
|
book = currentBook?.toBookInfoBookUi(),
|
||||||
chapterList = currentChapterList,
|
hasChapters = currentChapterList.isNotEmpty(),
|
||||||
webFiles = currentWebFiles,
|
webFiles = currentWebFiles,
|
||||||
relatedBooks = currentRelatedBooks.toImmutableList(),
|
relatedBooks = currentRelatedBooks.toImmutableList(),
|
||||||
kindLabels = currentKindLabels,
|
kindLabels = currentKindLabels,
|
||||||
@@ -1301,7 +1307,7 @@ class BookInfoViewModel(
|
|||||||
readRecordTotalTime = currentReadRecordTotalTime,
|
readRecordTotalTime = currentReadRecordTotalTime,
|
||||||
readRecordTimelineDays = currentReadRecordTimelineDays,
|
readRecordTimelineDays = currentReadRecordTimelineDays,
|
||||||
inBookshelf = inBookshelf,
|
inBookshelf = inBookshelf,
|
||||||
bookSource = bookSource,
|
bookSource = bookSource?.toBookInfoSourceUi(),
|
||||||
isTocLoading = isTocLoading,
|
isTocLoading = isTocLoading,
|
||||||
deleteAlertEnabled = LocalConfig.bookInfoDeleteAlert,
|
deleteAlertEnabled = LocalConfig.bookInfoDeleteAlert,
|
||||||
deleteOriginal = LocalConfig.deleteBookOriginal,
|
deleteOriginal = LocalConfig.deleteBookOriginal,
|
||||||
@@ -1332,14 +1338,50 @@ class BookInfoViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadRelatedBooks(book: Book, source: BookSource) {
|
private fun scheduleRelatedBooksLoad(
|
||||||
val modulesJson = source.ruleBookInfo?.relatedBooks
|
book: Book,
|
||||||
if (modulesJson.isNullOrBlank()) {
|
source: BookSource,
|
||||||
|
delayMillis: Long = 350L,
|
||||||
|
) {
|
||||||
|
relatedBooksLoadJob?.cancel()
|
||||||
|
relatedBooksLoadJob = viewModelScope.launch {
|
||||||
|
delay(delayMillis)
|
||||||
|
if (!isCurrentBookSource(book, source)) return@launch
|
||||||
|
|
||||||
|
val modules = parseRelatedBookModules(source)
|
||||||
|
if (modules.isEmpty()) {
|
||||||
currentRelatedBooks = emptyList()
|
currentRelatedBooks = emptyList()
|
||||||
syncUiState()
|
syncUiState()
|
||||||
return
|
return@launch
|
||||||
}
|
}
|
||||||
val modules = try {
|
|
||||||
|
try {
|
||||||
|
val result = withContext(IO) {
|
||||||
|
loadRelatedBooks(book, source, modules)
|
||||||
|
}
|
||||||
|
if (!isCurrentBookSource(book, source)) return@launch
|
||||||
|
currentRelatedBooks = result
|
||||||
|
syncUiState()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (!isCurrentBookSource(book, source)) return@launch
|
||||||
|
currentRelatedBooks = emptyList()
|
||||||
|
syncUiState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isCurrentBookSource(book: Book, source: BookSource): Boolean {
|
||||||
|
return currentBook?.bookUrl == book.bookUrl && bookSource?.bookSourceUrl == source.bookSourceUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRelatedBookModules(source: BookSource): List<RelatedBooksDef> {
|
||||||
|
val modulesJson = source.ruleBookInfo?.relatedBooks
|
||||||
|
if (modulesJson.isNullOrBlank()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
return try {
|
||||||
GSON.fromJsonArray<RelatedBooksDef>(modulesJson)
|
GSON.fromJsonArray<RelatedBooksDef>(modulesJson)
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
?.filter { !it.url.isNullOrBlank() }
|
?.filter { !it.url.isNullOrBlank() }
|
||||||
@@ -1348,37 +1390,34 @@ class BookInfoViewModel(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
if (modules.isEmpty()) {
|
|
||||||
currentRelatedBooks = emptyList()
|
|
||||||
syncUiState()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
execute {
|
|
||||||
coroutineScope {
|
private suspend fun loadRelatedBooks(
|
||||||
|
book: Book,
|
||||||
|
source: BookSource,
|
||||||
|
modules: List<RelatedBooksDef>,
|
||||||
|
): List<RelatedBooksUi> {
|
||||||
|
return coroutineScope {
|
||||||
modules.map { def ->
|
modules.map { def ->
|
||||||
async {
|
async {
|
||||||
|
val url = def.url.orEmpty()
|
||||||
val (resolvedUrl, books) = try {
|
val (resolvedUrl, books) = try {
|
||||||
resolveAndExplore(source, def.url!!, book)
|
resolveAndExplore(source, url, book)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
def.url!! to emptyList()
|
url to emptyList()
|
||||||
}
|
}
|
||||||
RelatedBooksUi(
|
RelatedBooksUi(
|
||||||
key = def.key ?: def.title.orEmpty(),
|
key = def.key ?: def.title.orEmpty(),
|
||||||
title = def.title.orEmpty(),
|
title = def.title.orEmpty(),
|
||||||
url = def.url!!,
|
url = url,
|
||||||
resolvedUrl = resolvedUrl,
|
resolvedUrl = resolvedUrl,
|
||||||
books = books.filter { it.bookUrl != book.bookUrl }.toImmutableList(),
|
books = books.filter { it.bookUrl != book.bookUrl }.toImmutableList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.awaitAll().filter { it.books.isNotEmpty() }
|
}.awaitAll().filter { it.books.isNotEmpty() }
|
||||||
}
|
}
|
||||||
}.onSuccess { result ->
|
|
||||||
currentRelatedBooks = result
|
|
||||||
syncUiState()
|
|
||||||
}.onError {
|
|
||||||
currentRelatedBooks = emptyList()
|
|
||||||
syncUiState()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun resolveAndExplore(
|
private suspend fun resolveAndExplore(
|
||||||
@@ -1393,6 +1432,36 @@ class BookInfoViewModel(
|
|||||||
_effects.tryEmit(effect)
|
_effects.tryEmit(effect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Book.toBookInfoBookUi(): BookInfoBookUi {
|
||||||
|
return BookInfoBookUi(
|
||||||
|
bookUrl = bookUrl,
|
||||||
|
name = name,
|
||||||
|
author = author,
|
||||||
|
realAuthor = getRealAuthor(),
|
||||||
|
origin = origin,
|
||||||
|
originName = originName,
|
||||||
|
coverPath = getDisplayCover(),
|
||||||
|
group = group,
|
||||||
|
isLocal = isLocal,
|
||||||
|
type = type,
|
||||||
|
canUpdate = canUpdate,
|
||||||
|
splitLongChapter = getSplitLongChapter(),
|
||||||
|
durChapterTitle = durChapterTitle,
|
||||||
|
latestChapterTitle = latestChapterTitle,
|
||||||
|
totalChapterNum = totalChapterNum,
|
||||||
|
durChapterIndex = durChapterIndex,
|
||||||
|
remark = remark,
|
||||||
|
displayIntro = getDisplayIntro(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BookSource.toBookInfoSourceUi(): BookInfoSourceUi {
|
||||||
|
return BookInfoSourceUi(
|
||||||
|
sourceUrl = bookSourceUrl,
|
||||||
|
hasLogin = !loginUrl.isNullOrBlank(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun Book.uiCopy(): Book {
|
private fun Book.uiCopy(): Book {
|
||||||
return copy().also { snapshot ->
|
return copy().also { snapshot ->
|
||||||
snapshot.infoHtml = infoHtml
|
snapshot.infoHtml = infoHtml
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.graphics.withSave
|
import androidx.core.graphics.withSave
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
@@ -58,6 +59,7 @@ fun BookCoverImage(
|
|||||||
path: String?,
|
path: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sourceOrigin: String? = null,
|
sourceOrigin: String? = null,
|
||||||
|
memoryCacheKey: String? = null,
|
||||||
ignoreUseDefaultCover: Boolean = false,
|
ignoreUseDefaultCover: Boolean = false,
|
||||||
showLoadingPlaceholder: Boolean = true,
|
showLoadingPlaceholder: Boolean = true,
|
||||||
contentScale: ContentScale = ContentScale.Crop,
|
contentScale: ContentScale = ContentScale.Crop,
|
||||||
@@ -65,6 +67,7 @@ fun BookCoverImage(
|
|||||||
onSuccess: (() -> Unit)? = null,
|
onSuccess: (() -> Unit)? = null,
|
||||||
onError: (() -> Unit)? = null,
|
onError: (() -> Unit)? = null,
|
||||||
sharedCoverKey: String? = null,
|
sharedCoverKey: String? = null,
|
||||||
|
requestBuilder: ImageRequest.Builder.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isNight = LegadoTheme.isDark
|
val isNight = LegadoTheme.isDark
|
||||||
@@ -116,7 +119,8 @@ fun BookCoverImage(
|
|||||||
sourceOrigin = sourceOrigin,
|
sourceOrigin = sourceOrigin,
|
||||||
loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
|
loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
|
||||||
crossfade = showLoadingPlaceholder,
|
crossfade = showLoadingPlaceholder,
|
||||||
memoryCacheKey = finalPath,
|
memoryCacheKey = memoryCacheKey ?: finalPath,
|
||||||
|
configure = requestBuilder,
|
||||||
),
|
),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
imageLoader = koinInject(),
|
imageLoader = koinInject(),
|
||||||
|
|||||||
Reference in New Issue
Block a user