diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index a9f72d074..26009eb80 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/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" } diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index 1167cd48f..b34c3e06b 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/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) + } + } diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt index 23a3984d6..e8cf1be84 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt +++ b/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, diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt index e93f820db..7b5447bbf 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt +++ b/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(null) val selectedKindTitle = _selectedKindTitle.asStateFlow() + private val _layoutState = MutableStateFlow(AppConfig.exploreLayoutState) // 0=列表, 1=网格 + val layoutState: StateFlow = _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