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

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.AppDatabase
import io.legado.app.data.entities.BookSource 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.help.book.isNotShelf import io.legado.app.help.book.isNotShelf
import io.legado.app.help.source.exploreKinds
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -15,6 +18,7 @@ interface ExploreRepository {
suspend fun getBookSource(url: String): BookSource? suspend fun getBookSource(url: String): BookSource?
suspend fun exploreBook(source: BookSource, url: String, page: Int): Result<List<SearchBook>> suspend fun exploreBook(source: BookSource, url: String, page: Int): Result<List<SearchBook>>
suspend fun saveSearchBooks(books: List<SearchBook>) suspend fun saveSearchBooks(books: List<SearchBook>)
suspend fun getSourceExploreKinds(sourceUrl: String): List<ExploreKind>
} }
class ExploreRepositoryImpl( 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>) { override suspend fun saveSearchBooks(books: List<SearchBook>) {
appDb.searchBookDao.insert(*books.toTypedArray()) appDb.searchBookDao.insert(*books.toTypedArray())
} }
@@ -5,6 +5,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack 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.Check
import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.LoadingIndicator
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.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
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.model.BookShelfState import io.legado.app.model.BookShelfState
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 org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@@ -74,21 +81,61 @@ fun ExploreShowScreen(
} }
val books by viewModel.uiBooks.collectAsState() val books by viewModel.uiBooks.collectAsState()
val kinds by viewModel.kinds.collectAsState()
val isLoading by viewModel.isLoading.collectAsState() val isLoading by viewModel.isLoading.collectAsState()
val errorMsg by viewModel.errorMsg.collectAsState() val errorMsg by viewModel.errorMsg.collectAsState()
val filterState by viewModel.filterState.collectAsState() val filterState by viewModel.filterState.collectAsState()
val selectedTitle by viewModel.selectedKindTitle.collectAsState()
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() 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( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
ExploreTopBar( ExploreTopBar(
title = title, title = selectedTitle ?: title,
filterState = filterState, filterState = filterState,
onBack = onBack, onBack = onBack,
onFilterSelect = viewModel::setFilterState, onFilterSelect = viewModel::setFilterState,
onSelectKindClick = { showKindSheet = true },
scrollBehavior = scrollBehavior 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ExploreTopBar( fun ExploreTopBar(
@@ -154,12 +232,13 @@ fun ExploreTopBar(
filterState: BookFilterState, filterState: BookFilterState,
onBack: () -> Unit, onBack: () -> Unit,
onFilterSelect: (BookFilterState) -> Unit, onFilterSelect: (BookFilterState) -> Unit,
onSelectKindClick: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
MediumTopAppBar( MediumTopAppBar(
title = { Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis) }, title = { AnimatedTextLine(title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
@@ -169,7 +248,9 @@ fun ExploreTopBar(
IconButton(onClick = { showMenu = true }) { IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.FilterList, contentDescription = "Filter") Icon(Icons.Default.FilterList, contentDescription = "Filter")
} }
IconButton(onClick = { onSelectKindClick() }) {
Icon(Icons.AutoMirrored.Filled.List, contentDescription = "分类")
}
DropdownMenu( DropdownMenu(
expanded = showMenu, expanded = showMenu,
onDismissRequest = { showMenu = false } onDismissRequest = { showMenu = false }
@@ -5,6 +5,7 @@ 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.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.repository.ExploreRepository 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
@@ -52,7 +53,10 @@ class ExploreShowViewModel(
private var page = 1 private var page = 1
private var isEnd = false private var isEnd = false
private val _bookshelf = MutableStateFlow<Set<BookKey>>(emptySet()) 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( val uiBooks = combine(
_rawBooks, _rawBooks,
_filterState, _filterState,
@@ -96,11 +100,24 @@ class ExploreShowViewModel(
viewModelScope.launch { viewModelScope.launch {
if (bookSource == null && sourceUrl != null) { if (bookSource == null && sourceUrl != null) {
bookSource = repository.getBookSource(sourceUrl) bookSource = repository.getBookSource(sourceUrl)
loadKinds(sourceUrl)
} }
loadMore(isRefresh = true) 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) { fun setFilterState(state: BookFilterState) {
_filterState.value = state _filterState.value = state
AppConfig.exploreFilterState = state.id AppConfig.exploreFilterState = state.id