[优化] 正确处理发现页的一些情况
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user