[优化] 正确处理发现页的一些情况

This commit is contained in:
HapeLee committed 2025-12-06 15:08:13 +08:00
1 parent 374da29a8b
commit 7a1ade76e2
6 files changed
+206 -104

No files matched your search

@@ -204,6 +204,10 @@ object PreferKey {
const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable" const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable"
const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape" const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"
const val bookshelfLayoutGridPortrait = "bookshelfLayoutGridPortrait" const val bookshelfLayoutGridPortrait = "bookshelfLayoutGridPortrait"
const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape"
const val exploreLayoutGridPortrait = "exploreLayoutGridPortrait"
const val tabletInterface = "tabletInterface" const val tabletInterface = "tabletInterface"
const val pureBlack = "pure_black" const val pureBlack = "pure_black"
const val labelVisibilityMode = "labelVisibilityMode" const val labelVisibilityMode = "labelVisibilityMode"
@@ -37,7 +37,7 @@ class ExploreRepositoryImpl(
} }
override suspend fun exploreBook(source: BookSource, url: String, page: Int): Result<List<SearchBook>> { override suspend fun exploreBook(source: BookSource, url: String, page: Int): Result<List<SearchBook>> {
return withContext(Dispatchers.IO) { return withContext(IO) {
try { try {
val books = WebBook.exploreBookSuspend(source, url, page) val books = WebBook.exploreBookSuspend(source, url, page)
Result.success(books) Result.success(books)
@@ -237,6 +237,18 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
appCtx.putPrefInt(PreferKey.bookshelfLayoutGridPortrait, value) appCtx.putPrefInt(PreferKey.bookshelfLayoutGridPortrait, value)
} }
var exploreLayoutGridLandscape: Int
get() = appCtx.getPrefInt(PreferKey.exploreLayoutGridLandscape, 7)
set(value) {
appCtx.putPrefInt(PreferKey.exploreLayoutGridLandscape, value)
}
var exploreLayoutGridPortrait: Int
get() = appCtx.getPrefInt(PreferKey.exploreLayoutGridPortrait, 3)
set(value) {
appCtx.putPrefInt(PreferKey.exploreLayoutGridPortrait, value)
}
var bookshelfLayoutGridLandscape: Int var bookshelfLayoutGridLandscape: Int
get() = appCtx.getPrefInt(PreferKey.bookshelfLayoutGridLandscape, 7) get() = appCtx.getPrefInt(PreferKey.bookshelfLayoutGridLandscape, 7)
set(value) { set(value) {
@@ -3,8 +3,12 @@ package io.legado.app.ui.book.explore
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -40,6 +44,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.Grid4x4
import androidx.compose.material.icons.filled.GridView import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.FilterAlt import androidx.compose.material.icons.outlined.FilterAlt
@@ -53,8 +58,10 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetValue import androidx.compose.material3.SheetValue
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
@@ -74,7 +81,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -87,7 +93,7 @@ import io.legado.app.ui.widget.components.AnimatedTextButton
import io.legado.app.ui.widget.components.AnimatedTextLine import io.legado.app.ui.widget.components.AnimatedTextLine
import io.legado.app.ui.widget.components.Cover import io.legado.app.ui.widget.components.Cover
import io.legado.app.ui.widget.components.EmptyMessageView import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.utils.bookshelfLayoutGrid import kotlinx.coroutines.delay
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight") @SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight")
@@ -106,7 +112,7 @@ fun ExploreShowScreen(
} }
val books by viewModel.uiBooks.collectAsState() val books by viewModel.uiBooks.collectAsState()
val isBookEnd by viewModel.isRawDataEmptyAndEnd.collectAsState() val isBookEnd by viewModel.isEnd.collectAsState()
val shouldTriggerAutoLoad by viewModel.shouldTriggerAutoLoad.collectAsState() val shouldTriggerAutoLoad by viewModel.shouldTriggerAutoLoad.collectAsState()
val kinds by viewModel.kinds.collectAsState() val kinds by viewModel.kinds.collectAsState()
val isLoading by viewModel.isLoading.collectAsState() val isLoading by viewModel.isLoading.collectAsState()
@@ -119,16 +125,14 @@ fun ExploreShowScreen(
var showKindSheet by remember { mutableStateOf(false) } var showKindSheet by remember { mutableStateOf(false) }
val layoutState by viewModel.layoutState.collectAsState() val layoutState by viewModel.layoutState.collectAsState()
val isGridMode = layoutState == 1 val isGridMode = layoutState == 1
val context = LocalContext.current var showGridCountSheet by remember { mutableStateOf(false) }
val gridColumnCount = remember(context.resources.configuration.orientation) { val gridColumnCount by viewModel.gridCount.collectAsState()
context.bookshelfLayoutGrid
}
val shouldLoadMoreList = remember { val shouldLoadMoreList = remember {
derivedStateOf { derivedStateOf {
val total = listState.layoutInfo.totalItemsCount val total = listState.layoutInfo.totalItemsCount
val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
total > 0 && last >= total - 6 total > 0 && last >= total - 3
} }
} }
@@ -136,7 +140,7 @@ fun ExploreShowScreen(
derivedStateOf { derivedStateOf {
val total = gridState.layoutInfo.totalItemsCount val total = gridState.layoutInfo.totalItemsCount
val last = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 val last = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
total > 0 && last >= total - 6 total > 0 && last >= total - 1
} }
} }
@@ -166,6 +170,50 @@ fun ExploreShowScreen(
} }
} }
if (showGridCountSheet) {
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = { showGridCountSheet = false }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
) {
Text(
text = "当前:$gridColumnCount 列",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 12.dp)
)
Slider(
value = gridColumnCount.toFloat(),
onValueChange = {
val col = it.toInt().coerceIn(1, 10)
viewModel.saveGridCount(col)
},
valueRange = 1f..10f,
steps = 8,
modifier = Modifier.padding(horizontal = 8.dp)
)
Spacer(Modifier.height(20.dp))
OutlinedButton (
onClick = { showGridCountSheet = false },
modifier = Modifier.fillMaxWidth()
) {
Text("完成")
}
}
}
}
if (showKindSheet) { if (showKindSheet) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val sheetState = rememberModalBottomSheetState( val sheetState = rememberModalBottomSheetState(
@@ -218,6 +266,7 @@ fun ExploreShowScreen(
onSelectKindClick = { showKindSheet = true }, onSelectKindClick = { showKindSheet = true },
onToggleGridMode = { viewModel.setLayout() }, onToggleGridMode = { viewModel.setLayout() },
isGridMode = isGridMode, isGridMode = isGridMode,
onGridCountClick = { showGridCountSheet = true },
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
} }
@@ -227,74 +276,72 @@ fun ExploreShowScreen(
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
if (!isLoading && isBookEnd && errorMsg == null) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
EmptyMessageView(message = "暂无书籍")
}
} else {
Crossfade(
targetState = isGridMode,
animationSpec = tween(250),
label = "LayoutCrossfade"
) { isGrid ->
if (isGrid) {
LazyVerticalGrid(
state = gridState,
modifier = Modifier.fillMaxSize(),
columns = GridCells.Fixed(gridColumnCount),
contentPadding = PaddingValues(12.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = books,
key = { it.bookUrl }
) { book ->
val shelfState = viewModel.getCurrentBookShelfState(book)
ExploreBookGridItem(
book = book,
shelfState = shelfState,
onClick = { onBookClick(book) },
modifier = Modifier.animateItem()
)
}
item(span = { GridItemSpan(maxLineSpan) }) { Crossfade(
LoadMoreFooter( targetState = isGridMode,
isLoading = isLoading, animationSpec = tween(250),
errorMsg = errorMsg, label = "LayoutCrossfade"
onRetry = viewModel::loadMore ) { isGrid ->
) if (isGrid) {
} LazyVerticalGrid(
state = gridState,
modifier = Modifier.fillMaxSize(),
columns = GridCells.Fixed(gridColumnCount),
contentPadding = PaddingValues(12.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = books,
key = { it.bookUrl }
) { book ->
val shelfState = viewModel.getCurrentBookShelfState(book)
ExploreBookGridItem(
book = book,
shelfState = shelfState,
onClick = { onBookClick(book) },
modifier = Modifier.animateItem()
)
} }
} else {
LazyColumn(
state = listState,
contentPadding = PaddingValues(bottom = 16.dp)
) {
items(
items = books,
key = { it.bookUrl }
) { book ->
val shelfState = viewModel.getCurrentBookShelfState(book)
ExploreBookItem(
book = book,
shelfState = shelfState,
onClick = { onBookClick(book) },
modifier = Modifier.animateItem()
)
}
item { item(span = { GridItemSpan(maxLineSpan) }) {
LoadMoreFooter( LoadMoreFooter(
isLoading = isLoading, isLoading = isLoading,
errorMsg = errorMsg, errorMsg = errorMsg,
onRetry = viewModel::loadMore isEnd = isBookEnd,
) onRetry = viewModel::loadMore
} )
}
}
} else {
LazyColumn(
state = listState,
contentPadding = PaddingValues(bottom = 16.dp)
) {
items(
items = books,
key = { it.bookUrl }
) { book ->
val shelfState = viewModel.getCurrentBookShelfState(book)
ExploreBookItem(
book = book,
shelfState = shelfState,
onClick = { onBookClick(book) },
modifier = Modifier.animateItem()
)
}
item {
LoadMoreFooter(
isLoading = isLoading,
errorMsg = errorMsg,
isEnd = isBookEnd,
onRetry = viewModel::loadMore
)
} }
} }
} }
} }
} }
} }
@@ -341,6 +388,7 @@ fun ExploreTopBar(
onSelectKindClick: () -> Unit, onSelectKindClick: () -> Unit,
onToggleGridMode: () -> Unit, onToggleGridMode: () -> Unit,
isGridMode: Boolean, isGridMode: Boolean,
onGridCountClick: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
@@ -353,11 +401,25 @@ fun ExploreTopBar(
} }
}, },
actions = { actions = {
IconButton(onClick = { showMenu = true }) { Row(
Icon(Icons.Default.FilterList, contentDescription = "Filter") horizontalArrangement = Arrangement.End,
} modifier = Modifier.animateContentSize(tween(300))
IconButton(onClick = { onSelectKindClick() }) { ) {
Icon(Icons.Outlined.FilterAlt, contentDescription = "分类") IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.FilterList, contentDescription = "Filter")
}
IconButton(onClick = { onSelectKindClick() }) {
Icon(Icons.Outlined.FilterAlt, contentDescription = "分类")
}
AnimatedVisibility(
visible = isGridMode,
enter = fadeIn(tween(300)),
exit = fadeOut(tween(300))
) {
IconButton(onClick = onGridCountClick) {
Icon(Icons.Default.Grid4x4, contentDescription = "列数设置")
}
}
} }
IconButton(onClick = { onToggleGridMode() }) { IconButton(onClick = { onToggleGridMode() }) {
Icon( Icon(
@@ -592,22 +654,33 @@ fun TagChip(text: String) {
fun LoadMoreFooter( fun LoadMoreFooter(
isLoading: Boolean, isLoading: Boolean,
errorMsg: String?, errorMsg: String?,
isEnd: Boolean,
onRetry: () -> Unit onRetry: () -> Unit
) { ) {
LaunchedEffect(isLoading, errorMsg, isEnd) {
if (!isLoading && errorMsg == null && !isEnd) {
while (true) {
onRetry()
delay(1000L)
}
}
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(32.dp), .padding(32.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
AnimatedContent( AnimatedContent(
targetState = when { targetState = when {
isLoading -> "加载中…" isLoading -> "加载中…"
errorMsg != null -> "加载失败: $errorMsg" errorMsg != null -> "加载失败: $errorMsg"
else -> "已经到底了~" isEnd -> "已经到底了~"
else -> "我爱你"
}, },
label = "FooterTextChange" label = "FooterTextChange"
) { text -> ) { text ->
@@ -10,6 +10,7 @@ import io.legado.app.data.repository.ExploreRepository
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.model.BookShelfState import io.legado.app.model.BookShelfState
import io.legado.app.ui.book.search.BookKey import io.legado.app.ui.book.search.BookKey
import io.legado.app.utils.exploreLayoutGrid
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import splitties.init.appCtx
sealed class BookFilterState(val id: Int) { sealed class BookFilterState(val id: Int) {
data object SHOW_ALL : BookFilterState(0) data object SHOW_ALL : BookFilterState(0)
@@ -60,7 +62,13 @@ class ExploreShowViewModel(
val selectedKindTitle = _selectedKindTitle.asStateFlow() val selectedKindTitle = _selectedKindTitle.asStateFlow()
private val _layoutState = MutableStateFlow(AppConfig.exploreLayoutState) // 0=列表, 1=网格 private val _layoutState = MutableStateFlow(AppConfig.exploreLayoutState) // 0=列表, 1=网格
val layoutState: StateFlow<Int> = _layoutState.asStateFlow() val layoutState: StateFlow<Int> = _layoutState.asStateFlow()
private val _gridCount = MutableStateFlow(appCtx.exploreLayoutGrid)
val gridCount = _gridCount.asStateFlow()
val isEnd: StateFlow<Boolean> = _isEndStateFlow.asStateFlow()
fun saveGridCount(value: Int) {
appCtx.exploreLayoutGrid = value
_gridCount.value = value
}
val uiBooks = combine( val uiBooks = combine(
_rawBooks, _rawBooks,
_filterState, _filterState,
@@ -71,13 +79,6 @@ class ExploreShowViewModel(
} }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
val isRawDataEmptyAndEnd = combine(
_rawBooks,
_isEndStateFlow
) { rawBooks, isEndValue ->
rawBooks.isEmpty() && isEndValue
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
val shouldTriggerAutoLoad = combine( val shouldTriggerAutoLoad = combine(
_isLoading, _isLoading,
uiBooks, uiBooks,
@@ -138,21 +139,14 @@ class ExploreShowViewModel(
} }
fun loadMore(isRefresh: Boolean = false) { fun loadMore(isRefresh: Boolean = false) {
if (_isLoading.value || (_isEndStateFlow.value && !isRefresh)) return val source = bookSource
val url = exploreUrl
if (source == null || url == null || _isLoading.value || (_isEndStateFlow.value && !isRefresh)) return
viewModelScope.launch { viewModelScope.launch {
_isLoading.value = true _isLoading.value = true
_errorMsg.value = null _errorMsg.value = null
val source = bookSource
val url = exploreUrl
if (source == null || url == null) {
_isLoading.value = false
_errorMsg.value = "源或URL为空"
return@launch
}
if (isRefresh) { if (isRefresh) {
page = 1 page = 1
_isEndStateFlow.value = false _isEndStateFlow.value = false
@@ -165,16 +159,21 @@ class ExploreShowViewModel(
_isEndStateFlow.value = true _isEndStateFlow.value = true
} else { } else {
repository.saveSearchBooks(newBooks) repository.saveSearchBooks(newBooks)
val currentList = if (page == 1) emptyList() else _rawBooks.value
if (page == 1) { val currentList = _rawBooks.value
_rawBooks.value = newBooks.distinctBy { it.bookUrl } val existingUrls = currentList.map { it.bookUrl }.toSet()
val uniqueNewBooks = newBooks
.filter { it.bookUrl !in existingUrls }
.distinctBy { it.bookUrl }
if (uniqueNewBooks.isEmpty()) {
_isEndStateFlow.value = true
} else { } else {
val existingUrls = currentList.map { it.bookUrl }.toSet()
val uniqueNewBooks = newBooks.filter { it.bookUrl !in existingUrls }
_rawBooks.value = currentList + uniqueNewBooks _rawBooks.value = currentList + uniqueNewBooks
page++
_isEndStateFlow.value = false
} }
page++
_isEndStateFlow.value = false
} }
} }
.onFailure { .onFailure {
@@ -434,6 +434,20 @@ val Context.bookshelfLayoutGrid: Int
AppConfig.bookshelfLayoutGridPortrait AppConfig.bookshelfLayoutGridPortrait
} }
var Context.exploreLayoutGrid: Int
get() = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
AppConfig.exploreLayoutGridLandscape
} else {
AppConfig.exploreLayoutGridPortrait
}
set(value) {
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
AppConfig.exploreLayoutGridLandscape = value
} else {
AppConfig.exploreLayoutGridPortrait = value
}
}
fun Context.themeColor(attr: Int): Int { fun Context.themeColor(attr: Int): Int {
val typedValue = TypedValue() val typedValue = TypedValue()
theme.resolveAttribute(attr, typedValue, true) theme.resolveAttribute(attr, typedValue, true)