Просмотр исходного кода

[新增] 发现页新增网格布局

HapeLee 11 месяцев назад
Родитель
Сommit
53002cfe32

+ 2 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -222,4 +222,6 @@ object PreferKey {
     const val shouldShowExpandButton = "shouldShowExpandButton"
 
     const val exploreFilterState = "explore_filter_state"
+
+    const val exploreLayoutState = "exploreLayoutState"
 }

+ 7 - 0
app/src/main/java/io/legado/app/help/config/AppConfig.kt

@@ -902,5 +902,12 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
         set(value) {
             appCtx.putPrefInt(PreferKey.exploreFilterState, value)
         }
+
+    var exploreLayoutState: Int
+        get() = appCtx.getPrefInt(PreferKey.exploreLayoutState, 0)
+        set(value) {
+            appCtx.putPrefInt(PreferKey.exploreLayoutState, value)
+        }
+
 }
 

+ 143 - 23
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt

@@ -1,20 +1,29 @@
 package io.legado.app.ui.book.explore
 
 import android.content.Intent
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.rememberScrollState
@@ -23,8 +32,12 @@ 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.automirrored.filled.Sort
+import androidx.compose.material.icons.automirrored.filled.ViewList
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.FilterList
+import androidx.compose.material.icons.filled.GridView
+import androidx.compose.material.icons.filled.ViewList
 import androidx.compose.material3.DropdownMenu
 import androidx.compose.material3.DropdownMenuItem
 import androidx.compose.material3.ExperimentalMaterial3Api
@@ -52,6 +65,7 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.text.font.FontWeight
@@ -90,7 +104,8 @@ fun ExploreShowScreen(
     val listState = rememberLazyListState()
     val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
     var showKindSheet by remember { mutableStateOf(false) }
-
+    val layoutState by viewModel.layoutState.collectAsState()
+    val isGridMode = layoutState == 1
     if (showKindSheet) {
         val scrollState = rememberScrollState() // 记住滚动状态
 
@@ -137,6 +152,8 @@ fun ExploreShowScreen(
                 onBack = onBack,
                 onFilterSelect = viewModel::setFilterState,
                 onSelectKindClick = { showKindSheet = true },
+                onToggleGridMode = { viewModel.setLayout() },
+                isGridMode = isGridMode,
                 scrollBehavior = scrollBehavior
             )
         }
@@ -151,29 +168,71 @@ fun ExploreShowScreen(
                     EmptyMessageView(message = "暂无书籍")
                 }
             } 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()
-                        )
-                    }
+                Crossfade(
+                    targetState = isGridMode,
+                    animationSpec = tween(250),
+                    label = "LayoutCrossfade"
+                ) { isGrid ->
+                    if (isGrid) {
+                        LazyVerticalGrid(
+                            modifier = Modifier.fillMaxSize(),
+                            columns = GridCells.Adaptive(minSize = 90.dp),
+                            contentPadding = PaddingValues(
+                                start = 12.dp,
+                                end = 12.dp,
+                                top = 8.dp,
+                                bottom = 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 {
-                        LoadMoreFooter(
-                            isLoading = isLoading,
-                            errorMsg = errorMsg,
-                            onRetry = viewModel::loadMore
-                        )
+                            item {
+                                LoadMoreFooter(
+                                    isLoading = isLoading,
+                                    errorMsg = errorMsg,
+                                    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,
+                                    onRetry = viewModel::loadMore
+                                )
+                            }
+                        }
                     }
                 }
             }
@@ -234,6 +293,8 @@ fun ExploreTopBar(
     onBack: () -> Unit,
     onFilterSelect: (BookFilterState) -> Unit,
     onSelectKindClick: () -> Unit,
+    onToggleGridMode: () -> Unit,
+    isGridMode: Boolean,
     scrollBehavior: TopAppBarScrollBehavior? = null
 ) {
     var showMenu by remember { mutableStateOf(false) }
@@ -252,6 +313,12 @@ fun ExploreTopBar(
             IconButton(onClick = { onSelectKindClick() }) {
                 Icon(Icons.AutoMirrored.Filled.List, contentDescription = "分类")
             }
+            IconButton(onClick = { onToggleGridMode() }) {
+                Icon(
+                    imageVector = if (isGridMode) Icons.AutoMirrored.Filled.ViewList else Icons.Default.GridView,
+                    contentDescription = "切换布局"
+                )
+            }
             DropdownMenu(
                 expanded = showMenu,
                 onDismissRequest = { showMenu = false }
@@ -386,6 +453,59 @@ fun ExploreBookItem(
     }
 }
 
+@Composable
+fun ExploreBookGridItem(
+    book: SearchBook,
+    onClick: () -> Unit,
+    shelfState: BookShelfState,
+    modifier: Modifier = Modifier
+) {
+
+    val badgeText: String? = when (shelfState) {
+        BookShelfState.IN_SHELF -> "已在书架"
+        BookShelfState.SAME_NAME_AUTHOR -> "同名书籍"
+        BookShelfState.NOT_IN_SHELF -> null
+    }
+
+    val content: (@Composable RowScope.() -> Unit)? = if (!badgeText.isNullOrBlank()) {
+        {
+            Text(
+                text = badgeText,
+                style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight.Bold, fontSize = 9.sp)
+            )
+        }
+    } else {
+        null
+    }
+
+    Column(
+        modifier = modifier
+            .width(IntrinsicSize.Min)
+            .clip(RoundedCornerShape(4.dp))
+            .clickable(onClick = onClick)
+            .padding(4.dp)
+    ) {
+
+        Cover(
+            path = book.coverUrl,
+            modifier = Modifier
+                .fillMaxWidth()
+                .aspectRatio(12 / 17f),
+            badgeContent = content
+        )
+
+        Spacer(modifier = Modifier.height(4.dp))
+
+        Text(
+            text = book.name,
+            style = MaterialTheme.typography.bodySmall,
+            fontWeight = FontWeight.Bold,
+            maxLines = 2,
+            overflow = TextOverflow.Ellipsis
+        )
+    }
+}
+
 @Composable
 fun BookshelfStatusBadge(
     shelfState: BookShelfState,

+ 10 - 0
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt

@@ -12,6 +12,7 @@ import io.legado.app.model.BookShelfState
 import io.legado.app.ui.book.search.BookKey
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
@@ -57,6 +58,9 @@ class ExploreShowViewModel(
     val kinds = _kinds.asStateFlow()
     private val _selectedKindTitle = MutableStateFlow<String?>(null)
     val selectedKindTitle = _selectedKindTitle.asStateFlow()
+    private val _layoutState = MutableStateFlow(AppConfig.exploreLayoutState) // 0=列表, 1=网格
+    val layoutState: StateFlow<Int> = _layoutState.asStateFlow()
+
     val uiBooks = combine(
         _rawBooks,
         _filterState,
@@ -111,6 +115,12 @@ class ExploreShowViewModel(
         AppConfig.exploreFilterState = state.id
     }
 
+    fun setLayout() {
+        val newState = if (_layoutState.value == 0) 1 else 0
+        _layoutState.value = newState
+        AppConfig.exploreLayoutState = newState
+    }
+
     fun loadMore(isRefresh: Boolean = false) {
         if (_isLoading.value || (isEnd && !isRefresh)) return