[新增] 发现页快速选择分类
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user