From 7a1ade76e22c6125e27974e35b979af768324096 Mon Sep 17 00:00:00 2001 From: HapeLee <1321903405@qq.com> Date: Sat, 6 Dec 2025 15:08:13 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=8F=91=E7=8E=B0=E9=A1=B5=E7=9A=84=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/legado/app/constant/PreferKey.kt | 4 + .../app/data/repository/ExploreRepository.kt | 2 +- .../io/legado/app/help/config/AppConfig.kt | 12 + .../app/ui/book/explore/ExploreShowScreen.kt | 229 ++++++++++++------ .../ui/book/explore/ExploreShowViewModel.kt | 49 ++-- .../io/legado/app/utils/ContextExtensions.kt | 14 ++ 6 files changed, 206 insertions(+), 104 deletions(-) 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 583db0577..fc0928144 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -204,6 +204,10 @@ object PreferKey { const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable" const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape" const val bookshelfLayoutGridPortrait = "bookshelfLayoutGridPortrait" + + const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape" + + const val exploreLayoutGridPortrait = "exploreLayoutGridPortrait" const val tabletInterface = "tabletInterface" const val pureBlack = "pure_black" const val labelVisibilityMode = "labelVisibilityMode" diff --git a/app/src/main/java/io/legado/app/data/repository/ExploreRepository.kt b/app/src/main/java/io/legado/app/data/repository/ExploreRepository.kt index e13f22051..6919478e8 100644 --- a/app/src/main/java/io/legado/app/data/repository/ExploreRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/ExploreRepository.kt @@ -37,7 +37,7 @@ class ExploreRepositoryImpl( } override suspend fun exploreBook(source: BookSource, url: String, page: Int): Result> { - return withContext(Dispatchers.IO) { + return withContext(IO) { try { val books = WebBook.exploreBookSuspend(source, url, page) Result.success(books) 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 03837015c..d7df909d8 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 @@ -237,6 +237,18 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { appCtx.putPrefInt(PreferKey.bookshelfLayoutGridPortrait, value) } + var exploreLayoutGridLandscape: Int + get() = appCtx.getPrefInt(PreferKey.exploreLayoutGridLandscape, 7) + set(value) { + appCtx.putPrefInt(PreferKey.exploreLayoutGridLandscape, value) + } + + var exploreLayoutGridPortrait: Int + get() = appCtx.getPrefInt(PreferKey.exploreLayoutGridPortrait, 3) + set(value) { + appCtx.putPrefInt(PreferKey.exploreLayoutGridPortrait, value) + } + var bookshelfLayoutGridLandscape: Int get() = appCtx.getPrefInt(PreferKey.bookshelfLayoutGridLandscape, 7) set(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 f6d3869b8..f8604f7df 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 @@ -3,8 +3,12 @@ package io.legado.app.ui.book.explore import android.annotation.SuppressLint import android.content.Intent import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -40,6 +44,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.FilterList +import androidx.compose.material.icons.filled.Grid4x4 import androidx.compose.material.icons.filled.GridView import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.outlined.FilterAlt @@ -53,8 +58,10 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetValue +import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -74,7 +81,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -87,7 +93,7 @@ import io.legado.app.ui.widget.components.AnimatedTextButton import io.legado.app.ui.widget.components.AnimatedTextLine import io.legado.app.ui.widget.components.Cover import io.legado.app.ui.widget.components.EmptyMessageView -import io.legado.app.utils.bookshelfLayoutGrid +import kotlinx.coroutines.delay import org.koin.androidx.compose.koinViewModel @SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight") @@ -106,7 +112,7 @@ fun ExploreShowScreen( } val books by viewModel.uiBooks.collectAsState() - val isBookEnd by viewModel.isRawDataEmptyAndEnd.collectAsState() + val isBookEnd by viewModel.isEnd.collectAsState() val shouldTriggerAutoLoad by viewModel.shouldTriggerAutoLoad.collectAsState() val kinds by viewModel.kinds.collectAsState() val isLoading by viewModel.isLoading.collectAsState() @@ -119,16 +125,14 @@ fun ExploreShowScreen( var showKindSheet by remember { mutableStateOf(false) } val layoutState by viewModel.layoutState.collectAsState() val isGridMode = layoutState == 1 - val context = LocalContext.current - val gridColumnCount = remember(context.resources.configuration.orientation) { - context.bookshelfLayoutGrid - } + var showGridCountSheet by remember { mutableStateOf(false) } + val gridColumnCount by viewModel.gridCount.collectAsState() val shouldLoadMoreList = remember { derivedStateOf { val total = listState.layoutInfo.totalItemsCount val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 - total > 0 && last >= total - 6 + total > 0 && last >= total - 3 } } @@ -136,7 +140,7 @@ fun ExploreShowScreen( derivedStateOf { val total = gridState.layoutInfo.totalItemsCount val last = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 - total > 0 && last >= total - 6 + total > 0 && last >= total - 1 } } @@ -166,6 +170,50 @@ fun ExploreShowScreen( } } + if (showGridCountSheet) { + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { showGridCountSheet = false } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp) + ) { + + Text( + text = "当前:$gridColumnCount 列", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(bottom = 12.dp) + ) + + Slider( + value = gridColumnCount.toFloat(), + onValueChange = { + val col = it.toInt().coerceIn(1, 10) + viewModel.saveGridCount(col) + }, + valueRange = 1f..10f, + steps = 8, + modifier = Modifier.padding(horizontal = 8.dp) + ) + + Spacer(Modifier.height(20.dp)) + + OutlinedButton ( + onClick = { showGridCountSheet = false }, + modifier = Modifier.fillMaxWidth() + ) { + Text("完成") + } + } + } + } + if (showKindSheet) { val scrollState = rememberScrollState() val sheetState = rememberModalBottomSheetState( @@ -218,6 +266,7 @@ fun ExploreShowScreen( onSelectKindClick = { showKindSheet = true }, onToggleGridMode = { viewModel.setLayout() }, isGridMode = isGridMode, + onGridCountClick = { showGridCountSheet = true }, scrollBehavior = scrollBehavior ) } @@ -227,74 +276,72 @@ fun ExploreShowScreen( .fillMaxSize() .padding(paddingValues) ) { - if (!isLoading && isBookEnd && errorMsg == null) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - EmptyMessageView(message = "暂无书籍") - } - } else { - Crossfade( - targetState = isGridMode, - animationSpec = tween(250), - label = "LayoutCrossfade" - ) { isGrid -> - if (isGrid) { - LazyVerticalGrid( - state = gridState, - modifier = Modifier.fillMaxSize(), - columns = GridCells.Fixed(gridColumnCount), - contentPadding = PaddingValues(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(span = { GridItemSpan(maxLineSpan) }) { - LoadMoreFooter( - isLoading = isLoading, - errorMsg = errorMsg, - onRetry = viewModel::loadMore - ) - } + Crossfade( + targetState = isGridMode, + animationSpec = tween(250), + label = "LayoutCrossfade" + ) { isGrid -> + if (isGrid) { + LazyVerticalGrid( + state = gridState, + modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(gridColumnCount), + contentPadding = PaddingValues(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() + ) } - } 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 - ) - } + item(span = { GridItemSpan(maxLineSpan) }) { + LoadMoreFooter( + isLoading = isLoading, + errorMsg = errorMsg, + isEnd = isBookEnd, + 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, + isEnd = isBookEnd, + onRetry = viewModel::loadMore + ) } } } + } } } @@ -341,6 +388,7 @@ fun ExploreTopBar( onSelectKindClick: () -> Unit, onToggleGridMode: () -> Unit, isGridMode: Boolean, + onGridCountClick: () -> Unit, scrollBehavior: TopAppBarScrollBehavior? = null ) { var showMenu by remember { mutableStateOf(false) } @@ -353,11 +401,25 @@ fun ExploreTopBar( } }, actions = { - IconButton(onClick = { showMenu = true }) { - Icon(Icons.Default.FilterList, contentDescription = "Filter") - } - IconButton(onClick = { onSelectKindClick() }) { - Icon(Icons.Outlined.FilterAlt, contentDescription = "分类") + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.animateContentSize(tween(300)) + ) { + IconButton(onClick = { showMenu = true }) { + Icon(Icons.Default.FilterList, contentDescription = "Filter") + } + IconButton(onClick = { onSelectKindClick() }) { + Icon(Icons.Outlined.FilterAlt, contentDescription = "分类") + } + AnimatedVisibility( + visible = isGridMode, + enter = fadeIn(tween(300)), + exit = fadeOut(tween(300)) + ) { + IconButton(onClick = onGridCountClick) { + Icon(Icons.Default.Grid4x4, contentDescription = "列数设置") + } + } } IconButton(onClick = { onToggleGridMode() }) { Icon( @@ -592,22 +654,33 @@ fun TagChip(text: String) { fun LoadMoreFooter( isLoading: Boolean, errorMsg: String?, + isEnd: Boolean, onRetry: () -> Unit ) { + + LaunchedEffect(isLoading, errorMsg, isEnd) { + if (!isLoading && errorMsg == null && !isEnd) { + while (true) { + onRetry() + delay(1000L) + } + } + } + Box( modifier = Modifier .fillMaxWidth() .padding(32.dp), contentAlignment = Alignment.Center ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { AnimatedContent( targetState = when { isLoading -> "加载中…" errorMsg != null -> "加载失败: $errorMsg" - else -> "已经到底了~" + isEnd -> "已经到底了~" + else -> "我爱你" }, label = "FooterTextChange" ) { text -> 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 f77f5ade3..1926d8c42 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 @@ -10,6 +10,7 @@ import io.legado.app.data.repository.ExploreRepository import io.legado.app.help.config.AppConfig import io.legado.app.model.BookShelfState import io.legado.app.ui.book.search.BookKey +import io.legado.app.utils.exploreLayoutGrid import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import splitties.init.appCtx sealed class BookFilterState(val id: Int) { data object SHOW_ALL : BookFilterState(0) @@ -60,7 +62,13 @@ class ExploreShowViewModel( val selectedKindTitle = _selectedKindTitle.asStateFlow() private val _layoutState = MutableStateFlow(AppConfig.exploreLayoutState) // 0=列表, 1=网格 val layoutState: StateFlow = _layoutState.asStateFlow() - + private val _gridCount = MutableStateFlow(appCtx.exploreLayoutGrid) + val gridCount = _gridCount.asStateFlow() + val isEnd: StateFlow = _isEndStateFlow.asStateFlow() + fun saveGridCount(value: Int) { + appCtx.exploreLayoutGrid = value + _gridCount.value = value + } val uiBooks = combine( _rawBooks, _filterState, @@ -71,13 +79,6 @@ class ExploreShowViewModel( } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) - val isRawDataEmptyAndEnd = combine( - _rawBooks, - _isEndStateFlow - ) { rawBooks, isEndValue -> - rawBooks.isEmpty() && isEndValue - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) - val shouldTriggerAutoLoad = combine( _isLoading, uiBooks, @@ -138,21 +139,14 @@ class ExploreShowViewModel( } fun loadMore(isRefresh: Boolean = false) { - if (_isLoading.value || (_isEndStateFlow.value && !isRefresh)) return + val source = bookSource + val url = exploreUrl + if (source == null || url == null || _isLoading.value || (_isEndStateFlow.value && !isRefresh)) return viewModelScope.launch { _isLoading.value = true _errorMsg.value = null - val source = bookSource - val url = exploreUrl - - if (source == null || url == null) { - _isLoading.value = false - _errorMsg.value = "源或URL为空" - return@launch - } - if (isRefresh) { page = 1 _isEndStateFlow.value = false @@ -165,16 +159,21 @@ class ExploreShowViewModel( _isEndStateFlow.value = true } else { repository.saveSearchBooks(newBooks) - val currentList = if (page == 1) emptyList() else _rawBooks.value - if (page == 1) { - _rawBooks.value = newBooks.distinctBy { it.bookUrl } + + val currentList = _rawBooks.value + val existingUrls = currentList.map { it.bookUrl }.toSet() + + val uniqueNewBooks = newBooks + .filter { it.bookUrl !in existingUrls } + .distinctBy { it.bookUrl } + + if (uniqueNewBooks.isEmpty()) { + _isEndStateFlow.value = true } else { - val existingUrls = currentList.map { it.bookUrl }.toSet() - val uniqueNewBooks = newBooks.filter { it.bookUrl !in existingUrls } _rawBooks.value = currentList + uniqueNewBooks + page++ + _isEndStateFlow.value = false } - page++ - _isEndStateFlow.value = false } } .onFailure { diff --git a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt index a73826b7e..b44410399 100644 --- a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt @@ -434,6 +434,20 @@ val Context.bookshelfLayoutGrid: Int AppConfig.bookshelfLayoutGridPortrait } +var Context.exploreLayoutGrid: Int + get() = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + AppConfig.exploreLayoutGridLandscape + } else { + AppConfig.exploreLayoutGridPortrait + } + set(value) { + if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + AppConfig.exploreLayoutGridLandscape = value + } else { + AppConfig.exploreLayoutGridPortrait = value + } + } + fun Context.themeColor(attr: Int): Int { val typedValue = TypedValue() theme.resolveAttribute(attr, typedValue, true)