[新增] 发现页快速选择分类

This commit is contained in:
HapeLee
2025-11-30 02:46:42 +08:00
parent 3720847ebb
commit 6f5606b9e1
3 changed files with 112 additions and 5 deletions
@@ -3,9 +3,12 @@ package io.legado.app.data.repository
import io.legado.app.data.AppDatabase
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.help.book.isNotShelf
import io.legado.app.help.source.exploreKinds
import io.legado.app.model.webBook.WebBook
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
@@ -15,6 +18,7 @@ interface ExploreRepository {
suspend fun getBookSource(url: String): BookSource?
suspend fun exploreBook(source: BookSource, url: String, page: Int): Result<List<SearchBook>>
suspend fun saveSearchBooks(books: List<SearchBook>)
suspend fun getSourceExploreKinds(sourceUrl: String): List<ExploreKind>
}
class ExploreRepositoryImpl(
@@ -44,6 +48,11 @@ class ExploreRepositoryImpl(
}
}
override suspend fun getSourceExploreKinds(sourceUrl: String): List<ExploreKind> = withContext(IO) {
val source = appDb.bookSourceDao.getBookSource(sourceUrl)
return@withContext source?.exploreKinds() ?: emptyList()
}
override suspend fun saveSearchBooks(books: List<SearchBook>) {
appDb.searchBookDao.insert(*books.toTypedArray())
}
@@ -5,6 +5,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -18,19 +19,23 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -55,7 +60,9 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.model.BookShelfState
import io.legado.app.ui.widget.components.AnimatedTextLine
import io.legado.app.ui.widget.components.Cover
import org.koin.androidx.compose.koinViewModel
@@ -74,21 +81,61 @@ fun ExploreShowScreen(
}
val books by viewModel.uiBooks.collectAsState()
val kinds by viewModel.kinds.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
val errorMsg by viewModel.errorMsg.collectAsState()
val filterState by viewModel.filterState.collectAsState()
val selectedTitle by viewModel.selectedKindTitle.collectAsState()
val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var showKindSheet by remember { mutableStateOf(false) }
if (showKindSheet) {
val scrollState = rememberScrollState() // 记住滚动状态
ModalBottomSheet(
onDismissRequest = { showKindSheet = false }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(scrollState)
.padding(12.dp)
) {
Text(
"选择分类",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.padding(bottom = 8.dp)
)
FlowRow(
modifier = Modifier.fillMaxWidth()
) {
kinds.forEach { kind ->
KindListItem(
kind = kind,
currentTitle = selectedTitle ?: title,
onClick = {
showKindSheet = false
viewModel.switchExploreUrl(kind)
}
)
}
}
}
}
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
ExploreTopBar(
title = title,
title = selectedTitle ?: title,
filterState = filterState,
onBack = onBack,
onFilterSelect = viewModel::setFilterState,
onSelectKindClick = { showKindSheet = true },
scrollBehavior = scrollBehavior
)
}
@@ -147,6 +194,37 @@ fun ExploreShowScreen(
}
}
@Composable
fun KindListItem(
kind: ExploreKind,
currentTitle: String?,
onClick: () -> Unit
) {
val isClickable = !kind.url.isNullOrBlank()
val isSelected = kind.title == currentTitle
FilterChip(
onClick = { if (isClickable) onClick() },
enabled = isClickable,
selected = isSelected,
label = {
Text(
text = kind.title,
fontSize = 14.sp,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
modifier = Modifier
.then(
if (!isClickable) Modifier.fillMaxWidth()
else Modifier.fillMaxWidth(1 / 3f)
)
.padding(horizontal = 4.dp)
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExploreTopBar(
@@ -154,12 +232,13 @@ fun ExploreTopBar(
filterState: BookFilterState,
onBack: () -> Unit,
onFilterSelect: (BookFilterState) -> Unit,
onSelectKindClick: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null
) {
var showMenu by remember { mutableStateOf(false) }
MediumTopAppBar(
title = { Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
title = { AnimatedTextLine(title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
@@ -169,7 +248,9 @@ fun ExploreTopBar(
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.FilterList, contentDescription = "Filter")
}
IconButton(onClick = { onSelectKindClick() }) {
Icon(Icons.AutoMirrored.Filled.List, contentDescription = "分类")
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.data.repository.ExploreRepository
import io.legado.app.help.config.AppConfig
import io.legado.app.model.BookShelfState
@@ -52,7 +53,10 @@ class ExploreShowViewModel(
private var page = 1
private var isEnd = false
private val _bookshelf = MutableStateFlow<Set<BookKey>>(emptySet())
private val _kinds = MutableStateFlow<List<ExploreKind>>(emptyList())
val kinds = _kinds.asStateFlow()
private val _selectedKindTitle = MutableStateFlow<String?>(null)
val selectedKindTitle = _selectedKindTitle.asStateFlow()
val uiBooks = combine(
_rawBooks,
_filterState,
@@ -96,11 +100,24 @@ class ExploreShowViewModel(
viewModelScope.launch {
if (bookSource == null && sourceUrl != null) {
bookSource = repository.getBookSource(sourceUrl)
loadKinds(sourceUrl)
}
loadMore(isRefresh = true)
}
}
fun loadKinds(sourceUrl: String) {
viewModelScope.launch {
_kinds.value = repository.getSourceExploreKinds(sourceUrl)
}
}
fun switchExploreUrl(kind: ExploreKind) {
_selectedKindTitle.value = kind.title
exploreUrl = kind.url
loadMore(isRefresh = true)
}
fun setFilterState(state: BookFilterState) {
_filterState.value = state
AppConfig.exploreFilterState = state.id