فهرست منبع

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

HapeLee 11 ماه پیش
والد
کامیت
6f5606b9e1

+ 9 - 0
app/src/main/java/io/legado/app/data/repository/ExploreRepository.kt

@@ -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())
     }

+ 85 - 4
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt

@@ -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 }

+ 18 - 1
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt

@@ -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