[优化] 性能优化
This commit is contained in:
@@ -76,6 +76,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -109,6 +110,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -139,6 +141,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -169,6 +172,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -204,6 +208,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -240,6 +245,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -271,6 +277,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -303,6 +310,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -333,6 +341,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -364,6 +373,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -394,6 +404,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -424,6 +435,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -454,6 +466,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
@@ -484,6 +497,7 @@ interface BookDao {
|
||||
bookUrl,
|
||||
name,
|
||||
author,
|
||||
origin,
|
||||
originName,
|
||||
coverUrl,
|
||||
customCoverUrl,
|
||||
|
||||
@@ -42,7 +42,6 @@ sealed interface BookInfoSheet {
|
||||
}
|
||||
|
||||
sealed interface BookInfoDialog {
|
||||
data object AddToShelfOnBack : BookInfoDialog
|
||||
data class DeleteBook(val isLocal: Boolean) : BookInfoDialog
|
||||
data class EditRemark(val remark: String?) : BookInfoDialog
|
||||
data class PhotoPreview(val path: String) : BookInfoDialog
|
||||
@@ -60,7 +59,6 @@ data class BookInfoWebFile(
|
||||
}
|
||||
|
||||
sealed interface BookInfoIntent {
|
||||
data object BackPressed : BookInfoIntent
|
||||
data object DismissSheet : BookInfoIntent
|
||||
data object DismissDialog : BookInfoIntent
|
||||
data object DismissAppLogSheet : BookInfoIntent
|
||||
@@ -77,7 +75,6 @@ sealed interface BookInfoIntent {
|
||||
data object ChangeSourceClick : BookInfoIntent
|
||||
data object ReadRecordClick : BookInfoIntent
|
||||
data object RemarkClick : BookInfoIntent
|
||||
data object ConfirmBackAddToShelf : BookInfoIntent
|
||||
data class ConfirmDelete(val deleteOriginal: Boolean) : BookInfoIntent
|
||||
data class UpdateRemark(val remark: String) : BookInfoIntent
|
||||
data class SelectGroup(val groupId: Long) : BookInfoIntent
|
||||
|
||||
@@ -5,6 +5,9 @@ import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -33,6 +36,7 @@ import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.startActivity
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun BookInfoRouteScreen(
|
||||
bookUrl: String,
|
||||
@@ -40,6 +44,9 @@ fun BookInfoRouteScreen(
|
||||
onBack: () -> Unit,
|
||||
onFinish: (resultCode: Int?, afterTransition: Boolean) -> Unit,
|
||||
onOpenSearch: (String) -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
onRegisterVariableSetter: (((String, String?) -> Unit)?) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -156,6 +163,9 @@ fun BookInfoRouteScreen(
|
||||
state = viewModel.uiState.collectAsStateWithLifecycle().value,
|
||||
onIntent = viewModel::onIntent,
|
||||
onBack = onBack,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package io.legado.app.ui.book.info
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@@ -109,6 +111,9 @@ fun BookInfoScreen(
|
||||
state: BookInfoUiState,
|
||||
onIntent: (BookInfoIntent) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
) {
|
||||
val bookColorTheme = rememberBookInfoColorTheme(state.book)
|
||||
|
||||
@@ -117,16 +122,22 @@ fun BookInfoScreen(
|
||||
state = state,
|
||||
onIntent = onIntent,
|
||||
onBack = onBack,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
private fun BookInfoScreenContent(
|
||||
state: BookInfoUiState,
|
||||
onIntent: (BookInfoIntent) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope?,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope?,
|
||||
sharedCoverKey: String?,
|
||||
) {
|
||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||
val scrollBehavior = if (isMiuix) {
|
||||
@@ -138,8 +149,6 @@ private fun BookInfoScreenContent(
|
||||
val pullState = rememberPullToRefreshState()
|
||||
var showMenu by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
BackHandler { onIntent(BookInfoIntent.BackPressed) }
|
||||
|
||||
AppScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -150,7 +159,7 @@ private fun BookInfoScreenContent(
|
||||
showMenu = showMenu,
|
||||
onShowMenuChange = { showMenu = it },
|
||||
onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
|
||||
onBackPressed = { onIntent(BookInfoIntent.BackPressed) },
|
||||
onBackPressed = onBack,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
@@ -211,6 +220,9 @@ private fun BookInfoScreenContent(
|
||||
onAuthorClick = { onIntent(BookInfoIntent.AuthorClick(it)) },
|
||||
onBookNameClick = { onIntent(BookInfoIntent.BookNameClick(it)) },
|
||||
onOriginClick = { onIntent(BookInfoIntent.OriginClick) },
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
}
|
||||
item {
|
||||
@@ -313,7 +325,7 @@ private fun BookInfoScreenContent(
|
||||
)
|
||||
}
|
||||
|
||||
BookInfoDialogs(state = state, onIntent = onIntent, onBack = onBack)
|
||||
BookInfoDialogs(state = state, onIntent = onIntent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -391,14 +403,27 @@ private fun BookInfoTransparentTopAppBar(
|
||||
@Composable
|
||||
private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? {
|
||||
val imageLoader = koinInject<ImageLoader>()
|
||||
val coverPath = book?.getDisplayCover()
|
||||
val sourceOrigin = book?.origin
|
||||
var shouldExtractColor by remember(book?.bookUrl) { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(book?.bookUrl) {
|
||||
shouldExtractColor = false
|
||||
if (book != null) {
|
||||
delay(520)
|
||||
shouldExtractColor = true
|
||||
}
|
||||
}
|
||||
|
||||
val coverPath = if (shouldExtractColor) book?.getDisplayCover() else null
|
||||
val sourceOrigin = if (shouldExtractColor) book?.origin else null
|
||||
val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
|
||||
val requestKey = remember(coverPath, sourceOrigin, loadOnlyWifi) {
|
||||
listOf(coverPath, sourceOrigin, loadOnlyWifi)
|
||||
}
|
||||
|
||||
val seedColor = rememberImageSeedColor(
|
||||
imageLoader = imageLoader,
|
||||
data = coverPath,
|
||||
requestKey = listOf(coverPath, sourceOrigin, loadOnlyWifi),
|
||||
requestKey = requestKey,
|
||||
) {
|
||||
setParameter("sourceOrigin", sourceOrigin)
|
||||
setParameter("loadOnlyWifi", loadOnlyWifi)
|
||||
@@ -449,6 +474,16 @@ private fun BookInfoBackdrop(book: Book) {
|
||||
val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
|
||||
val context = LocalContext.current
|
||||
val imageLoader = koinInject<ImageLoader>()
|
||||
var showBackdropImage by remember(cover) { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(cover) {
|
||||
showBackdropImage = false
|
||||
if (!cover.isNullOrBlank()) {
|
||||
delay(520)
|
||||
showBackdropImage = true
|
||||
}
|
||||
}
|
||||
|
||||
val backdropRequest = remember(cover, sourceOrigin, loadOnlyWifi, context) {
|
||||
buildCoverImageRequest(
|
||||
context = context,
|
||||
@@ -464,14 +499,15 @@ private fun BookInfoBackdrop(book: Book) {
|
||||
0.42f
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (!cover.isNullOrBlank()) {
|
||||
if (!cover.isNullOrBlank() && showBackdropImage) {
|
||||
AsyncImage(
|
||||
model = backdropRequest,
|
||||
imageLoader = imageLoader,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.blur(32.dp),
|
||||
.fillMaxWidth()
|
||||
.height(360.dp)
|
||||
.blur(24.dp),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
@@ -600,6 +636,9 @@ private fun BookInfoHeader(
|
||||
onAuthorClick: (Boolean) -> Unit,
|
||||
onBookNameClick: (Boolean) -> Unit,
|
||||
onOriginClick: () -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope?,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope?,
|
||||
sharedCoverKey: String?,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -631,12 +670,25 @@ private fun BookInfoHeader(
|
||||
.width(112.dp)
|
||||
.combinedClickable(onClick = onCoverClick, onLongClick = onCoverLongClick)
|
||||
) {
|
||||
val coverModifier = with(sharedTransitionScope) {
|
||||
if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) {
|
||||
Modifier
|
||||
.width(112.dp)
|
||||
.sharedElement(
|
||||
sharedContentState = rememberSharedContentState(sharedCoverKey),
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
} else {
|
||||
Modifier.width(112.dp)
|
||||
}
|
||||
}
|
||||
CoilBookCover(
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
path = book.getDisplayCover(),
|
||||
sourceOrigin = book.origin,
|
||||
modifier = Modifier.width(112.dp)
|
||||
modifier = coverModifier,
|
||||
showLoadingPlaceholder = sharedCoverKey == null,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
@@ -895,25 +947,11 @@ private fun BookInfoSummary(
|
||||
private fun BookInfoDialogs(
|
||||
state: BookInfoUiState,
|
||||
onIntent: (BookInfoIntent) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val dialog = state.dialog
|
||||
var deleteOriginal by remember(dialog, state.deleteOriginal) { mutableStateOf(state.deleteOriginal) }
|
||||
var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) }
|
||||
|
||||
if (dialog is BookInfoDialog.AddToShelfOnBack) {
|
||||
AppAlertDialog(
|
||||
show = true,
|
||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
title = stringResource(R.string.add_to_bookshelf),
|
||||
text = stringResource(R.string.check_add_bookshelf, state.book?.name.orEmpty()),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = { onIntent(BookInfoIntent.ConfirmBackAddToShelf) },
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = onBack,
|
||||
)
|
||||
}
|
||||
|
||||
if (dialog is BookInfoDialog.DeleteBook) {
|
||||
AppAlertDialog(
|
||||
show = true,
|
||||
|
||||
@@ -118,17 +118,22 @@ class BookInfoViewModel(
|
||||
clearReadRecordObserve()
|
||||
_uiState.value = BookInfoUiState()
|
||||
execute {
|
||||
appDb.bookDao.getBook(bookUrl)?.let {
|
||||
val book = appDb.bookDao.getBook(bookUrl)?.let {
|
||||
inBookshelf = !it.isNotShelf
|
||||
return@execute it
|
||||
}
|
||||
appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()?.let {
|
||||
it
|
||||
} ?: appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()?.let {
|
||||
inBookshelf = false
|
||||
return@execute it
|
||||
it
|
||||
} ?: throw NoStackTraceException("未找到书籍")
|
||||
|
||||
val source = if (book.isLocal) {
|
||||
null
|
||||
} else {
|
||||
appDb.bookSourceDao.getBookSource(book.origin)
|
||||
}
|
||||
throw NoStackTraceException("未找到书籍")
|
||||
book to source
|
||||
}.onSuccess {
|
||||
upBook(it)
|
||||
upBook(it.first, it.second)
|
||||
}.onError {
|
||||
context.toastOnUi(it.localizedMessage ?: "未找到书籍")
|
||||
emitEffect(BookInfoEffect.Finish(afterTransition = true))
|
||||
@@ -137,7 +142,6 @@ class BookInfoViewModel(
|
||||
|
||||
fun onIntent(intent: BookInfoIntent) {
|
||||
when (intent) {
|
||||
BookInfoIntent.BackPressed -> onBackPressed()
|
||||
BookInfoIntent.DismissSheet -> dismissSheet()
|
||||
BookInfoIntent.DismissDialog -> dismissDialog()
|
||||
is BookInfoIntent.MenuAction -> handleMenuAction(intent.action)
|
||||
@@ -159,13 +163,6 @@ class BookInfoViewModel(
|
||||
BookInfoIntent.ChangeSourceClick -> setSheet(BookInfoSheet.SourcePicker)
|
||||
BookInfoIntent.ReadRecordClick -> setSheet(BookInfoSheet.ReadRecord)
|
||||
BookInfoIntent.RemarkClick -> showDialog(BookInfoDialog.EditRemark(currentBook?.remark))
|
||||
BookInfoIntent.ConfirmBackAddToShelf -> {
|
||||
dismissDialog()
|
||||
addToBookshelf {
|
||||
emitEffect(BookInfoEffect.Finish(afterTransition = true))
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoIntent.ConfirmDelete -> {
|
||||
dismissDialog()
|
||||
deleteBook(intent.deleteOriginal)
|
||||
@@ -246,7 +243,17 @@ class BookInfoViewModel(
|
||||
|
||||
fun onInfoEdited() {
|
||||
currentBook?.bookUrl?.let { bookUrl ->
|
||||
appDb.bookDao.getBook(bookUrl)?.let { upBook(it) }
|
||||
execute {
|
||||
val book = appDb.bookDao.getBook(bookUrl) ?: return@execute null
|
||||
val source = if (book.isLocal) {
|
||||
null
|
||||
} else {
|
||||
appDb.bookSourceDao.getBookSource(book.origin)
|
||||
}
|
||||
book to source
|
||||
}.onSuccess {
|
||||
it?.let { (book, source) -> upBook(book, source) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,18 +658,17 @@ class BookInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun upBook(book: Book) {
|
||||
private fun upBook(book: Book, source: BookSource?) {
|
||||
currentBook = book
|
||||
currentChapterList = emptyList()
|
||||
currentWebFiles = emptyList()
|
||||
currentKindLabels = emptyList()
|
||||
currentGroupNames = null
|
||||
currentHasCustomGroup = false
|
||||
bookSource = source
|
||||
syncUiState(isTocLoading = true)
|
||||
refreshMeta(book)
|
||||
upCoverByRule(book)
|
||||
bookSource = if (book.isLocal) null else appDb.bookSourceDao.getBookSource(book.origin)
|
||||
syncUiState(isTocLoading = true)
|
||||
if (book.tocUrl.isEmpty() && !book.isLocal) {
|
||||
loadBookInfo(book, runPreUpdateJs = inBookshelf)
|
||||
} else {
|
||||
@@ -801,14 +807,6 @@ class BookInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBackPressed() {
|
||||
if (!inBookshelf && AppConfig.showAddToShelfAlert && currentBook != null) {
|
||||
showDialog(BookInfoDialog.AddToShelfOnBack)
|
||||
} else {
|
||||
emitEffect(BookInfoEffect.Finish(afterTransition = true))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onReadClick() {
|
||||
val book = currentBook ?: return
|
||||
if (book.isWebFile) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package io.legado.app.ui.main
|
||||
|
||||
fun bookCoverSharedElementKey(bookUrl: String): String = "book-cover:$bookUrl"
|
||||
@@ -6,6 +6,8 @@ import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
@@ -24,6 +26,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.R
|
||||
@@ -340,6 +343,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
routeEvents.tryEmit(resolveStartRoute(intent))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val orientation = resources.configuration.orientation
|
||||
@@ -362,9 +366,10 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
transitionSpec = {
|
||||
SharedTransitionLayout {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
transitionSpec = {
|
||||
(slideIntoContainer(
|
||||
towards = AnimatedContentTransitionScope.SlideDirection.Start,
|
||||
animationSpec = tween(
|
||||
@@ -441,7 +446,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
finish()
|
||||
}
|
||||
},
|
||||
entryProvider = entryProvider {
|
||||
entryProvider = entryProvider {
|
||||
entry<MainRouteHome> {
|
||||
MainScreen(
|
||||
useRail = useRail,
|
||||
@@ -508,7 +513,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
openUrl = openUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
sharedTransitionScope = this@SharedTransitionLayout,
|
||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -671,6 +678,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
onOpenSearch = { keyword ->
|
||||
navigateToRoute(backStack, MainRouteSearch(key = keyword))
|
||||
},
|
||||
sharedTransitionScope = this@SharedTransitionLayout,
|
||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||
sharedCoverKey = bookCoverSharedElementKey(route.bookUrl),
|
||||
onRegisterVariableSetter = { setter ->
|
||||
bookInfoVariableSetter = setter
|
||||
}
|
||||
@@ -695,8 +705,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToRoute(backStack: MutableList<NavKey>, route: NavKey) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.legado.app.ui.main
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import io.legado.app.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
sealed class MainDestination(
|
||||
val route: String,
|
||||
@@ -28,6 +29,6 @@ sealed class MainDestination(
|
||||
)
|
||||
|
||||
companion object {
|
||||
val mainDestinations = listOf(Bookshelf, Explore, Rss, My)
|
||||
val mainDestinations = persistentListOf<MainDestination>(Bookshelf, Explore, Rss, My)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package io.legado.app.ui.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@@ -74,13 +81,18 @@ import io.legado.app.ui.widget.components.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.ui.widget.dialog.TextDialog
|
||||
import io.legado.app.utils.sendToClip
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.startActivityForBook
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class,
|
||||
ExperimentalFoundationApi::class
|
||||
ExperimentalFoundationApi::class, ExperimentalSharedTransitionApi::class
|
||||
)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
@@ -95,14 +107,46 @@ fun MainScreen(
|
||||
onNavigateToBookInfo: (name: String, author: String, bookUrl: String) -> Unit,
|
||||
onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
|
||||
onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
|
||||
onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit
|
||||
onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val mainUiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
val bookshelfViewModel: BookshelfViewModel = koinViewModel()
|
||||
val bookshelfGroupState by bookshelfViewModel.groupSelectorState.collectAsStateWithLifecycle()
|
||||
LaunchedEffect(viewModel, context) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is MainEffect.OpenUrl -> {
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(effect.url))
|
||||
)
|
||||
}
|
||||
|
||||
is MainEffect.CopyUrl -> context.sendToClip(effect.url)
|
||||
is MainEffect.ShowMarkdown -> {
|
||||
val activity = context as? AppCompatActivity ?: return@collect
|
||||
val title = effect.title.ifBlank { context.getString(R.string.help) }
|
||||
val mdText = withContext(Dispatchers.IO) {
|
||||
context.assets
|
||||
.open("web/help/md/${effect.path}.md")
|
||||
.bufferedReader()
|
||||
.use { it.readText() }
|
||||
}
|
||||
activity.showDialogFragment(TextDialog(title, mdText, TextDialog.Mode.MD))
|
||||
}
|
||||
|
||||
is MainEffect.StartActivity -> {
|
||||
context.startActivity(Intent(context, effect.destination).apply {
|
||||
effect.configTag?.let { putExtra("configTag", it) }
|
||||
})
|
||||
}
|
||||
|
||||
MainEffect.ExitApp -> (context as? ComponentActivity)?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val hazeState = remember { HazeState() }
|
||||
val floatingBarSurfaceColor = MaterialTheme.colorScheme.surface
|
||||
@@ -213,34 +257,15 @@ fun MainScreen(
|
||||
)
|
||||
|
||||
if (destination == MainDestination.Bookshelf && showGroupMenu) {
|
||||
RoundDropdownMenu(
|
||||
BookshelfRailGroupMenu(
|
||||
expanded = showGroupMenu,
|
||||
onDismissRequest = { showGroupMenu = false }
|
||||
) { dismiss ->
|
||||
bookshelfGroupState.groups.forEachIndexed { groupIndex, group ->
|
||||
RoundDropdownMenuItem(
|
||||
text = group.groupName,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
if (pagerState.currentPage != index) {
|
||||
pagerState.scrollToPage(index)
|
||||
}
|
||||
bookshelfViewModel.changeGroup(group.groupId)
|
||||
dismiss()
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
if (bookshelfGroupState.selectedGroupIndex == groupIndex) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
onDismissRequest = { showGroupMenu = false },
|
||||
onBeforeSelectGroup = {
|
||||
if (pagerState.currentPage != index) {
|
||||
pagerState.scrollToPage(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -372,7 +397,9 @@ fun MainScreen(
|
||||
onNavigateToSearch = { query -> onNavigateToSearch(query) },
|
||||
onNavigateToRemoteImport = onNavigateToRemoteImport,
|
||||
onNavigateToLocalImport = onNavigateToLocalImport,
|
||||
onNavigateToCache = onNavigateToCache
|
||||
onNavigateToCache = onNavigateToCache,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
|
||||
MainDestination.Explore -> ExploreScreen(
|
||||
@@ -392,7 +419,7 @@ fun MainScreen(
|
||||
if (event == PrefClickEvent.OpenBookCacheManage) {
|
||||
onNavigateToBookCacheManage()
|
||||
} else {
|
||||
viewModel.onPrefClickEvent(context, event)
|
||||
viewModel.onPrefClickEvent(event)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -403,6 +430,44 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BookshelfRailGroupMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
onBeforeSelectGroup: suspend () -> Unit,
|
||||
viewModel: BookshelfViewModel = koinViewModel()
|
||||
) {
|
||||
val groupState by viewModel.groupSelectorState.collectAsStateWithLifecycle()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
RoundDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = onDismissRequest
|
||||
) { dismiss ->
|
||||
groupState.groups.forEachIndexed { groupIndex, group ->
|
||||
RoundDropdownMenuItem(
|
||||
text = group.groupName,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
onBeforeSelectGroup()
|
||||
viewModel.changeGroup(group.groupId)
|
||||
dismiss()
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
if (groupState.selectedGroupIndex == groupIndex) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NavigationIcon(
|
||||
destination: MainDestination,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package io.legado.app.ui.main
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.EventBus
|
||||
@@ -12,16 +9,17 @@ import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
|
||||
import io.legado.app.domain.usecase.WebDavBackupUseCase
|
||||
import io.legado.app.ui.config.mainConfig.MainConfig
|
||||
import io.legado.app.ui.main.my.PrefClickEvent
|
||||
import io.legado.app.ui.widget.dialog.TextDialog
|
||||
import io.legado.app.utils.defaultSharedPreferences
|
||||
import io.legado.app.utils.eventBus.FlowEventBus
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.getPrefString
|
||||
import io.legado.app.utils.sendToClip
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class MainViewModel(
|
||||
application: Application,
|
||||
@@ -49,6 +47,8 @@ class MainViewModel(
|
||||
|
||||
private val _uiState = MutableStateFlow(readMainUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
private val _effects = MutableSharedFlow<MainEffect>(extraBufferCapacity = 8)
|
||||
val effects = _effects.asSharedFlow()
|
||||
|
||||
init {
|
||||
prefs.registerOnSharedPreferenceChangeListener(preferenceListener)
|
||||
@@ -90,35 +90,27 @@ class MainViewModel(
|
||||
MainConfig.navExtended = expanded
|
||||
}
|
||||
|
||||
fun onPrefClickEvent(context: Context, event: PrefClickEvent) {
|
||||
fun onPrefClickEvent(event: PrefClickEvent) {
|
||||
when (event) {
|
||||
is PrefClickEvent.OpenUrl -> context.startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
android.net.Uri.parse(event.url)
|
||||
is PrefClickEvent.OpenUrl -> _effects.tryEmit(MainEffect.OpenUrl(event.url))
|
||||
is PrefClickEvent.CopyUrl -> _effects.tryEmit(MainEffect.CopyUrl(event.url))
|
||||
is PrefClickEvent.ShowMd -> _effects.tryEmit(
|
||||
MainEffect.ShowMarkdown(
|
||||
title = event.title,
|
||||
path = event.path
|
||||
)
|
||||
)
|
||||
|
||||
is PrefClickEvent.CopyUrl -> context.sendToClip(event.url)
|
||||
is PrefClickEvent.ShowMd -> {
|
||||
if (context is AppCompatActivity) {
|
||||
val title = event.title.ifBlank { context.getString(io.legado.app.R.string.help) }
|
||||
val mdText = String(context.assets.open("web/help/md/${event.path}.md").readBytes())
|
||||
context.showDialogFragment(TextDialog(title, mdText, TextDialog.Mode.MD))
|
||||
}
|
||||
}
|
||||
|
||||
is PrefClickEvent.StartActivity -> {
|
||||
context.startActivity(Intent(context, event.destination).apply {
|
||||
event.configTag?.let { putExtra("configTag", it) }
|
||||
})
|
||||
_effects.tryEmit(
|
||||
MainEffect.StartActivity(
|
||||
destination = event.destination,
|
||||
configTag = event.configTag
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
PrefClickEvent.ExitApp -> {
|
||||
if (context is androidx.activity.ComponentActivity) {
|
||||
context.finish()
|
||||
}
|
||||
}
|
||||
PrefClickEvent.ExitApp -> _effects.tryEmit(MainEffect.ExitApp)
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
@@ -126,8 +118,20 @@ class MainViewModel(
|
||||
|
||||
}
|
||||
|
||||
sealed interface MainEffect {
|
||||
data class OpenUrl(val url: String) : MainEffect
|
||||
data class CopyUrl(val url: String) : MainEffect
|
||||
data class ShowMarkdown(val title: String, val path: String) : MainEffect
|
||||
data class StartActivity(
|
||||
val destination: Class<*>,
|
||||
val configTag: String? = null
|
||||
) : MainEffect
|
||||
|
||||
data object ExitApp : MainEffect
|
||||
}
|
||||
|
||||
data class MainUiState(
|
||||
val destinations: List<MainDestination> = MainDestination.mainDestinations,
|
||||
val destinations: ImmutableList<MainDestination> = MainDestination.mainDestinations,
|
||||
val defaultHomePage: String = "bookshelf",
|
||||
val showBottomView: Boolean = true,
|
||||
val useFloatingBottomBar: Boolean = false,
|
||||
@@ -147,7 +151,7 @@ private fun MainViewModel.readMainUiState(): MainUiState {
|
||||
MainDestination.Rss -> showRss
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
return MainUiState(
|
||||
destinations = destinations,
|
||||
defaultHomePage = context.getPrefString(PreferKey.defaultHomePage, "bookshelf")
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package io.legado.app.ui.main.bookshelf
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@@ -389,6 +392,7 @@ fun BookGroupItemList(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun BookItem(
|
||||
book: BookShelfItem,
|
||||
@@ -404,6 +408,9 @@ fun BookItem(
|
||||
coverShadow: Boolean = false,
|
||||
isSearchMode: Boolean = false,
|
||||
searchKey: String = "",
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit)?
|
||||
) {
|
||||
@@ -447,15 +454,28 @@ fun BookItem(
|
||||
}
|
||||
} else null,
|
||||
cover = { modifier ->
|
||||
val coverModifier = with(sharedTransitionScope) {
|
||||
if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) {
|
||||
Modifier.fillMaxWidth().sharedElement(
|
||||
sharedContentState = rememberSharedContentState(sharedCoverKey),
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
}
|
||||
}
|
||||
BookshelfCover(
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
path = book.getDisplayCover(),
|
||||
isUpdating = isUpdating,
|
||||
modifier = modifier,
|
||||
coverModifier = coverModifier,
|
||||
sourceOrigin = book.origin,
|
||||
badgeText = if (layoutMode != 0) unreadText else null,
|
||||
showBadgeDot = BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && book.isNew,
|
||||
leftBottomText = matchedSourceLabel ?: bookTypeLabel
|
||||
leftBottomText = matchedSourceLabel ?: bookTypeLabel,
|
||||
showLoadingPlaceholder = sharedCoverKey == null,
|
||||
)
|
||||
},
|
||||
title = book.name,
|
||||
|
||||
@@ -10,6 +10,7 @@ data class BookShelfItem(
|
||||
val bookUrl: String,
|
||||
val name: String,
|
||||
val author: String,
|
||||
val origin: String,
|
||||
val originName: String,
|
||||
val coverUrl: String?,
|
||||
val customCoverUrl: String?,
|
||||
@@ -44,6 +45,7 @@ data class BookShelfItem(
|
||||
|
||||
fun BookShelfItem.toLightBook() = Book(
|
||||
bookUrl = bookUrl,
|
||||
origin = origin,
|
||||
originName = originName,
|
||||
name = name,
|
||||
author = author,
|
||||
|
||||
@@ -7,6 +7,9 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -77,6 +80,7 @@ import io.legado.app.data.entities.BookGroup
|
||||
import io.legado.app.ui.about.AppLogSheet
|
||||
import io.legado.app.ui.book.info.GroupSelectSheet
|
||||
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||
import io.legado.app.ui.main.bookCoverSharedElementKey
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
@@ -110,7 +114,7 @@ import sh.calvin.reorderable.rememberReorderableLazyGridState
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
|
||||
ExperimentalMaterial3ExpressiveApi::class
|
||||
ExperimentalMaterial3ExpressiveApi::class, ExperimentalSharedTransitionApi::class
|
||||
)
|
||||
@Composable
|
||||
fun BookshelfScreen(
|
||||
@@ -120,7 +124,9 @@ fun BookshelfScreen(
|
||||
onNavigateToSearch: (String) -> Unit,
|
||||
onNavigateToRemoteImport: () -> Unit,
|
||||
onNavigateToLocalImport: () -> Unit,
|
||||
onNavigateToCache: (Long) -> Unit
|
||||
onNavigateToCache: (Long) -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -260,10 +266,6 @@ fun BookshelfScreen(
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.items) {
|
||||
viewModel.pruneSelectionToVisible(uiState.items)
|
||||
}
|
||||
|
||||
BackHandler(enabled = isEditMode) {
|
||||
if (selectedBookUrls.isNotEmpty()) {
|
||||
clearSelection()
|
||||
@@ -722,7 +724,9 @@ fun BookshelfScreen(
|
||||
onSyncDragState = { _, _ -> },
|
||||
onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
|
||||
onBookClick = onBookClick,
|
||||
onBookLongClick = onBookLongClick
|
||||
onBookLongClick = onBookLongClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
} else {
|
||||
HorizontalPager(
|
||||
@@ -741,7 +745,8 @@ fun BookshelfScreen(
|
||||
}
|
||||
val canReorderBooks = isEditMode &&
|
||||
!uiState.isSearch &&
|
||||
group.getRealBookSort() == 3 &&
|
||||
(group.bookSort.takeIf { it >= 0 }
|
||||
?: uiState.bookshelfSort) == 3 &&
|
||||
isSelectedGroup
|
||||
BookshelfPage(
|
||||
paddingValues = paddingValues,
|
||||
@@ -782,7 +787,9 @@ fun BookshelfScreen(
|
||||
},
|
||||
onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
|
||||
onBookClick = onBookClick,
|
||||
onBookLongClick = onBookLongClick
|
||||
onBookLongClick = onBookLongClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1030,7 +1037,9 @@ fun BookshelfPage(
|
||||
onSyncDragState: (books: List<BookShelfItem>, canReorderBooks: Boolean) -> Unit,
|
||||
onGlobalSearch: () -> Unit,
|
||||
onBookClick: (BookShelfItem) -> Unit,
|
||||
onBookLongClick: (BookShelfItem) -> Unit
|
||||
onBookLongClick: (BookShelfItem) -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
) {
|
||||
if (books.isEmpty()) {
|
||||
if (uiState.isSearch) {
|
||||
@@ -1135,6 +1144,9 @@ fun BookshelfPage(
|
||||
coverShadow = BookshelfConfig.bookshelfCoverShadow,
|
||||
isSearchMode = uiState.isSearch,
|
||||
searchKey = uiState.searchKey,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl),
|
||||
onClick = {
|
||||
if (isEditMode) {
|
||||
onToggleBookSelection(book)
|
||||
|
||||
@@ -2,9 +2,15 @@ package io.legado.app.ui.main.bookshelf
|
||||
|
||||
import io.legado.app.data.entities.BookGroup
|
||||
import io.legado.app.ui.widget.components.list.ListUiState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
data class BookshelfGroupSelectorState(
|
||||
val groups: List<BookGroup> = emptyList(),
|
||||
val groups: ImmutableList<BookGroup> = persistentListOf(),
|
||||
val selectedGroupIndex: Int = 0,
|
||||
val selectedGroupId: Long = BookGroup.IdAll
|
||||
)
|
||||
@@ -22,31 +28,33 @@ sealed interface BookshelfOverlay {
|
||||
}
|
||||
|
||||
data class BookshelfUiState(
|
||||
override val items: List<BookShelfItem> = emptyList(),
|
||||
override val selectedIds: Set<Any> = emptySet(),
|
||||
override val items: ImmutableList<BookShelfItem> = persistentListOf(),
|
||||
override val selectedIds: ImmutableSet<Any> = persistentSetOf(),
|
||||
override val searchKey: String = "",
|
||||
override val isSearch: Boolean = false,
|
||||
override val isLoading: Boolean = false,
|
||||
val groups: List<BookGroup> = emptyList(),
|
||||
val allGroups: List<BookGroup> = emptyList(),
|
||||
val groupPreviews: Map<Long, List<BookShelfItem>> = emptyMap(),
|
||||
val groupBookCounts: Map<Long, Int> = emptyMap(),
|
||||
val groups: ImmutableList<BookGroup> = persistentListOf(),
|
||||
val allGroups: ImmutableList<BookGroup> = persistentListOf(),
|
||||
val groupPreviews: ImmutableMap<Long, ImmutableList<BookShelfItem>> = persistentMapOf(),
|
||||
val groupBookCounts: ImmutableMap<Long, Int> = persistentMapOf(),
|
||||
val currentGroupBookCount: Int = 0,
|
||||
val allBooksCount: Int = 0,
|
||||
val selectedGroupIndex: Int = 0,
|
||||
val selectedGroupId: Long = BookGroup.IdAll,
|
||||
val loadingText: String? = null,
|
||||
val upBooksCount: Int = 0,
|
||||
val updatingBooks: Set<String> = emptySet(),
|
||||
val updatingBooks: ImmutableSet<String> = persistentSetOf(),
|
||||
val activeOverlay: BookshelfOverlay? = null,
|
||||
val isEditMode: Boolean = false,
|
||||
val selectedBookUrls: Set<String> = emptySet(),
|
||||
val selectedBookUrls: ImmutableSet<String> = persistentSetOf(),
|
||||
val isInFolderRoot: Boolean = false,
|
||||
val isRefreshing: Boolean = false,
|
||||
val bookGroupStyle: Int = 0,
|
||||
val bookshelfSort: Int = 0,
|
||||
val bookshelfSortOrder: Int = 1,
|
||||
val title: String = "",
|
||||
val subtitle: String? = null,
|
||||
val currentGroupName: String? = null,
|
||||
val draggingBooks: List<BookShelfItem>? = null,
|
||||
val pendingSavedBooks: List<BookShelfItem>? = null
|
||||
val draggingBooks: ImmutableList<BookShelfItem>? = null,
|
||||
val pendingSavedBooks: ImmutableList<BookShelfItem>? = null
|
||||
) : ListUiState<BookShelfItem>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package io.legado.app.ui.main.bookshelf
|
||||
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
@@ -54,11 +53,17 @@ import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.printOnDebug
|
||||
import io.legado.app.utils.readText
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
@@ -71,9 +76,8 @@ import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.isActive
|
||||
@@ -101,7 +105,6 @@ class BookshelfViewModel(
|
||||
private val groupIdFlow = MutableStateFlow(BookshelfConfig.saveTabPosition)
|
||||
private val searchKeyFlow = MutableStateFlow("")
|
||||
private val searchModeFlow = MutableStateFlow(false)
|
||||
private val refreshTrigger = MutableStateFlow(0)
|
||||
private val loadingTextFlow = MutableStateFlow<String?>(null)
|
||||
private val activeOverlayFlow = MutableStateFlow<BookshelfOverlay?>(null)
|
||||
private val isEditModeFlow = MutableStateFlow(false)
|
||||
@@ -112,7 +115,23 @@ class BookshelfViewModel(
|
||||
private val draggingBooksFlow = MutableStateFlow<List<BookShelfItem>?>(null)
|
||||
private val pendingSavedBooksFlow = MutableStateFlow<List<BookShelfItem>?>(null)
|
||||
|
||||
private data class BookshelfSortConfig(
|
||||
val sort: Int,
|
||||
val sortOrder: Int
|
||||
)
|
||||
|
||||
private fun readSortConfig() = BookshelfSortConfig(
|
||||
sort = BookshelfConfig.bookshelfSort,
|
||||
sortOrder = BookshelfConfig.bookshelfSortOrder
|
||||
)
|
||||
|
||||
private val sortConfigFlow: StateFlow<BookshelfSortConfig> = snapshotFlow {
|
||||
readSortConfig()
|
||||
}.distinctUntilChanged()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), readSortConfig())
|
||||
|
||||
// 更新相关
|
||||
private val updateQueueLock = Any()
|
||||
private val waitUpTocBooks = LinkedList<String>()
|
||||
private val onUpTocBooks = ConcurrentHashMap.newKeySet<String>()
|
||||
private val updatingBooksFlow = MutableStateFlow<Set<String>>(emptySet())
|
||||
@@ -144,8 +163,8 @@ class BookshelfViewModel(
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
private data class GroupPreviewState(
|
||||
val previews: Map<Long, List<BookShelfItem>>,
|
||||
val counts: Map<Long, Int>,
|
||||
val previews: ImmutableMap<Long, ImmutableList<BookShelfItem>>,
|
||||
val counts: ImmutableMap<Long, Int>,
|
||||
val allBookCount: Int
|
||||
)
|
||||
|
||||
@@ -154,7 +173,7 @@ class BookshelfViewModel(
|
||||
groupIdFlow
|
||||
) { groups, selectedGroupId ->
|
||||
BookshelfGroupSelectorState(
|
||||
groups = groups,
|
||||
groups = groups.toImmutableList(),
|
||||
selectedGroupIndex = groups.indexOfFirst { it.groupId == selectedGroupId }
|
||||
.coerceAtLeast(0),
|
||||
selectedGroupId = selectedGroupId
|
||||
@@ -163,22 +182,45 @@ class BookshelfViewModel(
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfGroupSelectorState())
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val booksFlow = combine(groupIdFlow, refreshTrigger) { groupId, _ -> groupId }
|
||||
val booksFlow = groupIdFlow
|
||||
.flatMapLatest { groupId ->
|
||||
combine(
|
||||
appDb.bookDao.flowBookShelfByGroup(groupId),
|
||||
groupsFlow
|
||||
) { list, groups ->
|
||||
groupsFlow,
|
||||
sortConfigFlow
|
||||
) { list, groups, sortConfig ->
|
||||
sortBooks(
|
||||
list,
|
||||
groups.find { it.groupId == groupId }
|
||||
groups.find { it.groupId == groupId },
|
||||
sortConfig
|
||||
)
|
||||
}
|
||||
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
||||
|
||||
private val visibleBooksFlow = combine(
|
||||
booksFlow,
|
||||
searchKeyFlow,
|
||||
searchModeFlow
|
||||
) { books, searchKey, isSearchMode ->
|
||||
filterBooks(books, searchKey, isSearchMode)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private val selectedVisibleBookUrlsFlow = combine(
|
||||
selectedBookUrlsFlow,
|
||||
visibleBooksFlow
|
||||
) { selectedBookUrls, visibleBooks ->
|
||||
val visibleBookUrls = visibleBooks.mapTo(hashSetOf()) { it.bookUrl }
|
||||
selectedBookUrls.intersect(visibleBookUrls)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private val groupPreviewsFlow =
|
||||
combine(groupsFlow, allBooksFlow, refreshTrigger, bookGroupStyleFlow) { groups, allBooks, _, bookGroupStyle ->
|
||||
buildGroupPreviewState(groups, allBooks, bookGroupStyle)
|
||||
combine(
|
||||
groupsFlow,
|
||||
allBooksFlow,
|
||||
bookGroupStyleFlow,
|
||||
sortConfigFlow
|
||||
) { groups, allBooks, bookGroupStyle, sortConfig ->
|
||||
buildGroupPreviewState(groups, allBooks, bookGroupStyle, sortConfig)
|
||||
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
||||
|
||||
private val coreInternalStateFlow = combine(
|
||||
@@ -188,23 +230,36 @@ class BookshelfViewModel(
|
||||
loadingTextFlow,
|
||||
updatingBooksFlow
|
||||
) { groupId, searchKey, isSearchMode, loadingText, updatingBooks ->
|
||||
InternalState(groupId, searchKey, isSearchMode, loadingText, updatingBooks, 0)
|
||||
InternalState(
|
||||
groupId = groupId,
|
||||
searchKey = searchKey,
|
||||
isSearchMode = isSearchMode,
|
||||
loadingText = loadingText,
|
||||
updatingBooks = updatingBooks,
|
||||
upBooksCount = 0,
|
||||
sortConfig = readSortConfig()
|
||||
)
|
||||
}
|
||||
|
||||
private val internalStateFlow = combine(
|
||||
coreInternalStateFlow,
|
||||
upBooksCountFlow
|
||||
) { core, upBooksCount ->
|
||||
core.copy(upBooksCount = upBooksCount)
|
||||
upBooksCountFlow,
|
||||
sortConfigFlow
|
||||
) { core, upBooksCount, sortConfig ->
|
||||
core.copy(
|
||||
upBooksCount = upBooksCount,
|
||||
sortConfig = sortConfig
|
||||
)
|
||||
}
|
||||
|
||||
data class InternalState(
|
||||
private data class InternalState(
|
||||
val groupId: Long,
|
||||
val searchKey: String,
|
||||
val isSearchMode: Boolean,
|
||||
val loadingText: String?,
|
||||
val updatingBooks: Set<String>,
|
||||
val upBooksCount: Int
|
||||
val upBooksCount: Int,
|
||||
val sortConfig: BookshelfSortConfig
|
||||
)
|
||||
|
||||
data class BookshelfInteractionState(
|
||||
@@ -221,7 +276,7 @@ class BookshelfViewModel(
|
||||
private val editStateFlow = combine(
|
||||
activeOverlayFlow,
|
||||
isEditModeFlow,
|
||||
selectedBookUrlsFlow,
|
||||
selectedVisibleBookUrlsFlow,
|
||||
isInFolderRootFlow
|
||||
) { activeOverlay, isEditMode, selectedBookUrls, isInFolderRoot ->
|
||||
EditState(activeOverlay, isEditMode, selectedBookUrls, isInFolderRoot)
|
||||
@@ -280,11 +335,7 @@ class BookshelfViewModel(
|
||||
val allGroups = data.allGroups
|
||||
val previews = data.previews
|
||||
val internal = data.internal
|
||||
val filteredBooks = if (!internal.isSearchMode || internal.searchKey.isBlank()) {
|
||||
books
|
||||
} else {
|
||||
books.filter { it.matchesSearchKey(internal.searchKey) }
|
||||
}
|
||||
val filteredBooks = filterBooks(books, internal.searchKey, internal.isSearchMode)
|
||||
val selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId }
|
||||
.coerceAtLeast(0)
|
||||
val currentGroupName = allGroups.firstOrNull { it.groupId == internal.groupId }?.groupName
|
||||
@@ -300,10 +351,10 @@ class BookshelfViewModel(
|
||||
)
|
||||
|
||||
BookshelfUiState(
|
||||
items = filteredBooks,
|
||||
selectedIds = selectedIds,
|
||||
groups = groups,
|
||||
allGroups = allGroups,
|
||||
items = filteredBooks.toImmutableList(),
|
||||
selectedIds = selectedIds.toImmutableSet(),
|
||||
groups = groups.toImmutableList(),
|
||||
allGroups = allGroups.toImmutableList(),
|
||||
groupPreviews = previews.previews,
|
||||
groupBookCounts = previews.counts,
|
||||
currentGroupBookCount = books.size,
|
||||
@@ -315,13 +366,15 @@ class BookshelfViewModel(
|
||||
isLoading = internal.loadingText != null,
|
||||
loadingText = internal.loadingText,
|
||||
upBooksCount = internal.upBooksCount,
|
||||
updatingBooks = internal.updatingBooks,
|
||||
updatingBooks = internal.updatingBooks.toImmutableSet(),
|
||||
activeOverlay = interaction.activeOverlay,
|
||||
isEditMode = interaction.isEditMode,
|
||||
selectedBookUrls = interaction.selectedBookUrls,
|
||||
selectedBookUrls = interaction.selectedBookUrls.toImmutableSet(),
|
||||
isInFolderRoot = interaction.isInFolderRoot,
|
||||
isRefreshing = interaction.isRefreshing,
|
||||
bookGroupStyle = interaction.bookGroupStyle,
|
||||
bookshelfSort = internal.sortConfig.sort,
|
||||
bookshelfSortOrder = internal.sortConfig.sortOrder,
|
||||
title = title,
|
||||
subtitle = if (interaction.isEditMode) {
|
||||
context.getString(R.string.bookshelf_total_count, previews.allBookCount)
|
||||
@@ -329,8 +382,8 @@ class BookshelfViewModel(
|
||||
null
|
||||
},
|
||||
currentGroupName = currentGroupName,
|
||||
draggingBooks = interaction.draggingBooks,
|
||||
pendingSavedBooks = interaction.pendingSavedBooks
|
||||
draggingBooks = interaction.draggingBooks?.toImmutableList(),
|
||||
pendingSavedBooks = interaction.pendingSavedBooks?.toImmutableList()
|
||||
)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfUiState())
|
||||
|
||||
@@ -340,29 +393,19 @@ class BookshelfViewModel(
|
||||
upAllBookToc()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
FlowEventBus.with<Unit>(EventBus.BOOKSHELF_REFRESH).collect {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听排序配置变化,触发刷新
|
||||
viewModelScope.launch {
|
||||
snapshotFlow { BookshelfConfig.bookshelfSort }.collect { refresh() }
|
||||
}
|
||||
viewModelScope.launch {
|
||||
snapshotFlow { BookshelfConfig.bookshelfSortOrder }.collect { refresh() }
|
||||
}
|
||||
viewModelScope.launch {
|
||||
snapshotFlow { BookshelfConfig.bookGroupStyle }
|
||||
.distinctUntilChanged()
|
||||
.collect { style ->
|
||||
updateBookGroupStyle(style)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
snapshotFlow { BookshelfConfig.showWaitUpCount }.collect { postUpBooksCount() }
|
||||
snapshotFlow { BookshelfConfig.showWaitUpCount }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
postUpBooksCount()
|
||||
}
|
||||
}
|
||||
|
||||
if (BookshelfConfig.autoRefreshBook) {
|
||||
@@ -370,9 +413,29 @@ class BookshelfViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortBooks(list: List<BookShelfItem>, group: BookGroup?): List<BookShelfItem> {
|
||||
val bookSort = group?.getRealBookSort() ?: BookshelfConfig.bookshelfSort
|
||||
val isDescending = BookshelfConfig.bookshelfSortOrder == 1
|
||||
private fun filterBooks(
|
||||
books: List<BookShelfItem>,
|
||||
searchKey: String,
|
||||
isSearchMode: Boolean
|
||||
): List<BookShelfItem> {
|
||||
return if (!isSearchMode || searchKey.isBlank()) {
|
||||
books
|
||||
} else {
|
||||
books.filter { it.matchesSearchKey(searchKey) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortBooks(
|
||||
list: List<BookShelfItem>,
|
||||
group: BookGroup?,
|
||||
sortConfig: BookshelfSortConfig
|
||||
): List<BookShelfItem> {
|
||||
val bookSort = if (group != null && group.bookSort >= 0) {
|
||||
group.bookSort
|
||||
} else {
|
||||
sortConfig.sort
|
||||
}
|
||||
val isDescending = sortConfig.sortOrder == 1
|
||||
|
||||
return when (bookSort) {
|
||||
1 -> if (isDescending) list.sortedByDescending { it.latestChapterTime }
|
||||
@@ -435,10 +498,11 @@ class BookshelfViewModel(
|
||||
private fun buildGroupPreviewState(
|
||||
groups: List<BookGroup>,
|
||||
allBooks: List<BookShelfItem>,
|
||||
bookGroupStyle: Int
|
||||
bookGroupStyle: Int,
|
||||
sortConfig: BookshelfSortConfig
|
||||
): GroupPreviewState {
|
||||
if (bookGroupStyle !in 2..3) {
|
||||
return GroupPreviewState(emptyMap(), emptyMap(), allBooks.size)
|
||||
return GroupPreviewState(persistentMapOf(), persistentMapOf(), allBooks.size)
|
||||
}
|
||||
|
||||
val buckets = HashMap<Long, MutableList<BookShelfItem>>(groups.size)
|
||||
@@ -482,14 +546,14 @@ class BookshelfViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val previews = HashMap<Long, List<BookShelfItem>>(groups.size)
|
||||
val previews = HashMap<Long, ImmutableList<BookShelfItem>>(groups.size)
|
||||
val counts = HashMap<Long, Int>(groups.size)
|
||||
groups.forEach { group ->
|
||||
val groupBooks = buckets[group.groupId].orEmpty()
|
||||
counts[group.groupId] = groupBooks.size
|
||||
previews[group.groupId] = buildGroupPreview(sortBooks(groupBooks, group))
|
||||
previews[group.groupId] = buildGroupPreview(sortBooks(groupBooks, group, sortConfig))
|
||||
}
|
||||
return GroupPreviewState(previews, counts, allBooks.size)
|
||||
return GroupPreviewState(previews.toImmutableMap(), counts.toImmutableMap(), allBooks.size)
|
||||
}
|
||||
|
||||
private fun BookShelfItem.isRootGroupBook(sumUserGroupIds: Long): Boolean {
|
||||
@@ -509,13 +573,13 @@ class BookshelfViewModel(
|
||||
(sumUserGroupIds and group) == 0L
|
||||
}
|
||||
|
||||
private fun buildGroupPreview(sortedBooks: List<BookShelfItem>): List<BookShelfItem> {
|
||||
private fun buildGroupPreview(sortedBooks: List<BookShelfItem>): ImmutableList<BookShelfItem> {
|
||||
val booksWithCover = sortedBooks.filter { it.getDisplayCover() != null }
|
||||
return if (booksWithCover.size >= 4) {
|
||||
booksWithCover.take(4)
|
||||
} else {
|
||||
(booksWithCover + sortedBooks.filter { it.getDisplayCover() == null }).take(4)
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun changeGroup(groupId: Long) {
|
||||
@@ -539,10 +603,6 @@ class BookshelfViewModel(
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
refreshTrigger.value++
|
||||
}
|
||||
|
||||
fun showOverlay(overlay: BookshelfOverlay) {
|
||||
activeOverlayFlow.value = overlay
|
||||
}
|
||||
@@ -590,11 +650,6 @@ class BookshelfViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun pruneSelectionToVisible(books: List<BookShelfItem>) {
|
||||
val visibleBookUrls = books.mapTo(hashSetOf()) { it.bookUrl }
|
||||
selectedBookUrlsFlow.value = selectedBookUrlsFlow.value.intersect(visibleBookUrls)
|
||||
}
|
||||
|
||||
fun setInFolderRoot(isInFolderRoot: Boolean) {
|
||||
if (isInFolderRootFlow.value != isInFolderRoot) {
|
||||
isInFolderRootFlow.value = isInFolderRoot
|
||||
@@ -738,55 +793,95 @@ class BookshelfViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun addToWaitUp(books: List<Book>) {
|
||||
books.forEach { book ->
|
||||
if (!waitUpTocBooks.contains(book.bookUrl) && !onUpTocBooks.contains(book.bookUrl)) {
|
||||
waitUpTocBooks.add(book.bookUrl)
|
||||
synchronized(updateQueueLock) {
|
||||
books.forEach { book ->
|
||||
if (!waitUpTocBooks.contains(book.bookUrl) &&
|
||||
!onUpTocBooks.contains(book.bookUrl)
|
||||
) {
|
||||
waitUpTocBooks.add(book.bookUrl)
|
||||
}
|
||||
}
|
||||
if (upTocJob == null && waitUpTocBooks.isNotEmpty()) {
|
||||
startUpTocJobLocked()
|
||||
}
|
||||
}
|
||||
postUpBooksCount()
|
||||
if (upTocJob == null) {
|
||||
startUpTocJob()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startUpTocJob() {
|
||||
postUpBooksCount()
|
||||
private fun startUpTocJobLocked() {
|
||||
upTocJob = viewModelScope.launch(updateDispatcher) {
|
||||
var completedWithoutFlowError = true
|
||||
flow {
|
||||
while (true) {
|
||||
emit(waitUpTocBooks.poll() ?: break)
|
||||
emit(pollWaitUpBookUrl() ?: break)
|
||||
}
|
||||
}.onEachParallel(updateConcurrency) {
|
||||
onUpTocBooks.add(it)
|
||||
updatingBooksFlow.value = onUpTocBooks.toSet()
|
||||
postEvent(EventBus.UP_BOOKSHELF, it)
|
||||
updateToc(it)
|
||||
}.onEach {
|
||||
onUpTocBooks.remove(it)
|
||||
updatingBooksFlow.value = onUpTocBooks.toSet()
|
||||
postEvent(EventBus.UP_BOOKSHELF, it)
|
||||
postUpBooksCount()
|
||||
}.onCompletion {
|
||||
upTocJob = null
|
||||
if (waitUpTocBooks.isNotEmpty()) {
|
||||
startUpTocJob()
|
||||
} else {
|
||||
completeRefreshIfIdle()
|
||||
}
|
||||
if (it == null && cacheBookJob == null && !CacheBookService.isRun) {
|
||||
cacheBook()
|
||||
markBookUpdateStarted(it)
|
||||
try {
|
||||
postEvent(EventBus.UP_BOOKSHELF, it)
|
||||
updateToc(it)
|
||||
} finally {
|
||||
markBookUpdateFinished(it)
|
||||
}
|
||||
}.catch {
|
||||
completedWithoutFlowError = false
|
||||
AppLog.put("更新目录出错\n${it.localizedMessage}", it)
|
||||
}.collect()
|
||||
|
||||
finishUpTocJob(completedWithoutFlowError)
|
||||
}
|
||||
postUpBooksCount()
|
||||
}
|
||||
|
||||
private fun pollWaitUpBookUrl(): String? = synchronized(updateQueueLock) {
|
||||
waitUpTocBooks.poll()
|
||||
}
|
||||
|
||||
private fun markBookUpdateStarted(bookUrl: String) {
|
||||
synchronized(updateQueueLock) {
|
||||
onUpTocBooks.add(bookUrl)
|
||||
}
|
||||
updatingBooksFlow.value = onUpTocBooksSnapshot()
|
||||
}
|
||||
|
||||
private fun markBookUpdateFinished(bookUrl: String) {
|
||||
synchronized(updateQueueLock) {
|
||||
onUpTocBooks.remove(bookUrl)
|
||||
}
|
||||
updatingBooksFlow.value = onUpTocBooksSnapshot()
|
||||
postEvent(EventBus.UP_BOOKSHELF, bookUrl)
|
||||
postUpBooksCount()
|
||||
}
|
||||
|
||||
private fun onUpTocBooksSnapshot(): Set<String> = synchronized(updateQueueLock) {
|
||||
onUpTocBooks.toSet()
|
||||
}
|
||||
|
||||
private fun finishUpTocJob(completedWithoutFlowError: Boolean) {
|
||||
val restarted = synchronized(updateQueueLock) {
|
||||
upTocJob = null
|
||||
if (waitUpTocBooks.isNotEmpty()) {
|
||||
startUpTocJobLocked()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
if (!restarted) {
|
||||
completeRefreshIfIdle()
|
||||
}
|
||||
if (!restarted && completedWithoutFlowError && cacheBookJob == null && !CacheBookService.isRun) {
|
||||
cacheBook()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun completeRefreshIfIdle() {
|
||||
if (upTocJob == null && waitUpTocBooks.isEmpty() && onUpTocBooks.isEmpty()) {
|
||||
val isIdle = synchronized(updateQueueLock) {
|
||||
upTocJob == null && waitUpTocBooks.isEmpty() && onUpTocBooks.isEmpty()
|
||||
}
|
||||
if (isIdle) {
|
||||
isRefreshingFlow.value = false
|
||||
}
|
||||
}
|
||||
@@ -841,8 +936,13 @@ class BookshelfViewModel(
|
||||
}
|
||||
|
||||
private fun postUpBooksCount() {
|
||||
val count =
|
||||
if (BookshelfConfig.showWaitUpCount) waitUpTocBooks.size + onUpTocBooks.size else 0
|
||||
val count = if (BookshelfConfig.showWaitUpCount) {
|
||||
synchronized(updateQueueLock) {
|
||||
waitUpTocBooks.size + onUpTocBooks.size
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
upBooksCountFlow.value = count
|
||||
}
|
||||
|
||||
@@ -868,7 +968,7 @@ class BookshelfViewModel(
|
||||
cacheBookJob = viewModelScope.launch(updateDispatcher) {
|
||||
launch {
|
||||
while (isActive && CacheBook.isRun) {
|
||||
CacheBook.setWorkingState(waitUpTocBooks.isEmpty() && onUpTocBooks.isEmpty())
|
||||
CacheBook.setWorkingState(isUpdateQueueIdle())
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
@@ -876,6 +976,10 @@ class BookshelfViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun isUpdateQueueIdle(): Boolean = synchronized(updateQueueLock) {
|
||||
waitUpTocBooks.isEmpty() && onUpTocBooks.isEmpty()
|
||||
}
|
||||
|
||||
fun addBookByUrl(bookUrls: String) {
|
||||
var successCount = 0
|
||||
loadingTextFlow.value = "添加中..."
|
||||
|
||||
@@ -41,6 +41,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
@@ -89,7 +90,10 @@ fun ExploreScreen(
|
||||
val context = LocalContext.current
|
||||
val activity = context as? AppCompatActivity
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
var sourceToDelete by remember { mutableStateOf<BookSourcePart?>(null) }
|
||||
var sourceToDeleteUrl by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val sourceToDelete = remember(sourceToDeleteUrl, uiState.items) {
|
||||
uiState.items.firstOrNull { it.bookSourceUrl == sourceToDeleteUrl }
|
||||
}
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val exploreKindUseCase: ExploreKindUiUseCase = koinInject()
|
||||
@@ -195,7 +199,13 @@ fun ExploreScreen(
|
||||
) {
|
||||
items(
|
||||
items = uiState.listItems,
|
||||
key = { it.key }
|
||||
key = { it.key },
|
||||
contentType = {
|
||||
when (it) {
|
||||
is ExploreListItem.Header -> "source-header"
|
||||
is ExploreListItem.KindRow -> "kind-row"
|
||||
}
|
||||
}
|
||||
) { listItem ->
|
||||
when (listItem) {
|
||||
is ExploreListItem.Header -> {
|
||||
@@ -225,7 +235,7 @@ fun ExploreScreen(
|
||||
}
|
||||
},
|
||||
onRefresh = { viewModel.refreshExploreKinds(item) },
|
||||
onDelete = { sourceToDelete = item },
|
||||
onDelete = { sourceToDeleteUrl = item.bookSourceUrl },
|
||||
isMiuix = composeEngine
|
||||
)
|
||||
}
|
||||
@@ -297,15 +307,15 @@ fun ExploreScreen(
|
||||
|
||||
AppAlertDialog(
|
||||
data = sourceToDelete,
|
||||
onDismissRequest = { sourceToDelete = null },
|
||||
onDismissRequest = { sourceToDeleteUrl = null },
|
||||
title = stringResource(R.string.sure_del),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = { source ->
|
||||
viewModel.deleteSource(source)
|
||||
sourceToDelete = null
|
||||
sourceToDeleteUrl = null
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { sourceToDelete = null },
|
||||
onDismiss = { sourceToDeleteUrl = null },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,17 @@ import io.legado.app.help.source.getExploreInfoMap
|
||||
import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
|
||||
import io.legado.app.ui.widget.components.explore.calculateExploreKindRows
|
||||
import io.legado.app.ui.widget.components.list.ListUiState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -20,6 +30,10 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -39,7 +53,8 @@ class ExploreViewModel(
|
||||
private val _effects = MutableSharedFlow<ExploreEffect>(extraBufferCapacity = 8)
|
||||
val effects = _effects.asSharedFlow()
|
||||
|
||||
private var exploreJob: Job? = null
|
||||
private val searchKeyFlow = MutableStateFlow("")
|
||||
private val groupFlow = MutableStateFlow("")
|
||||
private var kindsJob: Job? = null
|
||||
|
||||
init {
|
||||
@@ -52,19 +67,19 @@ class ExploreViewModel(
|
||||
exploreRepository.getExploreGroups()
|
||||
.flowOn(IO)
|
||||
.collectLatest { groups ->
|
||||
_uiState.update { it.copy(groups = groups) }
|
||||
_uiState.update { it.copy(groups = groups.toImmutableList()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun search(key: String) {
|
||||
searchKeyFlow.value = key
|
||||
_uiState.update { it.copy(searchKey = key, expandedId = null) }
|
||||
observeExplore()
|
||||
}
|
||||
|
||||
fun setGroup(group: String) {
|
||||
groupFlow.value = group
|
||||
_uiState.update { it.copy(selectedGroup = group, expandedId = null) }
|
||||
observeExplore()
|
||||
}
|
||||
|
||||
fun toggleSearchVisible(visible: Boolean) {
|
||||
@@ -74,17 +89,23 @@ class ExploreViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
private fun observeExplore() {
|
||||
exploreJob?.cancel()
|
||||
exploreJob = viewModelScope.launch {
|
||||
val state = _uiState.value
|
||||
val query = state.searchKey
|
||||
val selectedGroup = state.selectedGroup
|
||||
|
||||
exploreRepository.getExploreSources(query, selectedGroup)
|
||||
viewModelScope.launch {
|
||||
combine(
|
||||
searchKeyFlow
|
||||
.debounce(250)
|
||||
.distinctUntilChanged(),
|
||||
groupFlow
|
||||
) { query, selectedGroup ->
|
||||
query to selectedGroup
|
||||
}
|
||||
.flatMapLatest { (query, selectedGroup) ->
|
||||
exploreRepository.getExploreSources(query, selectedGroup)
|
||||
}
|
||||
.flowOn(IO)
|
||||
.collectLatest { items ->
|
||||
_uiState.update { it.copy(items = items) }
|
||||
_uiState.update { it.copy(items = items.toImmutableList()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,9 +116,9 @@ class ExploreViewModel(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
expandedId = newExpandedId,
|
||||
exploreKinds = emptyList(),
|
||||
kindDisplayNames = emptyMap(),
|
||||
kindValues = emptyMap(),
|
||||
exploreKinds = persistentListOf(),
|
||||
kindDisplayNames = persistentMapOf(),
|
||||
kindValues = persistentMapOf(),
|
||||
loadingKinds = newExpandedId != null
|
||||
)
|
||||
}
|
||||
@@ -125,9 +146,9 @@ class ExploreViewModel(
|
||||
_uiState.update {
|
||||
if (it.expandedId == source.bookSourceUrl) {
|
||||
it.copy(
|
||||
exploreKinds = kinds,
|
||||
kindDisplayNames = displayNames,
|
||||
kindValues = values,
|
||||
exploreKinds = kinds.toImmutableList(),
|
||||
kindDisplayNames = displayNames.toImmutableMap(),
|
||||
kindValues = values.toImmutableMap(),
|
||||
loadingKinds = false
|
||||
)
|
||||
} else it
|
||||
@@ -160,7 +181,7 @@ class ExploreViewModel(
|
||||
|
||||
fun updateKindValue(sourceUrl: String, kind: ExploreKind, value: String) {
|
||||
_uiState.update { state ->
|
||||
state.copy(kindValues = state.kindValues + (kind.title to value))
|
||||
state.copy(kindValues = (state.kindValues + (kind.title to value)).toImmutableMap())
|
||||
}
|
||||
viewModelScope.launch(IO) {
|
||||
getExploreInfoMap(sourceUrl).apply {
|
||||
@@ -181,23 +202,23 @@ class ExploreViewModel(
|
||||
}
|
||||
|
||||
data class ExploreUiState(
|
||||
override val items: List<BookSourcePart> = emptyList(),
|
||||
override val selectedIds: Set<String> = emptySet(),
|
||||
override val items: ImmutableList<BookSourcePart> = persistentListOf(),
|
||||
override val selectedIds: ImmutableSet<String> = persistentSetOf(),
|
||||
override val searchKey: String = "",
|
||||
override val isSearch: Boolean = false,
|
||||
override val isLoading: Boolean = false,
|
||||
val groups: List<String> = emptyList(),
|
||||
val groups: ImmutableList<String> = persistentListOf(),
|
||||
val selectedGroup: String = "",
|
||||
val expandedId: String? = null,
|
||||
val exploreKinds: List<ExploreKind> = emptyList(),
|
||||
val kindDisplayNames: Map<String, String> = emptyMap(),
|
||||
val kindValues: Map<String, String> = emptyMap(),
|
||||
val exploreKinds: ImmutableList<ExploreKind> = persistentListOf(),
|
||||
val kindDisplayNames: ImmutableMap<String, String> = persistentMapOf(),
|
||||
val kindValues: ImmutableMap<String, String> = persistentMapOf(),
|
||||
val loadingKinds: Boolean = false,
|
||||
val listItems: List<ExploreListItem> = emptyList()
|
||||
val listItems: ImmutableList<ExploreListItem> = persistentListOf()
|
||||
) : ListUiState<BookSourcePart>
|
||||
|
||||
private fun buildExploreListItems(state: ExploreUiState): List<ExploreListItem> {
|
||||
if (state.items.isEmpty()) return emptyList()
|
||||
private fun buildExploreListItems(state: ExploreUiState): ImmutableList<ExploreListItem> {
|
||||
if (state.items.isEmpty()) return persistentListOf()
|
||||
val expandedId = state.expandedId
|
||||
val kindRows = if (expandedId != null) {
|
||||
calculateExploreKindRows(state.exploreKinds, 6)
|
||||
@@ -213,13 +234,13 @@ class ExploreViewModel(
|
||||
ExploreListItem.KindRow(
|
||||
sourceUrl = source.bookSourceUrl,
|
||||
rowIndex = index,
|
||||
rowItems = row
|
||||
rowItems = row.toImmutableList()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun buildKindValues(
|
||||
@@ -269,7 +290,7 @@ sealed interface ExploreListItem {
|
||||
data class KindRow(
|
||||
val sourceUrl: String,
|
||||
val rowIndex: Int,
|
||||
val rowItems: List<Pair<ExploreKind, Int>>
|
||||
val rowItems: ImmutableList<Pair<ExploreKind, Int>>
|
||||
) : ExploreListItem {
|
||||
override val key: String = "${sourceUrl}_$rowIndex"
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -79,21 +81,27 @@ fun RssScreen(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
var sourceToDelete by remember { mutableStateOf<RssSource?>(null) }
|
||||
var sourceToDeleteUrl by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val sourceToDelete = remember(sourceToDeleteUrl, uiState.items) {
|
||||
uiState.items.firstOrNull { it.sourceUrl == sourceToDeleteUrl }
|
||||
}
|
||||
val currentContext by rememberUpdatedState(context)
|
||||
val currentOnOpenSort by rememberUpdatedState(onOpenSort)
|
||||
val currentOnOpenRead by rememberUpdatedState(onOpenRead)
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.effects.collectLatest { effect ->
|
||||
when (effect) {
|
||||
is RssEffect.OpenSort -> {
|
||||
onOpenSort(effect.sourceUrl, effect.sortUrl, effect.key)
|
||||
currentOnOpenSort(effect.sourceUrl, effect.sortUrl, effect.key)
|
||||
}
|
||||
|
||||
is RssEffect.OpenRead -> {
|
||||
onOpenRead(effect.title, effect.origin, effect.link, effect.openUrl)
|
||||
currentOnOpenRead(effect.title, effect.origin, effect.link, effect.openUrl)
|
||||
}
|
||||
|
||||
is RssEffect.OpenExternalUrl -> {
|
||||
context.openUrl(effect.url)
|
||||
currentContext.openUrl(effect.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,7 +184,7 @@ fun RssScreen(
|
||||
onClick = { viewModel.openSource(source) },
|
||||
onTop = { viewModel.topSource(source) },
|
||||
onEdit = { edit(source) },
|
||||
onDelete = { sourceToDelete = source },
|
||||
onDelete = { sourceToDeleteUrl = source.sourceUrl },
|
||||
onDisable = { viewModel.disable(source) },
|
||||
onLogin = { login(source) }
|
||||
)
|
||||
@@ -186,15 +194,15 @@ fun RssScreen(
|
||||
|
||||
AppAlertDialog(
|
||||
data = sourceToDelete,
|
||||
onDismissRequest = { sourceToDelete = null },
|
||||
onDismissRequest = { sourceToDeleteUrl = null },
|
||||
title = stringResource(R.string.draw),
|
||||
confirmText = stringResource(R.string.yes),
|
||||
onConfirm = { source ->
|
||||
viewModel.del(source)
|
||||
sourceToDelete = null
|
||||
sourceToDeleteUrl = null
|
||||
},
|
||||
dismissText = stringResource(R.string.no),
|
||||
onDismiss = { sourceToDelete = null }
|
||||
onDismiss = { sourceToDeleteUrl = null }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,17 @@ package io.legado.app.ui.main.rss
|
||||
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.ui.widget.components.list.ListUiState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
data class RssUiState(
|
||||
override val items: List<RssSource> = emptyList(),
|
||||
override val selectedIds: Set<String> = emptySet(),
|
||||
override val items: ImmutableList<RssSource> = persistentListOf(),
|
||||
override val selectedIds: ImmutableSet<String> = persistentSetOf(),
|
||||
override val searchKey: String = "",
|
||||
override val isSearch: Boolean = false,
|
||||
override val isLoading: Boolean = false,
|
||||
val groups: List<String> = emptyList(),
|
||||
val groups: ImmutableList<String> = persistentListOf(),
|
||||
val group: String = ""
|
||||
) : ListUiState<RssSource>
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.data.repository.RssRepository
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -43,7 +44,7 @@ class RssViewModel(
|
||||
rssRepository.getEnabledGroups()
|
||||
.flowOn(IO)
|
||||
.collect { groups ->
|
||||
_uiState.update { state -> state.copy(groups = groups) }
|
||||
_uiState.update { state -> state.copy(groups = groups.toImmutableList()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +62,7 @@ class RssViewModel(
|
||||
}
|
||||
.flowOn(IO)
|
||||
.onEach { sources ->
|
||||
_uiState.update { state -> state.copy(items = sources) }
|
||||
_uiState.update { state -> state.copy(items = sources.toImmutableList()) }
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
@@ -20,21 +20,24 @@ fun BookshelfCover(
|
||||
author: String?,
|
||||
path: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
coverModifier: Modifier = Modifier.fillMaxWidth(),
|
||||
isUpdating: Boolean = false,
|
||||
badgeText: String? = null,
|
||||
showBadgeDot: Boolean = false,
|
||||
leftBottomText: String? = null,
|
||||
sourceOrigin: String? = null,
|
||||
onLoadFinish: (() -> Unit)? = null
|
||||
onLoadFinish: (() -> Unit)? = null,
|
||||
showLoadingPlaceholder: Boolean = true,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
CoilBookCover(
|
||||
name = name,
|
||||
author = author,
|
||||
path = path,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = coverModifier,
|
||||
sourceOrigin = sourceOrigin,
|
||||
onLoadFinish = onLoadFinish
|
||||
onLoadFinish = onLoadFinish,
|
||||
showLoadingPlaceholder = showLoadingPlaceholder,
|
||||
)
|
||||
|
||||
if (!badgeText.isNullOrEmpty()) {
|
||||
|
||||
@@ -41,6 +41,7 @@ import coil.compose.AsyncImage
|
||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.card.NormalCard
|
||||
import kotlinx.coroutines.delay
|
||||
import org.koin.compose.koinInject
|
||||
import io.legado.app.model.BookCover as BookCoverModel
|
||||
|
||||
@@ -52,7 +53,8 @@ fun CoilBookCover(
|
||||
modifier: Modifier = Modifier.width(64.dp),
|
||||
sourceOrigin: String? = null,
|
||||
onLoadFinish: (() -> Unit)? = null,
|
||||
ignoreUseDefaultCover: Boolean = false
|
||||
ignoreUseDefaultCover: Boolean = false,
|
||||
showLoadingPlaceholder: Boolean = true,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isNight = isSystemInDarkTheme()
|
||||
@@ -75,9 +77,23 @@ fun CoilBookCover(
|
||||
|
||||
var isOnlineCoverLoaded by remember(path) { mutableStateOf(false) }
|
||||
var isFinalPathLoaded by remember(path) { mutableStateOf(false) }
|
||||
var isFinalPathLoadFinished by remember(path) { mutableStateOf(false) }
|
||||
var isPlaceholderAllowed by remember(path, showLoadingPlaceholder) {
|
||||
mutableStateOf(showLoadingPlaceholder)
|
||||
}
|
||||
LaunchedEffect(finalPath) {
|
||||
isOnlineCoverLoaded = false
|
||||
isFinalPathLoaded = false
|
||||
isFinalPathLoadFinished = finalPath == null
|
||||
}
|
||||
LaunchedEffect(finalPath, showLoadingPlaceholder) {
|
||||
if (showLoadingPlaceholder) {
|
||||
isPlaceholderAllowed = true
|
||||
} else {
|
||||
isPlaceholderAllowed = false
|
||||
delay(600)
|
||||
isPlaceholderAllowed = true
|
||||
}
|
||||
}
|
||||
|
||||
NormalCard(
|
||||
@@ -133,10 +149,12 @@ fun CoilBookCover(
|
||||
onSuccess = {
|
||||
isOnlineCoverLoaded = true
|
||||
isFinalPathLoaded = true
|
||||
isFinalPathLoadFinished = true
|
||||
onLoadFinish?.invoke()
|
||||
},
|
||||
onError = {
|
||||
isOnlineCoverLoaded = false
|
||||
isFinalPathLoadFinished = true
|
||||
onLoadFinish?.invoke()
|
||||
}
|
||||
)
|
||||
@@ -146,7 +164,9 @@ fun CoilBookCover(
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
||||
val showPlaceholder = isPlaceholderAllowed && (showLoadingPlaceholder || isFinalPathLoadFinished)
|
||||
|
||||
if (!hasCustomDefault && !isOnlineCoverLoaded && showPlaceholder) {
|
||||
Icon(
|
||||
Icons.Default.Book,
|
||||
contentDescription = null,
|
||||
@@ -157,7 +177,7 @@ fun CoilBookCover(
|
||||
)
|
||||
}
|
||||
|
||||
if (!isOnlineCoverLoaded) {
|
||||
if (!isOnlineCoverLoaded && showPlaceholder) {
|
||||
CoverTextOverlay(
|
||||
name = name,
|
||||
author = author,
|
||||
|
||||
Reference in New Issue
Block a user