优化
This commit is contained in:
@@ -19,7 +19,6 @@ interface ExploreRepository {
|
|||||||
fun getExploreGroups(): Flow<List<String>>
|
fun getExploreGroups(): Flow<List<String>>
|
||||||
fun getExploreSources(query: String, selectedGroup: String): Flow<List<BookSourcePart>>
|
fun getExploreSources(query: String, selectedGroup: String): Flow<List<BookSourcePart>>
|
||||||
suspend fun getBookSource(sourceUrl: String): BookSource?
|
suspend fun getBookSource(sourceUrl: String): BookSource?
|
||||||
suspend fun saveSearchBooks(books: List<SearchBook>)
|
|
||||||
suspend fun getSourceExploreKinds(sourceUrl: String): List<ExploreKind>
|
suspend fun getSourceExploreKinds(sourceUrl: String): List<ExploreKind>
|
||||||
suspend fun topSource(bookSource: BookSourcePart)
|
suspend fun topSource(bookSource: BookSourcePart)
|
||||||
suspend fun deleteSource(sourceUrl: String)
|
suspend fun deleteSource(sourceUrl: String)
|
||||||
@@ -91,10 +90,6 @@ class ExploreRepositoryImpl(
|
|||||||
return@withContext source?.exploreKinds() ?: emptyList()
|
return@withContext source?.exploreKinds() ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveSearchBooks(books: List<SearchBook>) {
|
|
||||||
appDb.searchBookDao.insert(*books.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun topSource(bookSource: BookSourcePart) {
|
override suspend fun topSource(bookSource: BookSourcePart) {
|
||||||
val minOrder = appDb.bookSourceDao.minOrder
|
val minOrder = appDb.bookSourceDao.minOrder
|
||||||
appDb.bookSourceDao.upOrder(bookSource.copy(customOrder = minOrder - 1))
|
appDb.bookSourceDao.upOrder(bookSource.copy(customOrder = minOrder - 1))
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
package io.legado.app.domain.usecase
|
package io.legado.app.domain.usecase
|
||||||
|
|
||||||
import io.legado.app.constant.BookType
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
|
import io.legado.app.data.repository.BookRepository
|
||||||
import io.legado.app.help.book.removeType
|
import io.legado.app.help.book.removeType
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class AddToBookshelfUseCase {
|
class AddToBookshelfUseCase(
|
||||||
|
private val bookRepository: BookRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
suspend fun execute(book: SearchBook) = withContext(Dispatchers.IO) {
|
suspend fun execute(book: SearchBook) {
|
||||||
val b = book.toBook()
|
val b = book.toBook()
|
||||||
b.removeType(BookType.notShelf)
|
b.removeType(BookType.notShelf)
|
||||||
if (b.order == 0) {
|
if (b.order == 0) {
|
||||||
b.order = appDb.bookDao.minOrder - 1
|
b.order = bookRepository.getMinOrder() - 1
|
||||||
}
|
}
|
||||||
b.save()
|
bookRepository.insert(b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ sealed interface ExploreShowSheet {
|
|||||||
|
|
||||||
sealed interface ExploreShowIntent {
|
sealed interface ExploreShowIntent {
|
||||||
data class InitData(
|
data class InitData(
|
||||||
val sourceUrl: String?,
|
val sourceUrl: String,
|
||||||
val exploreUrl: String?,
|
val exploreUrl: String?,
|
||||||
) : ExploreShowIntent
|
) : ExploreShowIntent
|
||||||
|
|
||||||
|
|||||||
@@ -135,30 +135,38 @@ fun ExploreShowScreen(
|
|||||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
val isGridMode = state.layoutState == 1
|
val isGridMode = state.layoutState == 1
|
||||||
val hazeState = remember { HazeState() }
|
val hazeState = remember { HazeState() }
|
||||||
val shouldLoadMoreList = remember {
|
val showLoadMoreFooter = !state.isRefreshing &&
|
||||||
|
(state.isLoading || state.errorMsg != null || state.isEnd)
|
||||||
|
val canLoadMore = state.books.isNotEmpty() &&
|
||||||
|
!state.isLoading &&
|
||||||
|
!state.isRefreshing &&
|
||||||
|
!state.isEnd &&
|
||||||
|
state.errorMsg == null
|
||||||
|
val shouldLoadMore by remember(isGridMode) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val total = listState.layoutInfo.totalItemsCount
|
if (isGridMode) {
|
||||||
val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
val total = gridState.layoutInfo.totalItemsCount
|
||||||
total > 0 && last >= total - 3
|
val last = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||||
|
total > 0 && last >= total - 1
|
||||||
|
} else {
|
||||||
|
val total = listState.layoutInfo.totalItemsCount
|
||||||
|
val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||||
|
total > 0 && last >= total - 3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val shouldLoadMoreGrid = remember {
|
LaunchedEffect(
|
||||||
derivedStateOf {
|
shouldLoadMore,
|
||||||
val total = gridState.layoutInfo.totalItemsCount
|
isGridMode,
|
||||||
val last = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
canLoadMore,
|
||||||
total > 0 && last >= total - 1
|
state.books.size,
|
||||||
|
) {
|
||||||
|
if (shouldLoadMore && canLoadMore) {
|
||||||
|
viewModel.onIntent(ExploreShowIntent.LoadMore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(shouldLoadMoreList.value, isGridMode) {
|
|
||||||
if (!isGridMode && shouldLoadMoreList.value) viewModel.onIntent(ExploreShowIntent.LoadMore)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(shouldLoadMoreGrid.value, isGridMode) {
|
|
||||||
if (isGridMode && shouldLoadMoreGrid.value) viewModel.onIntent(ExploreShowIntent.LoadMore)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(isGridMode) {
|
LaunchedEffect(isGridMode) {
|
||||||
if (isGridMode) {
|
if (isGridMode) {
|
||||||
if (listState.firstVisibleItemIndex > 0) {
|
if (listState.firstVisibleItemIndex > 0) {
|
||||||
@@ -325,14 +333,14 @@ fun ExploreShowScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
if (showLoadMoreFooter) {
|
||||||
LoadMoreFooter(
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
isLoading = state.isLoading && !state.isRefreshing,
|
ExploreShowLoadMoreFooter(
|
||||||
errorMsg = state.errorMsg,
|
state = state,
|
||||||
isEnd = state.isEnd,
|
onRetry = { viewModel.onIntent(ExploreShowIntent.LoadMore) },
|
||||||
onRetry = { viewModel.onIntent(ExploreShowIntent.LoadMore) },
|
onLoadMore = { viewModel.onIntent(ExploreShowIntent.ForceLoadNext) },
|
||||||
onLoadMore = { viewModel.onIntent(ExploreShowIntent.ForceLoadNext) }
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -376,14 +384,14 @@ fun ExploreShowScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
if (showLoadMoreFooter) {
|
||||||
LoadMoreFooter(
|
item {
|
||||||
isLoading = state.isLoading && !state.isRefreshing,
|
ExploreShowLoadMoreFooter(
|
||||||
errorMsg = state.errorMsg,
|
state = state,
|
||||||
isEnd = state.isEnd,
|
onRetry = { viewModel.onIntent(ExploreShowIntent.LoadMore) },
|
||||||
onRetry = { viewModel.onIntent(ExploreShowIntent.LoadMore) },
|
onLoadMore = { viewModel.onIntent(ExploreShowIntent.ForceLoadNext) },
|
||||||
onLoadMore = { viewModel.onIntent(ExploreShowIntent.ForceLoadNext) }
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,6 +418,22 @@ fun ExploreShowScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExploreShowLoadMoreFooter(
|
||||||
|
state: ExploreShowUiState,
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
onLoadMore: () -> Unit,
|
||||||
|
) {
|
||||||
|
LoadMoreFooter(
|
||||||
|
isLoading = state.isLoading,
|
||||||
|
errorMsg = state.errorMsg,
|
||||||
|
isEnd = state.isEnd,
|
||||||
|
onRetry = onRetry,
|
||||||
|
onLoadMore = onLoadMore,
|
||||||
|
autoLoad = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ExploreBookItem(
|
fun ExploreBookItem(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package io.legado.app.ui.book.explore
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.data.entities.BookSource
|
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.data.entities.rule.ExploreKind
|
import io.legado.app.data.entities.rule.ExploreKind
|
||||||
import io.legado.app.data.repository.ExploreRepository
|
import io.legado.app.data.repository.ExploreRepository
|
||||||
@@ -25,6 +24,25 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
private data class ExploreShowLoadState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val isRefreshing: Boolean = false,
|
||||||
|
val isEnd: Boolean = false,
|
||||||
|
val errorMsg: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class ExploreShowKindState(
|
||||||
|
val kinds: List<ExploreKind> = emptyList(),
|
||||||
|
val selectedKindTitle: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class ExploreShowDisplayState(
|
||||||
|
val sourceUrl: String? = null,
|
||||||
|
val layoutState: Int,
|
||||||
|
val gridCount: Int,
|
||||||
|
val sheet: ExploreShowSheet = ExploreShowSheet.None,
|
||||||
|
)
|
||||||
|
|
||||||
class ExploreShowViewModel(
|
class ExploreShowViewModel(
|
||||||
private val repository: ExploreRepository,
|
private val repository: ExploreRepository,
|
||||||
private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase,
|
private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase,
|
||||||
@@ -35,15 +53,18 @@ class ExploreShowViewModel(
|
|||||||
|
|
||||||
private val _rawBooks = MutableStateFlow<List<SearchBook>>(emptyList())
|
private val _rawBooks = MutableStateFlow<List<SearchBook>>(emptyList())
|
||||||
private val _bookshelf = MutableStateFlow<Set<BookShelfKey>>(emptySet())
|
private val _bookshelf = MutableStateFlow<Set<BookShelfKey>>(emptySet())
|
||||||
private val _isLoading = MutableStateFlow(false)
|
private val _loadState = MutableStateFlow(ExploreShowLoadState())
|
||||||
private val _isEnd = MutableStateFlow(false)
|
private val _kindState = MutableStateFlow(ExploreShowKindState())
|
||||||
private val _errorMsg = MutableStateFlow<String?>(null)
|
private val _displayState = MutableStateFlow(
|
||||||
private val _kinds = MutableStateFlow<List<ExploreKind>>(emptyList())
|
ExploreShowDisplayState(
|
||||||
private val _selectedKindTitle = MutableStateFlow<String?>(null)
|
layoutState = AppConfig.exploreLayoutState,
|
||||||
|
gridCount = appCtx.exploreLayoutGrid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
private var bookSource: BookSource? = null
|
|
||||||
private var sourceUrl: String? = null
|
private var sourceUrl: String? = null
|
||||||
private var exploreUrl: String? = null
|
private var exploreUrl: String? = null
|
||||||
|
private var initialized = false
|
||||||
private var page = 1
|
private var page = 1
|
||||||
private var autoPageCount = 0
|
private var autoPageCount = 0
|
||||||
|
|
||||||
@@ -54,8 +75,8 @@ class ExploreShowViewModel(
|
|||||||
|
|
||||||
private val _uiState = MutableStateFlow(
|
private val _uiState = MutableStateFlow(
|
||||||
ExploreShowUiState(
|
ExploreShowUiState(
|
||||||
layoutState = AppConfig.exploreLayoutState,
|
layoutState = _displayState.value.layoutState,
|
||||||
gridCount = appCtx.exploreLayoutGrid,
|
gridCount = _displayState.value.gridCount,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
@@ -77,8 +98,8 @@ class ExploreShowViewModel(
|
|||||||
is ExploreShowIntent.SwitchKind -> switchKind(intent.kind)
|
is ExploreShowIntent.SwitchKind -> switchKind(intent.kind)
|
||||||
ExploreShowIntent.ToggleLayout -> toggleLayout()
|
ExploreShowIntent.ToggleLayout -> toggleLayout()
|
||||||
is ExploreShowIntent.SaveGridCount -> saveGridCount(intent.count)
|
is ExploreShowIntent.SaveGridCount -> saveGridCount(intent.count)
|
||||||
is ExploreShowIntent.ShowSheet -> _uiState.update { it.copy(sheet = intent.sheet) }
|
is ExploreShowIntent.ShowSheet -> _displayState.update { it.copy(sheet = intent.sheet) }
|
||||||
ExploreShowIntent.DismissSheet -> _uiState.update { it.copy(sheet = ExploreShowSheet.None) }
|
ExploreShowIntent.DismissSheet -> _displayState.update { it.copy(sheet = ExploreShowSheet.None) }
|
||||||
is ExploreShowIntent.OpenBook -> emitEffect(
|
is ExploreShowIntent.OpenBook -> emitEffect(
|
||||||
ExploreShowEffect.OpenBookInfo(
|
ExploreShowEffect.OpenBookInfo(
|
||||||
name = intent.book.name,
|
name = intent.book.name,
|
||||||
@@ -111,21 +132,10 @@ class ExploreShowViewModel(
|
|||||||
combine(
|
combine(
|
||||||
_rawBooks,
|
_rawBooks,
|
||||||
_bookshelf,
|
_bookshelf,
|
||||||
_isLoading,
|
_loadState,
|
||||||
_isEnd,
|
_kindState,
|
||||||
_errorMsg,
|
_displayState,
|
||||||
_kinds,
|
) { rawBooks, bookshelf, loadState, kindState, displayState ->
|
||||||
_selectedKindTitle,
|
|
||||||
) { values ->
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val rawBooks = values[0] as List<SearchBook>
|
|
||||||
val bookshelf = values[1] as Set<BookShelfKey>
|
|
||||||
val isLoading = values[2] as Boolean
|
|
||||||
val isEnd = values[3] as Boolean
|
|
||||||
val errorMsg = values[4] as String?
|
|
||||||
val kinds = values[5] as List<ExploreKind>
|
|
||||||
val selectedKindTitle = values[6] as String?
|
|
||||||
|
|
||||||
val books = rawBooks.map { item ->
|
val books = rawBooks.map { item ->
|
||||||
ExploreBookItemUi(
|
ExploreBookItemUi(
|
||||||
book = item,
|
book = item,
|
||||||
@@ -139,154 +149,154 @@ class ExploreShowViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExploreShowUiState(
|
ExploreShowUiState(
|
||||||
sourceUrl = sourceUrl,
|
sourceUrl = displayState.sourceUrl,
|
||||||
books = books.toImmutableList(),
|
books = books.toImmutableList(),
|
||||||
kinds = kinds.toImmutableList(),
|
kinds = kindState.kinds.toImmutableList(),
|
||||||
selectedKindTitle = selectedKindTitle,
|
selectedKindTitle = kindState.selectedKindTitle,
|
||||||
layoutState = _uiState.value.layoutState,
|
layoutState = displayState.layoutState,
|
||||||
gridCount = _uiState.value.gridCount,
|
gridCount = displayState.gridCount,
|
||||||
isLoading = isLoading,
|
isLoading = loadState.isLoading,
|
||||||
isRefreshing = isLoading && page == 1,
|
isRefreshing = loadState.isRefreshing,
|
||||||
isEnd = isEnd,
|
isEnd = loadState.isEnd,
|
||||||
errorMsg = errorMsg,
|
errorMsg = loadState.errorMsg,
|
||||||
sheet = _uiState.value.sheet,
|
sheet = displayState.sheet,
|
||||||
)
|
)
|
||||||
}.collect { newState ->
|
}.collect { newState ->
|
||||||
val oldState = _uiState.value
|
|
||||||
_uiState.value = newState
|
_uiState.value = newState
|
||||||
|
|
||||||
if (newState.books.isEmpty() && _rawBooks.value.isNotEmpty() && !newState.isEnd && !newState.isLoading) {
|
|
||||||
loadMore()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initData(incomingSourceUrl: String?, incomingExploreUrl: String?) {
|
private fun initData(incomingSourceUrl: String, incomingExploreUrl: String?) {
|
||||||
if (sourceUrl == incomingSourceUrl && exploreUrl == incomingExploreUrl && bookSource != null) {
|
if (initialized && sourceUrl == incomingSourceUrl && exploreUrl == incomingExploreUrl) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
initialized = true
|
||||||
sourceUrl = incomingSourceUrl
|
sourceUrl = incomingSourceUrl
|
||||||
exploreUrl = incomingExploreUrl
|
exploreUrl = incomingExploreUrl
|
||||||
page = 1
|
page = 1
|
||||||
autoPageCount = 0
|
autoPageCount = 0
|
||||||
bookSource = null
|
|
||||||
_rawBooks.value = emptyList()
|
_rawBooks.value = emptyList()
|
||||||
_isEnd.value = false
|
_loadState.value = ExploreShowLoadState()
|
||||||
_errorMsg.value = null
|
_kindState.value = ExploreShowKindState()
|
||||||
_selectedKindTitle.value = null
|
_displayState.update {
|
||||||
_kinds.value = emptyList()
|
it.copy(
|
||||||
|
sourceUrl = incomingSourceUrl,
|
||||||
viewModelScope.launch {
|
sheet = ExploreShowSheet.None,
|
||||||
if (bookSource == null && incomingSourceUrl != null) {
|
)
|
||||||
bookSource = repository.getBookSource(incomingSourceUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exploreUrl == null && bookSource != null) {
|
|
||||||
loadKinds(incomingSourceUrl!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMore(isRefresh = true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (incomingExploreUrl == null) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
loadKinds(incomingSourceUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMore(isRefresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadKinds(sourceUrl: String) {
|
private suspend fun loadKinds(sourceUrl: String) {
|
||||||
viewModelScope.launch {
|
_kindState.update { it.copy(kinds = repository.getSourceExploreKinds(sourceUrl)) }
|
||||||
_kinds.value = repository.getSourceExploreKinds(sourceUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchKind(kind: ExploreKind) {
|
private fun switchKind(kind: ExploreKind) {
|
||||||
_selectedKindTitle.value = kind.title
|
_kindState.update { it.copy(selectedKindTitle = kind.title) }
|
||||||
exploreUrl = kind.url
|
exploreUrl = kind.url
|
||||||
_isEnd.value = false
|
_loadState.update { it.copy(isEnd = false) }
|
||||||
autoPageCount = 0
|
autoPageCount = 0
|
||||||
loadMore(isRefresh = true)
|
loadMore(isRefresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleLayout() {
|
private fun toggleLayout() {
|
||||||
_uiState.update {
|
_displayState.update {
|
||||||
val newState = if (it.layoutState == 0) 1 else 0
|
val layoutState = if (it.layoutState == 0) 1 else 0
|
||||||
AppConfig.exploreLayoutState = newState
|
AppConfig.exploreLayoutState = layoutState
|
||||||
it.copy(layoutState = newState)
|
it.copy(layoutState = layoutState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveGridCount(count: Int) {
|
private fun saveGridCount(count: Int) {
|
||||||
appCtx.exploreLayoutGrid = count
|
appCtx.exploreLayoutGrid = count
|
||||||
_uiState.update { it.copy(gridCount = count) }
|
_displayState.update { it.copy(gridCount = count) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadMore(isRefresh: Boolean = false, forceLoad: Boolean = false) {
|
private fun loadMore(isRefresh: Boolean = false, forceLoad: Boolean = false) {
|
||||||
val source = bookSource
|
val source = sourceUrl
|
||||||
val url = exploreUrl ?: source?.exploreUrl
|
val url = exploreUrl
|
||||||
if (source == null || url == null || _isLoading.value || (_isEnd.value && !isRefresh && !forceLoad)) return
|
val loadState = _loadState.value
|
||||||
|
if (source == null || loadState.isLoading || (loadState.isEnd && !isRefresh && !forceLoad)) return
|
||||||
|
|
||||||
|
_loadState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = true,
|
||||||
|
isRefreshing = isRefresh,
|
||||||
|
isEnd = if (isRefresh || forceLoad) false else it.isEnd,
|
||||||
|
errorMsg = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isLoading.value = true
|
|
||||||
_errorMsg.value = null
|
|
||||||
|
|
||||||
if (isRefresh) {
|
if (isRefresh) {
|
||||||
page = 1
|
page = 1
|
||||||
_isEnd.value = false
|
|
||||||
autoPageCount = 0
|
autoPageCount = 0
|
||||||
_rawBooks.value = emptyList()
|
_rawBooks.value = emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceLoad) {
|
if (forceLoad) {
|
||||||
autoPageCount = 0
|
autoPageCount = 0
|
||||||
_isEnd.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchPage(source, url)
|
fetchPage(source, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchPage(source: BookSource, url: String) {
|
private suspend fun fetchPage(sourceUrl: String, url: String?) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
exploreBooksUseCase.execute(source.bookSourceUrl, url, args = null, page)
|
exploreBooksUseCase.execute(sourceUrl, url, args = null, page)
|
||||||
}.onSuccess { result ->
|
}.onSuccess { result ->
|
||||||
if (result.books.isEmpty()) {
|
val currentList = _rawBooks.value
|
||||||
page++
|
val existingUrls = currentList.map { it.bookUrl }.toSet()
|
||||||
autoPageCount++
|
val uniqueNewBooks = result.books
|
||||||
if (autoPageCount >= MAX_AUTO_PAGES) {
|
.filter { it.bookUrl !in existingUrls }
|
||||||
_isEnd.value = true
|
.distinctBy { it.bookUrl }
|
||||||
_isLoading.value = false
|
|
||||||
} else {
|
if (result.books.isNotEmpty()) {
|
||||||
delay(AUTO_PAGE_DELAY_MS)
|
|
||||||
fetchPage(source, url)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
saveSearchBooksUseCase.save(result.books)
|
saveSearchBooksUseCase.save(result.books)
|
||||||
|
|
||||||
val currentList = _rawBooks.value
|
|
||||||
val existingUrls = currentList.map { it.bookUrl }.toSet()
|
|
||||||
|
|
||||||
val uniqueNewBooks = result.books
|
|
||||||
.filter { it.bookUrl !in existingUrls }
|
|
||||||
.distinctBy { it.bookUrl }
|
|
||||||
|
|
||||||
if (uniqueNewBooks.isEmpty()) {
|
|
||||||
page++
|
|
||||||
autoPageCount++
|
|
||||||
if (autoPageCount >= MAX_AUTO_PAGES) {
|
|
||||||
_isEnd.value = true
|
|
||||||
_isLoading.value = false
|
|
||||||
} else {
|
|
||||||
delay(AUTO_PAGE_DELAY_MS)
|
|
||||||
fetchPage(source, url)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_rawBooks.value = currentList + uniqueNewBooks
|
|
||||||
page++
|
|
||||||
autoPageCount = 0
|
|
||||||
_isEnd.value = false
|
|
||||||
_isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.onFailure {
|
|
||||||
_errorMsg.value = it.stackTraceStr
|
if (uniqueNewBooks.isEmpty()) {
|
||||||
_isLoading.value = false
|
fetchNextAutoPageOrFinish(sourceUrl, url)
|
||||||
|
} else {
|
||||||
|
_rawBooks.value = currentList + uniqueNewBooks
|
||||||
|
page++
|
||||||
|
autoPageCount = 0
|
||||||
|
_loadState.update { it.copy(isEnd = false) }
|
||||||
|
finishLoading()
|
||||||
|
}
|
||||||
|
}.onFailure { throwable ->
|
||||||
|
_loadState.update { it.copy(errorMsg = throwable.stackTraceStr) }
|
||||||
|
finishLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchNextAutoPageOrFinish(sourceUrl: String, url: String?) {
|
||||||
|
page++
|
||||||
|
autoPageCount++
|
||||||
|
if (autoPageCount >= MAX_AUTO_PAGES) {
|
||||||
|
_loadState.update { it.copy(isEnd = true) }
|
||||||
|
finishLoading()
|
||||||
|
} else {
|
||||||
|
delay(AUTO_PAGE_DELAY_MS)
|
||||||
|
fetchPage(sourceUrl, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishLoading() {
|
||||||
|
_loadState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isRefreshing = false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -899,6 +899,7 @@ private fun ExpandedSourceSheet(
|
|||||||
title = sourceName,
|
title = sourceName,
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
val showLoadMoreFooter = isLoading || errorMsg != null || isEnd
|
||||||
|
|
||||||
val shouldLoadMore by remember {
|
val shouldLoadMore by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
@@ -935,13 +936,16 @@ private fun ExpandedSourceSheet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
if (showLoadMoreFooter) {
|
||||||
LoadMoreFooter(
|
item {
|
||||||
isLoading = isLoading,
|
LoadMoreFooter(
|
||||||
errorMsg = errorMsg,
|
isLoading = isLoading,
|
||||||
isEnd = isEnd,
|
errorMsg = errorMsg,
|
||||||
onRetry = onLoadMore,
|
isEnd = isEnd,
|
||||||
)
|
onRetry = onLoadMore,
|
||||||
|
autoLoad = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ fun RssArticlesPage(
|
|||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
topPadding = paddingValues.calculateTopPadding()
|
topPadding = paddingValues.calculateTopPadding()
|
||||||
) {
|
) {
|
||||||
|
val showLoadMoreFooter = !loadState.isRefreshing &&
|
||||||
|
(loadState.isLoadingMore || loadState.errorMessage != null || !loadState.hasMore)
|
||||||
when (layout) {
|
when (layout) {
|
||||||
RssArticleLayout.List, RssArticleLayout.LargeCard -> {
|
RssArticleLayout.List, RssArticleLayout.LargeCard -> {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
@@ -160,13 +162,16 @@ fun RssArticlesPage(
|
|||||||
onClick = onRead
|
onClick = onRead
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
if (showLoadMoreFooter) {
|
||||||
LoadMoreFooter(
|
item {
|
||||||
isLoading = loadState.isRefreshing || loadState.isLoadingMore,
|
LoadMoreFooter(
|
||||||
errorMsg = loadState.errorMessage,
|
isLoading = loadState.isLoadingMore,
|
||||||
isEnd = !loadState.hasMore,
|
errorMsg = loadState.errorMessage,
|
||||||
onRetry = { rssSource?.let(viewModel::loadMore) }
|
isEnd = !loadState.hasMore,
|
||||||
)
|
onRetry = { rssSource?.let(viewModel::loadMore) },
|
||||||
|
autoLoad = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,13 +202,16 @@ fun RssArticlesPage(
|
|||||||
onClick = onRead
|
onClick = onRead
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
if (showLoadMoreFooter) {
|
||||||
LoadMoreFooter(
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
isLoading = loadState.isRefreshing || loadState.isLoadingMore,
|
LoadMoreFooter(
|
||||||
errorMsg = loadState.errorMessage,
|
isLoading = loadState.isLoadingMore,
|
||||||
isEnd = !loadState.hasMore,
|
errorMsg = loadState.errorMessage,
|
||||||
onRetry = { rssSource?.let(viewModel::loadMore) }
|
isEnd = !loadState.hasMore,
|
||||||
)
|
onRetry = { rssSource?.let(viewModel::loadMore) },
|
||||||
|
autoLoad = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,13 +242,16 @@ fun RssArticlesPage(
|
|||||||
onClick = onRead
|
onClick = onRead
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item(span = StaggeredGridItemSpan.FullLine) {
|
if (showLoadMoreFooter) {
|
||||||
LoadMoreFooter(
|
item(span = StaggeredGridItemSpan.FullLine) {
|
||||||
isLoading = loadState.isRefreshing || loadState.isLoadingMore,
|
LoadMoreFooter(
|
||||||
errorMsg = loadState.errorMessage,
|
isLoading = loadState.isLoadingMore,
|
||||||
isEnd = !loadState.hasMore,
|
errorMsg = loadState.errorMessage,
|
||||||
onRetry = { rssSource?.let(viewModel::loadMore) }
|
isEnd = !loadState.hasMore,
|
||||||
)
|
onRetry = { rssSource?.let(viewModel::loadMore) },
|
||||||
|
autoLoad = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,16 +44,23 @@ fun LoadMoreFooter(
|
|||||||
isEnd: Boolean,
|
isEnd: Boolean,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onLoadMore: (() -> Unit)? = null,
|
onLoadMore: (() -> Unit)? = null,
|
||||||
|
autoLoad: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var showFullError by remember { mutableStateOf<String?>(null) }
|
var showFullError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(isLoading, errorMsg, isEnd) {
|
if (autoLoad) {
|
||||||
if (!isLoading && errorMsg == null && !isEnd) {
|
LaunchedEffect(isLoading, errorMsg, isEnd) {
|
||||||
onRetry()
|
if (!isLoading && errorMsg == null && !isEnd) {
|
||||||
|
onRetry()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (autoLoad && !isLoading && errorMsg == null && !isEnd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
AppAlertDialog(
|
AppAlertDialog(
|
||||||
data = showFullError,
|
data = showFullError,
|
||||||
onDismissRequest = { showFullError = null },
|
onDismissRequest = { showFullError = null },
|
||||||
@@ -270,39 +277,41 @@ fun LoadMoreFooter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
GlassCard(
|
if (!autoLoad) {
|
||||||
modifier = Modifier
|
GlassCard(
|
||||||
.fillMaxWidth(),
|
modifier = Modifier
|
||||||
containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
.fillMaxWidth(),
|
||||||
onClick = onRetry
|
containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
||||||
) {
|
onClick = onRetry
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
all = 16.dp
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
all = 16.dp
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
AppIcon(
|
AppIcon(
|
||||||
imageVector = Icons.Outlined.Info,
|
imageVector = Icons.Outlined.Info,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = LegadoTheme.colorScheme.onSurface
|
tint = LegadoTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
AppText(
|
AppText(
|
||||||
text = "加载更多",
|
text = "加载更多",
|
||||||
color = LegadoTheme.colorScheme.onSurface,
|
color = LegadoTheme.colorScheme.onSurface,
|
||||||
style = LegadoTheme.typography.bodySmall,
|
style = LegadoTheme.typography.bodySmall,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user