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

This commit is contained in:
HapeLee
2025-12-06 15:08:13 +08:00
parent 374da29a8b
commit 7a1ade76e2
6 changed files with 206 additions and 104 deletions
@@ -204,6 +204,10 @@ object PreferKey {
const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable"
const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"
const val bookshelfLayoutGridPortrait = "bookshelfLayoutGridPortrait"
const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape"
const val exploreLayoutGridPortrait = "exploreLayoutGridPortrait"
const val tabletInterface = "tabletInterface"
const val pureBlack = "pure_black"
const val labelVisibilityMode = "labelVisibilityMode"
@@ -37,7 +37,7 @@ class ExploreRepositoryImpl(
}
override suspend fun exploreBook(source: BookSource, url: String, page: Int): Result<List<SearchBook>> {
return withContext(Dispatchers.IO) {
return withContext(IO) {
try {
val books = WebBook.exploreBookSuspend(source, url, page)
Result.success(books)
@@ -237,6 +237,18 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
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
get() = appCtx.getPrefInt(PreferKey.bookshelfLayoutGridLandscape, 7)
set(value) {
@@ -3,8 +3,12 @@ package io.legado.app.ui.book.explore
import android.annotation.SuppressLint
import android.content.Intent
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.animateContentSize
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.horizontalScroll
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.filled.Check
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.Warning
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.MediumTopAppBar
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
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.Cover
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.utils.bookshelfLayoutGrid
import kotlinx.coroutines.delay
import org.koin.androidx.compose.koinViewModel
@SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight")
@@ -106,7 +112,7 @@ fun ExploreShowScreen(
}
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 kinds by viewModel.kinds.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
@@ -119,16 +125,14 @@ fun ExploreShowScreen(
var showKindSheet by remember { mutableStateOf(false) }
val layoutState by viewModel.layoutState.collectAsState()
val isGridMode = layoutState == 1
val context = LocalContext.current
val gridColumnCount = remember(context.resources.configuration.orientation) {
context.bookshelfLayoutGrid
}
var showGridCountSheet by remember { mutableStateOf(false) }
val gridColumnCount by viewModel.gridCount.collectAsState()
val shouldLoadMoreList = remember {
derivedStateOf {
val total = listState.layoutInfo.totalItemsCount
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 {
val total = gridState.layoutInfo.totalItemsCount
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) {
val scrollState = rememberScrollState()
val sheetState = rememberModalBottomSheetState(
@@ -218,6 +266,7 @@ fun ExploreShowScreen(
onSelectKindClick = { showKindSheet = true },
onToggleGridMode = { viewModel.setLayout() },
isGridMode = isGridMode,
onGridCountClick = { showGridCountSheet = true },
scrollBehavior = scrollBehavior
)
}
@@ -227,74 +276,72 @@ fun ExploreShowScreen(
.fillMaxSize()
.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) }) {
LoadMoreFooter(
isLoading = isLoading,
errorMsg = errorMsg,
onRetry = viewModel::loadMore
)
}
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()
)
}
} 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,
onRetry = viewModel::loadMore
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
LoadMoreFooter(
isLoading = isLoading,
errorMsg = errorMsg,
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,
onToggleGridMode: () -> Unit,
isGridMode: Boolean,
onGridCountClick: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null
) {
var showMenu by remember { mutableStateOf(false) }
@@ -353,11 +401,25 @@ fun ExploreTopBar(
}
},
actions = {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.FilterList, contentDescription = "Filter")
}
IconButton(onClick = { onSelectKindClick() }) {
Icon(Icons.Outlined.FilterAlt, contentDescription = "分类")
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.animateContentSize(tween(300))
) {
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() }) {
Icon(
@@ -592,22 +654,33 @@ fun TagChip(text: String) {
fun LoadMoreFooter(
isLoading: Boolean,
errorMsg: String?,
isEnd: Boolean,
onRetry: () -> Unit
) {
LaunchedEffect(isLoading, errorMsg, isEnd) {
if (!isLoading && errorMsg == null && !isEnd) {
while (true) {
onRetry()
delay(1000L)
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
AnimatedContent(
targetState = when {
isLoading -> "加载中…"
errorMsg != null -> "加载失败: $errorMsg"
else -> "已经到底了~"
isEnd -> "已经到底了~"
else -> "我爱你"
},
label = "FooterTextChange"
) { text ->
@@ -10,6 +10,7 @@ import io.legado.app.data.repository.ExploreRepository
import io.legado.app.help.config.AppConfig
import io.legado.app.model.BookShelfState
import io.legado.app.ui.book.search.BookKey
import io.legado.app.utils.exploreLayoutGrid
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import splitties.init.appCtx
sealed class BookFilterState(val id: Int) {
data object SHOW_ALL : BookFilterState(0)
@@ -60,7 +62,13 @@ class ExploreShowViewModel(
val selectedKindTitle = _selectedKindTitle.asStateFlow()
private val _layoutState = MutableStateFlow(AppConfig.exploreLayoutState) // 0=列表, 1=网格
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(
_rawBooks,
_filterState,
@@ -71,13 +79,6 @@ class ExploreShowViewModel(
}
}.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(
_isLoading,
uiBooks,
@@ -138,21 +139,14 @@ class ExploreShowViewModel(
}
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 {
_isLoading.value = true
_errorMsg.value = null
val source = bookSource
val url = exploreUrl
if (source == null || url == null) {
_isLoading.value = false
_errorMsg.value = "源或URL为空"
return@launch
}
if (isRefresh) {
page = 1
_isEndStateFlow.value = false
@@ -165,16 +159,21 @@ class ExploreShowViewModel(
_isEndStateFlow.value = true
} else {
repository.saveSearchBooks(newBooks)
val currentList = if (page == 1) emptyList() else _rawBooks.value
if (page == 1) {
_rawBooks.value = newBooks.distinctBy { it.bookUrl }
val currentList = _rawBooks.value
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 {
val existingUrls = currentList.map { it.bookUrl }.toSet()
val uniqueNewBooks = newBooks.filter { it.bookUrl !in existingUrls }
_rawBooks.value = currentList + uniqueNewBooks
page++
_isEndStateFlow.value = false
}
page++
_isEndStateFlow.value = false
}
}
.onFailure {
@@ -434,6 +434,20 @@ val Context.bookshelfLayoutGrid: Int
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 {
val typedValue = TypedValue()
theme.resolveAttribute(attr, typedValue, true)